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.
188 lines
16 KiB
Markdown
188 lines
16 KiB
Markdown
# 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`
|