Add the groundskeeper doctrine page, and a gate for the drift writing it exposed

Adds /doctrine/groundskeeper/ and a Doctrine section to hold it: the version-independent reasoning for sustaining a standing demonstration estate, in this site's own inline-SVG idiom. Four diagrams carry the two verbs on a timeline with backdating struck out, the decay-mode to surface to verb map, the catalog-to-estate boundary, and one upkeep cycle with the operator as the only accented node.

The dividing test is version independence, and it is why the page lands here rather than only in the catalog: a fact belongs on it only if it stays true when the engine version changes. Field names, input defaults and keeper vocabulary move with the engine, so the page links the component's own documentation once and pins no version. CATALOG-VERSION is untouched.

Three claims were corrected against the engine source before this was opened. The exit-code gloss said 0 meant the plan found nothing to do; the engine returns 0 for a successful apply that mutated the estate, so a reader would have inferred the opposite of what a green apply job means, and the page's own diagram already disagreed with its prose. A guardrail was described as "the pin label, the only opt-out" when the engine has two others. And a section reported counts of engine defects and partial applies from the first landings, which no reader can verify and which move every time an estate runs; it now argues from the design instead.

Writing the page surfaced an older defect. STATIC_PAGES in site/integrations/llms-txt.ts and the nav in site/src/lib/sections.ts are two hand-maintained lists of the same pages with nothing comparing them, and three had drifted out of the agent surface while rendering normally in the sidebar: principles/timelessness, design/diagrams and design/authoring. /llms-full.txt omitted their prose and no /<route>/index.md was emitted, so an agent reading only the bundle could not see three pages a human reader can. The drift recurred once already, across a release.

just validate-agent-surface closes it, fatal in both directions. Forward is the bug that shipped twice. Reverse is fatal because the integration hides it: a STATIC_PAGES entry whose route has no built page calls logger.warn and continues, so a stale entry degrades the bundle behind one line inside a thousand lines of npm output. Bundle-only routes get an explicit BUNDLE_ONLY allowlist, empty today and verified empty, rather than a softened check.

Standalone scripts/validate-agent-surface.py in the shape of validate-binding.py: PEP 723 inline metadata, the reasoning in the module docstring, main() -> int, invoked from the justfile as uv run --quiet with a python3 fallback. Node was available and deliberately not used: the CI validate job runs on alpine:3.24 with no node, and just validate runs before build does npm ci, so a node-based check would either not run in CI or only run after the build it gates. The extractor is a bracket-balanced, comment- and string-aware scanner rather than a regex, and it is fail-closed: an element shape it cannot model exits non-zero naming what to extend, so the fromStandard exemption cannot widen silently.

Verified able to fail. A deleted STATIC_PAGES entry, an entry with no nav home, and a nav row removed while the bundle kept its route each exit 1 naming the exact route; the same deletion driven through just validate stops the aggregate before validate-release-notes and audit-expiry run. Seventeen fixture cases run as --selftest inside just validate rather than as a transcript, eight shapes it must read and nine it must refuse, and the selftest was itself mutation-tested against five deliberate parser regressions to confirm it bites. Full just ship is green: the validate aggregate now runs nine recipes, 20 standards, agent surface 407,193 bytes with no stale-route warnings.

Merge request reports

Loading
Loading