Files
sukr/docs/plans/api-stabilization.md
Timothy DeHerrera 5c3299b774 docs: close API stabilization plan review and fix stale refs
Close plan review: fix Decisions table 404 naming (content/404.md
→ content/_404.md), check off Verification items, log
collect_items() quadruple-call as technical debt, fill in
Retrospective.

Fix post-changeset staleness: AGENTS.md YAML→TOML reference,
add [nav]/[feed]/[sitemap] config tables to AGENTS.md example,
mark charter workstream 1 complete.
2026-02-15 09:52:39 -07:00

188 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PLAN: sukr 1.0 API Stabilization
<!--
Source sketch: .sketches/2026-02-13-api-stabilization.md
Selected approach: B+ (essential features + cleanup + aliases + date validation)
Key pivot: YAML frontmatter → TOML frontmatter (CHALLENGE finding)
-->
## Goal
Implement the pre-1.0 API changes required to stabilize sukr's public contract: switch frontmatter from hand-rolled YAML to serde-backed TOML, normalize template variable naming, add missing features (draft mode, 404 page, tag listing pages, aliases, date validation, feed/sitemap config), remove dead code, and add template fallback behavior. After this work, the five public surfaces (site.toml schema, frontmatter fields, template variables, CLI, content directory conventions) are locked — post-1.0 breaking changes require explicit user approval.
## Constraints
- Pre-1.0: breaking changes are acceptable now but the goal is to make them unnecessary after this work
- Suckless philosophy: no speculative features, no new dependencies unless already transitively present
- `tree-house` (git dep) and `lightningcss` (alpha) are accepted risks — do not attempt to resolve
- `chrono` is acceptable for date validation — it's already a transitive dependency via `tera`
- Every surface stabilized is a surface committed to maintaining
## Decisions
| Decision | Choice | Rationale |
| :--------------------- | :------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Frontmatter format | **TOML** (replacing hand-rolled YAML) | `toml` crate + serde already in deps. Eliminates the fragile hand-rolled parser. Every future field is just a struct field with `#[derive(Deserialize)]`. |
| Frontmatter delimiter | `+++` | Hugo convention for TOML frontmatter. Unambiguous — no risk of confusion with YAML `---` or Markdown horizontal rules. |
| Template naming | Mirror site.toml structure (`config.nav.nested` not `config.nested_nav`) | Consistency between config and templates; pre-1.0 is only window for this break |
| Date type | `Option<chrono::NaiveDate>` with custom `deserialize_date` fn for TOML native dates | Parse, don't validate. Custom serde deserializer accepts `toml::Datetime`, extracts date, constructs `NaiveDate`. Invalid dates fail at deser. |
| Draft filtering | `draft: bool` (`#[serde(default)]`) in `Frontmatter`, filter in `collect_items()` and `discover()` | Filter early so drafts don't appear in nav, listings, sitemap, or feed. |
| Feed/sitemap config | `[feed]` and `[sitemap]` tables with `enabled` boolean in `SiteConfig` | Users need opt-out. Default `true` preserves backward compat. |
| Tag listing pages | Generate `/tags/<tag>.html` using a new `tags/default.html` template | Minimal approach — one template, one generation loop. No pagination. |
| Aliases | `aliases = ["/old/path"]` in frontmatter, generate HTML redirect stubs | Standard pattern (Hugo). `<meta http-equiv="refresh">` redirect. |
| 404 page | `content/_404.md``404.html` at output root | Underscore prefix convention (matches `_index.md`). Most static hosts auto-serve `/404.html`. |
| Template fallback | Try `section/<type>.html`, fall back to `section/default.html` | Removes the requirement to create a template for every section_type. |
| Dead template cleanup | Delete `section/features.html` and `homepage.html` | Byte-for-byte duplicate and dead code respectively. |
| `base_url` duplication | Remove top-level `base_url` template variable | Single source of truth via `config.base_url`. |
| Tags syntax | `tags = ["foo", "bar"]` (flat TOML array) | Replaces nested `taxonomies.tags` YAML. Simpler, no indirection. |
## Risks & Assumptions
| Risk / Assumption | Severity | Status | Mitigation / Evidence |
| :------------------------------------------------------ | :------- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TOML frontmatter breaks all 17 existing content files | **HIGH** | Validated | One-time mechanical migration. All files are simple key-value; no complex structures. Migration is part of Phase 1. |
| Documentation pages embed YAML frontmatter examples | MEDIUM | Validated | `configuration.md`, `content-organization.md`, `getting-started.md`, `feeds.md`, `sections.md`, `sitemap.md`, `templates.md` all contain example frontmatter in their body text. Must update these doc examples too. |
| Template naming break affects existing user templates | MEDIUM | Validated | Only `docs/templates/base.html` references `config.nested_nav` and `base_url`. The docs site is the only known deployment. |
| Tag listing pages need a template but no default ships | MEDIUM | Unvalidated | Must design and ship `tags/default.html` in `docs/templates/`. |
| `collect_items()` triple-call not fixed by this plan | LOW | Accepted | Performance issue, not API concern. Deferred. |
| Removing `base_url` top-level variable breaks templates | MEDIUM | Validated | Only `docs/templates/base.html` uses it. |
## Open Questions
All resolved during CHALLENGE. See CHALLENGE Notes below.
### CHALLENGE Notes
Items presented to nrd for decision:
1. **YAML → TOML frontmatter switch.** The hand-rolled YAML parser can't handle `aliases` lists. Rather than extending a fragile parser, nrd decided to switch frontmatter to TOML — the `toml` crate and serde are already dependencies. This eliminates the parser entirely and replaces 70 lines of hand-rolled code with `#[derive(Deserialize)]`. **Decision: switch to TOML.**
Items validated by codebase investigation:
2. **`ConfigContext` normalization is clean.** Flat struct at `template_engine.rs:139-155`. Refactoring to nested `nav: NavContext { nested, toc }` is straightforward.
3. **`base_url` duplication confirmed.** `template_engine.rs:118` injects standalone `base_url`. `ConfigContext.base_url` at line 142 is canonical. Removal safe.
4. **Template variable `content` is correct.** Lines 57 and 83 inject rendered HTML as `content`, matching the sketch contract.
5. **Phase ordering is sound.** No circular dependencies between phases.
6. **17 content files need migration.** All simple frontmatter — no nested structures beyond `taxonomies.tags` (which becomes flat `tags = [...]`). 7 files also contain embedded YAML examples in body text that need updating.
## Scope
### In Scope
1. **TOML frontmatter switch** — replace hand-rolled YAML parser with `#[derive(Deserialize)]` + `toml::from_str`, migrate 17 content files, change delimiter from `---` to `+++`
2. Template naming normalization (`config.nested_nav``config.nav.nested`, add `config.nav.toc`, remove duplicate `base_url`)
3. `draft` frontmatter field + filtering
4. `aliases` frontmatter field + redirect stub generation
5. Date validation (YYYY-MM-DD) at parse time
6. `[feed].enabled` and `[sitemap].enabled` config
7. `content/_404.md``404.html` support
8. Tag listing page generation (`/tags/<tag>.html`)
9. Template section fallback (`section/<type>.html``section/default.html`)
10. Dead template removal (`section/features.html`, `homepage.html`)
11. Tests for all new and changed behavior
### Out of Scope
- Pagination
- Asset co-location
- i18n
- Verbose/quiet CLI flags
- `collect_items()` caching
- `ContentKind` refactoring
- Magic string enum extraction
## Phases
1. **Phase 1: TOML Frontmatter & Config Normalization** — replace the parser, migrate content, fix naming
- [x] Replace `Frontmatter` struct with `#[derive(Deserialize)]`
- [x] Add new fields: `draft: bool` (`#[serde(default)]`), `aliases: Vec<String>` (`#[serde(default)]`), keep all existing fields
- [x] Replace `parse_frontmatter()` with `toml::from_str::<Frontmatter>()`
- [x] Update `extract_frontmatter()` to detect `+++` delimiters instead of `---`
- [x] Add date validation: custom `deserialize_date` fn for TOML native dates → `chrono::NaiveDate`
- [x] Change `tags` from `taxonomies.tags` nesting to flat `tags = ["..."]` (direct TOML array)
- [x] Migrate all 17 content files from YAML (`---`) to TOML (`+++`) frontmatter
- [x] Update embedded frontmatter examples in documentation pages (7 files)
- [x] Add `FeedConfig` and `SitemapConfig` structs to `config.rs` with `enabled: bool` (default `true`)
- [x] Wire feed/sitemap config into `SiteConfig` deserialization
- [x] Gate feed generation in `main.rs` on `config.feed.enabled`
- [x] Gate sitemap generation in `main.rs` on `config.sitemap.enabled`
- [x] Refactor `ConfigContext`: flat `nested_nav: bool` → nested `nav: NavContext { nested, toc }`
- [x] Remove duplicate `base_url` top-level template variable injection
- [x] Update `docs/templates/base.html`: `config.nested_nav``config.nav.nested`, `base_url``config.base_url`
- [x] Delete `docs/templates/section/features.html` and `docs/templates/homepage.html`
- [x] Add template section fallback in `render_section`: try `section/<type>.html`, fall back to `section/default.html`
- [x] Update/fix all existing tests to use TOML frontmatter
- [x] Add new tests: TOML parsing, date validation (valid + invalid), feed/sitemap config gating
- [x] Verify all 69 existing tests pass (updated for TOML)
2. **Phase 2: Draft & Alias Features** — implement filtering and redirect generation
- [x] Filter items where `draft == true` from `collect_items()` results
- [x] Filter drafts from `SiteManifest.posts` during discovery
- [x] Filter drafts from nav discovery (`discover_nav()`)
- [x] Filter drafts from sitemap entries
- [x] Filter drafts from feed entries
- [x] Generate HTML redirect stubs for each alias path (`<meta http-equiv="refresh">`)
- [x] Add tests: draft filtering (excluded from listing, nav, feed, sitemap)
- [x] Add tests: alias redirect generation (valid HTML, correct target URL)
3. **Phase 3: 404 & Tag Pages** — new content generation features
- [x] Detect `content/_404.md` in content discovery, treat as special page
- [x] Render `_404.md` to `404.html` in output root
- [x] Collect all unique tags across content items during build
- [x] Create `tags/default.html` template in `docs/templates/`
- [x] Generate `/tags/<tag>.html` for each unique tag with list of tagged items
- [x] Add tag listing page entries to sitemap (if enabled)
- [x] Add tests: 404 page generation
- [x] Add tests: tag listing page generation (correct paths, correct items per tag)
- [x] End-to-end: build `docs/` site and verify all outputs
## Verification
- [x] `cargo test` — all existing tests pass (updated for TOML frontmatter)
- [x] `cargo test` — all new tests pass (16 new tests across 3 phases: 69 → 84)
- [x] `cargo clippy -- -D warnings` — no warnings
- [x] `cargo build` — clean compilation
- [x] End-to-end: build `docs/` site with `cargo run`, verify:
- [x] `public/sitemap.xml` exists (default enabled)
- [x] `public/atom.xml` — N/A (no blog sections in docs site; feed gating verified by unit tests)
- [x] `public/404.html` exists (with \_404.md in docs/content)
- [x] Templates use `config.nav.nested` (not `config.nested_nav`)
- [x] Templates use `config.base_url` (not bare `base_url`)
- [x] No `section/features.html` or `homepage.html` templates remain
## Technical Debt
<!--
Populated during execution. Empty at plan creation.
-->
| Item | Severity | Why Introduced | Follow-Up | Resolved |
| :------------------------------------ | :------- | :----------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | :------: |
| `collect_items()` called 4× per build | LOW | Tag collection (commit 7) and alias generation (commit 6) each added a new call site, growing the original 2-call pattern to 4 | Cache section items in `SiteManifest` — deferred to Error Hardening or Performance pass | ☐ |
## Retrospective
### Process
The plan held up without requiring replanning during execution. The YAML → TOML pivot — the single largest decision — was identified during CHALLENGE (in `/plan`), not mid-execution. Once baked into the plan, execution was linear across all 3 phases.
Phase sequencing was sound: Phase 1 (parser + config) → Phase 2 (features depending on new fields) → Phase 3 (independent new features). No circular dependencies materialized.
Estimates were accurate: 3 phases, 8 commits predicted and delivered. CHALLENGE caught the risks that materialized — the 17-file content migration (flagged HIGH) was the most labor-intensive step but proceeded mechanically.
### Outcomes
**Unexpected debt:** `collect_items()` grew from 2 to 4 call sites. The original plan flagged the triple-call as "Accepted — LOW" but execution added a fourth. Logged in Technical Debt above.
**What we'd do differently:** Commit 3 (`45448cc`) bundled 4 concerns (config refactor, base_url dedup, dead template deletion, section fallback). Tests for that commit's new behavior landed in commit 4. A 2-commit split would have maintained stricter atomicity without being excessive.
### Pipeline Improvements
- AGENTS.md has stale fragment paths (`.agent/predicates/fragments/``.agent/personas/`). Discovered during predicate refresh — should be fixed independently.
- `/plan-review` should remind agents to mark Verification checkboxes during execution, not defer to review time.
## References
- Charter: `docs/charters/sukr-v1.md`
- Sketch: `.sketches/2026-02-13-api-stabilization.md`