diff --git a/context/code/doc-site.md b/context/code/doc-site.md index 61667fcb..2304f3cf 100644 --- a/context/code/doc-site.md +++ b/context/code/doc-site.md @@ -40,6 +40,21 @@ Neither is referenced by any published document except as a row in `repo-map.md`'s file inventory, which is inside a code fence, so leaving them out creates no dangling link. That was checked rather than assumed. +**And "not published" is now derived rather than assumed** (improvise-res9). +The generator walks `context/**/*.md` plus `docs/design-notes.md`, subtracts +the manifest, and fails unless the remainder is exactly the `OMITTED` map in +the script — which carries the reason beside the name, so this section and the +code cannot drift into disagreeing. An allowlist and not a count, because an +omission is a decision that should have to be argued in code; and the map is +checked in *both* directions, because an entry naming a file nobody can find +asserts a reason for nothing, which is the failure a dead ticket id is. + +The check was latent when it was filed and **live by the time it was written**: +`context/code/kb-tool.md` had been added after this table was drafted and was +publishing nothing, with no argument for its absence anywhere. The table above +says `context/code/*.md` is published; the manifest disagreed and nothing could +see it. That is the whole failure mode in one file. + **Test evidence is the one judgement call worth stating.** The argument for leaving it out is that 501 entries about individual tests are noise to a reader who wanted to know what a pivot table is. The argument for keeping it is @@ -136,6 +151,13 @@ filesystem instead. That is the whole reason `assemble.sh` runs the generator version skipped those 48 links while still counting them as checked, behind a comment promising they were "checked below" when there was no below. +That ordering is itself checked now (improvise-res9). The generator reads +`web/assemble.sh` and refuses to run unless its own invocation is that script's +last command. PR #63 appended a loop and it happened to land above the +generator; one landing below would have made the outward-link check pass +against a half-built tree, silently and in the one direction this section says +must not go unnoticed. + **And the link check is not sufficient, which is the more important lesson.** It answers one question — does every href resolve — and a defect outside that question is invisible to it however carefully it is run. The worked example is diff --git a/context/test-evidence/tooling.md b/context/test-evidence/tooling.md index 0f0b61c6..5409bfbb 100644 --- a/context/test-evidence/tooling.md +++ b/context/test-evidence/tooling.md @@ -46,3 +46,25 @@ is forbidden to do. Fault injected, 2026-09-21: moved `web/tests/bundle/wasm_surface.test.js` out of the tree. The test failed with "web/tests/bundle/ is empty — the split lost its other half" rather than passing. Restored; green again. + +### Note — the doc site's derived checks + +`scripts/build-doc-site.mjs` carries two gates that no test names, because the +generator *is* the test: it runs on every assembly and exits non-zero. + +Faults injected, 2026-09-21, each observed: + +| injected | reported | +|---|---| +| a new `context/code/newarea.md` | `context/code/newarea.md is neither published nor a named omission` | +| `context/plan.md` moved out of the tree | `context/plan.md is listed as omitted but does not exist` | +| a line appended below the generator in `web/assemble.sh` | `web/assemble.sh must invoke this generator LAST`, naming the offending command | + +The second arm is the one that would not get written first, and it is the point: +without it, deleting or renaming an omitted file leaves an entry asserting a +reason for a file nobody can find — the same failure as a dead ticket id, which +`AGENTS.md` already calls worse than no id. + +The first arm was filed as latent and was live when it was written: +`context/code/kb-tool.md` had been unpublished, unargued and unnoticed since it +was added. diff --git a/scripts/build-doc-site.mjs b/scripts/build-doc-site.mjs index e82dada9..14045806 100644 --- a/scripts/build-doc-site.mjs +++ b/scripts/build-doc-site.mjs @@ -11,7 +11,7 @@ // The reorder also moved WHERE a failure shows: a dead link now leaves a site/ // that looks complete with a partial docs/, where before it left one with no // app/. Contained only because assemble.sh is `set -eu` and CI gates on it. -import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, statSync } from "node:fs"; +import { readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, existsSync, statSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; @@ -59,6 +59,8 @@ const SECTIONS = [ ["context/code/clients.md", "code/clients", "The clients"], ["context/code/tui.md", "code/tui", "The TUI"], ["context/code/gp.md", "code/gp", "gp-tree and improvise-gp"], + ["context/code/kb-tool.md", "code/kb-tool", "The kb tool"], + ["context/code/tooling.md", "code/tooling", "The repository's own checkers"], ], }, { @@ -74,6 +76,7 @@ const SECTIONS = [ ["context/test-evidence/clients.md", "test-evidence/clients", "The clients"], ["context/test-evidence/tui.md", "test-evidence/tui", "The TUI"], ["context/test-evidence/gp.md", "test-evidence/gp", "gp-tree and improvise-gp"], + ["context/test-evidence/tooling.md", "test-evidence/tooling", "The repository's own gates"], ], }, { @@ -87,6 +90,56 @@ const SECTIONS = [ const PAGE_BY_SOURCE = new Map(); for (const s of SECTIONS) for (const [src, slug] of s.docs) PAGE_BY_SOURCE.set(src, `${slug}.html`); +// An omission is a DECISION, so it is restated here beside the manifest rather +// than only in doc-site.md section 2. An allowlist and not a count: a new +// omission has to be argued in code, and a vanished one has to be removed -- +// a stale entry asserting a reason for a file nobody can find is the failure a +// dead ticket id is (improvise-res9). +const OMITTED = new Map([ + ["context/plan.md", "a prompt, not a plan; cites a context/SPEC.md that no longer exists"], + ["context/cloud-agents.md", "operational notes for agents, not about the program"], +]); + +// Derived, because the manifest only ever grows and a doc added without a line +// here publishes nothing and says nothing: build green, link check green, doc +// simply absent. This runs at module scope, before any page is generated, so +// it answers the manifest question before the outward-link check answers a +// different one -- a missing doc is actionable wherever you ran the generator. +{ + const onDisk = []; + (function walk(d) { + for (const e of readdirSync(join(root, d), { withFileTypes: true })) { + const rel = `${d}/${e.name}`; + if (e.isDirectory()) walk(rel); + else if (e.name.endsWith(".md")) onDisk.push(rel); + } + })("context"); + onDisk.push("docs/design-notes.md"); + const unaccounted = onDisk.filter((f) => !PAGE_BY_SOURCE.has(f) && !OMITTED.has(f)); + const vanished = [...OMITTED.keys()].filter((f) => !existsSync(join(root, f))); + if (unaccounted.length || vanished.length) { + for (const f of unaccounted) console.error(`doc-site: ${f} is neither published nor a named omission`); + for (const f of vanished) console.error(`doc-site: ${f} is listed as omitted but does not exist`); + process.exit(1); + } +} + +// The generator's link check resolves ../index.html and ../app/ against the +// filesystem, so it must run once the rest of the tree is there. PR #63's +// appended loop happened to land above it; one landing below would make the +// outward-link check pass against a half-built tree, silently. +{ + const cmds = readFileSync(join(root, "web", "assemble.sh"), "utf8") + .split("\n") + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith("#")); + if (!cmds[cmds.length - 1].includes("build-doc-site.mjs")) { + console.error("doc-site: web/assemble.sh must invoke this generator LAST;"); + console.error(`doc-site: its last command is instead: ${cmds[cmds.length - 1]}`); + process.exit(1); + } +} + // --------------------------------------------------------------------------- // The ticket index. `.beads/issues.jsonl` is a passive export and lags the Dolt // database, so an id it does not carry is rendered as plain text rather than a