Uh oh!
There was an error while loading. Please reload this page.
feat(scripts): corpus-wide route-spelling detector for published prose, wired advisory - #11864
Conversation
#11050) A root-level gate (check-doc-authoring.mjs is the model) that extracts every /api/v1 wire-path literal from content/docs/** (minus releases/) and skills/**, and flags a literal that matches a route-ledger row's shape while differing from its spelling — the drift class #9180 step 2 exposed and #10178's two-file pin cannot generalize to. Findings are advisory-ready via --advisory; the self-test, dead/empty roots, ledger parse floors and the per-root literal floors are hard failures in every mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
…red in reverse verification The lenient direction let the param-spelled plural ride the same-arity :type/:section/:name/published row into an exact verdict; pinned with the decoy row in the self-test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
…riage ruling Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
ACCEPT — PM review, The open question: ruling B — soak advisory, flip on maintainer order. Your recommendation stands.Triage's wording ("advisory until the population is clean") is literally satisfied — the population is clean. I am still ruling B, for reasons the literal reading does not cover:
⛔ I am deliberately not putting this in the maintainer's decision box yet: there is nothing to decide on until the soak produces evidence. It becomes a decision when it has one. Both maintainer-ruled fences held — checked in the source, not the report
⭐ The wiring gets the one thing that could have made "advisory" worthless right. From the workflow comment: "Advisory covers FINDINGS only: the self-test, a dead root, an evaporated corpus or ledger and the literal-count floors are hard failures in every mode." An advisory gate that also swallowed "the scan reached nothing" would report clean whether it worked or not — which is the exact failure this card exists to detect, one level up. Getting that split right is the difference between a soak and a placebo. Zone 2 was answered before the design, which is why there is no heuristicThe assumption I flagged as load-bearing — whether the ledgers expose enough structure for shape-matching — was verified first: both expose per-row Population: 585 ⭐ The reverse verification caught a real defect, and the first run's red was true
The defect: the lenient "placeholder matches literal" rule let the param-spelled plural ride a same-arity The asymmetry argument at An ablation that goes green on its first run tells you nothing. This one went red, and the red was the product. Co-tenancy resolved as predictedYour step lands at line 988; PR #11716's is at 3594 in a 4091-line file. Disjoint, as I measured at dispatch. Recorded
Flipping to ready; arming once every check run completes green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11050
What this adds
scripts/check-doc-route-spelling.mjs— a root-level gate (thecheck-doc-authoring.mjsmodel; a package test would needcontent/docs/**andskills/**as declared turbo inputs) that extracts every/api/v1wire-path literal from the published corpora (content/docs/**minusreleases/, plusskills/**— the #11050 ruling's initial set) and judges each against the two route ledgers. A literal is flagged only when it matches a ledger row's shape while differing from its spelling — the drift class #9180 step ② exposed: two published sites kept teaching the retired plural, and the hand sweep that found them found a third spelling (/metadata/objects/...) invisible to a grep for the plural. #10178's pin (seepackages/rest/src/meta-state-route-doc-spelling.test.ts) stays as the derived two-file anchor for that one route; this is the corpus-wide generalization it deliberately was not.Wired into
Lint & Repo Gatesas a step (an unenrolled new job would be an advisory check-run with no signal — the #5617 shape), invoked asnodeper the #9465 fence on rootpackage.json.Zone 2 answer — the ledgers do expose enough structure
Both ledgers carry
route: 'VERB /path/:param'rows; the runtime ledger'sabsolute: trueand* /x/**wildcard rows are honored (absolute rows unprefixed, wildcard rows become family prefixes that exempt their subtrees, since their real tables live elsewhere — better-auth's, the cloud repo's, metadata-declared endpoints). Parsed textually so the gate has zero dependencies; parse floors make an under-parse (quoting migration, moved file) fail naming the ledger instead of scanning a smaller authority.Measurement first (triage ruling), and the population is clean
Enumerated before wiring, at
a6eca9223: 585/api/v1literals across 442 files (content/docs 521, skills 64) against 172 ledger rows + 4 wildcard families → 326 exact, 179 family-covered, 40 surface mentions, 40 unmatched (recorded, not judged — real routes outside the two ledgers, generic pattern teaching, deliberate refusal examples), 0 flagged. The gate ships green.Advisory, per the triage ruling — the lint.yml step runs
--advisory: findings print loudly and exit 0; the self-test, dead/empty roots, ledger parse floors and per-root literal floors (#4932) stay hard failures in every mode. Strengthening later is deleting the--advisoryflag, nothing else.Constraints respected (both maintainer rulings, by construction)
route-spelling-allowfor pages that must show a non-canonical spelling on purpose (migration guides, refusal examples). The dispatch()-side plural stays exactly as the 2026-08-17 re-weigh left it.META_URL_TO_SINGULARfold: the gate never touches it; the header names the distinction the runtime ledger row calls the specific error to avoid./meta/viewesteaching a 400, plural object names under/data/:object) is a pinned non-goal — parameter values are not the ledgers' vocabulary.Non-vacuity — fail-before / pass-after on the real corpus, from committed state
Mutating
content/docs/protocol/objectql/state-machine.mdx(mutation proven on disk: singular anchor 1→0, plural 0→1, blob4b69171d→b35236e9; restores byte-identical back to4b69171d, porcelain clean, undertrap … EXIT INT TERM):--advisoryexit 0 with the findings printed;metadata/objectsthird spelling → exit 1, naming both differing segments against the canonical row;The first round of this verification caught a real matcher defect: the lenient "prose placeholder matches row literal" rule let the param-spelled plural ride the same-arity
:type/:section/:name/publishedrow into an exact verdict. The rule is now strict (a placeholder never matches a row literal), and the regression is pinned in the self-test with that decoy row. Cost measured on the real corpus: 7 literals moved from lenient-exact to honest unmatched, zero new flags.Verification at
bf9fc922(HEAD of this PR; the gate union was derived and run at this commit)node scripts/pm/dispatch-gates.mjs(no paths — tool derives the changeset itself) → 20 matched families, all run locally, all exit 0, includingcheck:required-contexts,check:aggregator-roster,check:step-collectors,check:workflow-status-functions,check:type-check-coverageandcheck:type-check-debt(the last after building the packages closure, exactly as its refusal text prescribes; its surplus note is pre-existing ledger bookkeeping, untouched by this diff).node scripts/check-doc-route-spelling.mjs --self-test→ green (its own printed verdict line: "… all hold.").pnpm check:nul-bytes→ "OK (scanned 6599 text file(s) …)".scripts/**+.github/workflows/**only, nothing published changes;skip-changesetlabel applied.lint.yml co-tenancy note
PR #11716 also touches
.github/workflows/lint.yml(an 18-line insertion in the spec API-surface job region, ~line 3594). This step lands with the doc/corpus gates near line 990 — disjoint regions; no conflict observed at branch time.Out of scope, filed
trigger-api'sPOST /api/v1/automation/hooks/:flowName/:hookIdhost-server mount taught in a published skill and carried by no route ledger (the Route audit tranche 3: autonomously-mounted service routes (service-storage / service-i18n) — #3587 follow-up #3636/Three ledgered /meta routes are never mounted and die in the/meta/:typecatch-all — the route audit can't see this class because it treats the ledger as ground truth for what's mounted #7526 unledgered-mount class). Recorded there, untouched here.Generated by Claude Code