ADR-157: sitemap gate semantics + auto-update vs manual-curation
Status: Proposed Date: 2026-05-06 Related: warp#1551 (parent EPIC, fleet sitemap-as-source-of-truth), ADR-155 (S0.1 sister: sitemap.json schema), ADR-156 (S0.2 sister: sitemap UX), warp#1486 (sister structural pattern: docs-validate auto-regen gate), warp#1406 (rollover precedent: title-format-gate 1-week soft-warn)
Context
Sub-ticket S0.3 of EPIC warp#1551 (fleet sitemap-as-source-of-truth) — the third and last of the Phase 0 ADRs. ADR-155 (S0.1, shipped 2026-05-06 PR#664) names WHAT data exists in sitemap.json; ADR-156 (S0.2, shipped 2026-05-06 PR#668) names HOW it renders; this ADR names HOW IT STAYS TRUE OVER TIME — the bidirectional CI gate that makes the sitemap structural rather than documentary. Without locking the gate semantics before Phase 1 substrate (S1.1 SvelteKit discovery, S1.2 Go discovery, S1.3 sitemap-validate workflow template) ships, the discovery scripts could land without a corresponding workflow + rollover plan, and the structural-truth claim of the EPIC would not hold.
Operator (jon, 2026-05-06) framed the parent EPIC as "all routes need to go on a sitemap page like Windows Explorer view, and this is for all sites. The system structurally checks and if a route is missing from it it doesn't pass." That last sentence is this ADR's scope. The failure mode being retired is the same shape warp#1486's docs-validate gate retired for _index.json: a committed truth-file drifts silently from its source, consumers read stale data, the page that depends on it lies, and nothing fails until someone notices in production. This ADR mirrors warp#1486's auto-regen pattern (gate runs the discovery script, diffs against committed file, fails on drift), and warp#1406's rollover pattern (1-week soft-warn before the gate goes hard) so day-1 adoption never blocks ordinary work. Adopting both patterns is deliberate: each is well-understood in the fleet and each retired a class of pain.
Decision
The sitemap-validate CI workflow runs the per-stack discovery script, regenerates sitemap.json, diffs against the committed file, and fails the PR on drift; humans never hand-edit sitemap.json (metadata is declared at the source via +page.ts export const sitemap = {...} for SvelteKit or RegisterSitemap(path, meta) for Go); each repo gets a 1-week soft-warn rollover from first-adoption before the gate goes hard; an audited --skip-sitemap-validate "<reason ≥ 50 chars>" flag exists for genuinely-unfixable cases. The contract has three parts: the auto-update-or-fail loop, the soft-warn rollover, and the override semantics.
1. Auto-update-or-fail (sister to docs-validate from warp#1486)
The committed sitemap.json is the gate's single source of truth. On every PR that touches the discovery surface (src/routes/** for SvelteKit, the Go module mount-points), the workflow runs the discovery script, writes a fresh sitemap.json to a temp path, and diffs against the committed copy. Any difference fails the gate with a unified-diff-shaped error message naming the route(s) that drifted. The same shape — "regenerate, diff, fail on drift" — is the warp#1486 docs-validate contract for docs-viewer/_index.json; the implementation surface is different (different discovery script, different committed file) but the failure-mode contract is identical, so operators who learned one already understand the other.
SAMPLE GATE FAILURE
✗ sitemap-validate: drift detected between source and committed sitemap.json
Committed sitemap.json says:
+ /board/[id] "Item Detail" group=primary role=null
+ /board/new "Create Item" group=primary role=null
Discovery script emits:
+ /board/[id] "Item Detail" group=primary role=null
+ /board/new "New Item" group=primary role=null ← drift
+ /board/archive "Archive" group=primary role=null ← new
Fix: regenerate sitemap.json — `gyrum-sitemap-emit > sitemap.json`
Then re-commit. Do NOT hand-edit sitemap.json directly.
The hand-edit prohibition is the rule that keeps the gate honest. Hand-edits would make the committed file the truth and the source the lie, which inverts every consumer's assumption (the /sitemap renderer reads the committed file; the cross-fleet aggregator reads the committed file; the gate diffs against the committed file). Every hand-editable surface eventually grows a hand-edited drift; the structural fix is to make the committed file's regeneration the only legal write path.
2. Soft-warn rollover (sister to warp#1406)
The first time a repo adopts the workflow (detected by absence of a marker file at .gyrum/sitemap-rollover-began), the gate runs in soft-warn mode for 1 calendar week: it computes the diff and logs it as a workflow annotation, but exits 0 so the PR is never blocked by drift the operator did not yet know to commit. The marker file is written by the workflow on first run with an ISO-8601 timestamp; the workflow reads the timestamp on subsequent runs and switches to hard-fail mode when more than 7 days have passed. The same shape as warp#1406's title-format-gate rollover, which gave every repo 1 week of soft-warn before the title-prefix-gate went hard fleet-wide.
SAMPLE SOFT-WARN MESSAGE (during rollover window)
⚠ sitemap-validate: drift detected, but in soft-warn rollover mode
(rollover began 2026-05-08; gate goes hard 2026-05-15)
Committed sitemap.json drifts from discovery output. Treating as
warning — exit 0 — until rollover completes.
To fix now: gyrum-sitemap-emit > sitemap.json && git add sitemap.json
The 1-week window is a deliberate choice over both shorter (day) and longer (month). One day misses the operator who is on vacation when adoption ships; one month gives stale-state too long to accumulate without correction. One week is what worked for title-format-gate, and the fleet's calendar rhythm (weekly status, weekly retros) means every operator hits at least one prompt to look at their soft-warn output during the window.
3. Override semantics — --skip-sitemap-validate "<reason ≥ 50 chars>"
For the rare case where a route exists for build-time-only reasons (e.g. a SvelteKit prerender hook that compiles to a static asset, never a runtime request handler) and the discovery script flags it as drift, gyrum-review-pr --skip-sitemap-validate "<reason>" provides an audited bypass. The reason must be ≥ 50 characters (same prose-quality threshold as warp#1517's --allow-collision and other audited overrides) and is appended to ~/.gyrum/findings/findings.jsonl via the existing audit pattern. The override clears THIS PR's gate; it does NOT clear the underlying drift — the operator commits to a follow-up fix (either teach the discovery script to skip the route, or hidden: true it in the source-side metadata). The override is logged with the route names that drifted so the follow-up has the same context the override needed.
The 50-char floor on reasons (vs warp#1517's 10-char floor) reflects this gate's higher false-positive cost: a missing or stale route in sitemap.json is the failure mode the entire EPIC retires, so the override threshold sits higher than warp#1517's broader collision check, which gates a much more frequent decision.
Consequences
What becomes easier. Every fleet repo that adopts the workflow gets structural drift-detection for free; the /sitemap page (per ADR-156) and the cross-fleet aggregator (Phase 4) read truth that the gate guarantees stays in sync with the source. The hand-edit-prohibition is enforced rather than documented — an operator who tries to edit sitemap.json directly hits the gate on their first PR and learns the rule structurally. Soft-warn rollover means day-1 adoption never blocks ordinary work; the gate accumulates real-state context during the window so when it goes hard, the diff is small and auditable.
What becomes harder. Every fleet repo now ships with one extra committed file (sitemap.json) and one extra CI workflow (sitemap-validate); operators who hand-edit the committed file (as they would any other JSON config) hit the gate on their first PR and have to learn the auto-regen pattern. The override flag's 50-char reason floor is friction; the friction is the point — the override is the rare-case escape hatch, not the daily convenience. The marker-file rollover convention (.gyrum/sitemap-rollover-began) is one more file every adopting repo has to commit; the convention is shared with warp#1406's title-format rollover, so operators who already know that gate already know the marker shape.
What we sign up to operate. The workflow template (S1.3) is a gyrum-setup-propagated artefact; rolling out the gate fleet-wide is the same shape as warp#1463's docs-validate fleet-rollout. The 1-week rollover window is fleet-uniform; bumping it to a different value requires a follow-up ADR. The override audit log lives at the same ~/.gyrum/findings/findings.jsonl every other audited gate writes to; overrides accumulate there and are reviewed alongside other override classes during fleet-wide audit sweeps.
Alternatives considered
Hand-edit
sitemap.jsondirectly; no auto-regen. Lost: the failure mode the EPIC retires (silent route-vs-nav drift) returns under a different name (silent source-vs-sitemap.jsondrift). Hand-edits depend on every operator remembering to edit the committed file alongside their route change; experience from warp#1486's docs-validate retrofit shows that prompt is forgotten ~30% of the time, and stale committed truth is worse than no committed truth.Author-time hook (pre-commit) instead of CI gate. Lost: pre-commit hooks are bypassable (
git commit --no-verify), and discovery scripts are stack-dependent — a SvelteKit pre-commit hook does not catch a Go drift, so each repo would need its own hook glue. The CI gate runs the same shape across every stack and cannot be bypassed without the audited override.Hard-fail from day 1 (no rollover). Lost: every repo's first adoption-PR would block on the operator's day-1 ignorance of the regenerate-then-commit step. Soft-warn rollover lets the gate ship without breaking ordinary work, while still forcing the rule's adoption inside the calendar week. Same precedent as warp#1406's title-format rollover.
Longer rollover window (1 month). Lost: month-long soft-warn lets repos accumulate weeks of stale state before the gate goes hard, multiplying the size of the eventual fix. Week-long matches the fleet's calendar rhythm and the warp#1406 precedent.
No override flag — every drift must be fixed at source. Lost: build-time-only routes (prerender hooks, generated SSG assets) genuinely don't fit the discovery contract; without an escape hatch they would force the discovery script to grow per-repo special-cases. The audited override with a 50-char reason floor keeps the escape hatch costly enough to discourage routine use.
Override does NOT log to findings.jsonl. Lost: an unaudited override is just a per-PR bypass that accumulates silently. Logging to the same audit surface every other gate uses preserves the cross-gate review pattern.
Supersedes: none — extends EPIC warp#1551; sister to ADR-155 (S0.1, schema) and ADR-156 (S0.2, UX) of the same EPIC. Superseded by: {{leave blank until a later ADR reverses this one}}