Skip to content

Commit e77635b

Browse files
claude[bot]claude
andauthored
refactor(docs-audit): a route SOURCE of two kinds — admit spec contract declarations behind a runtime-registration guard (#15274)
* refactor(docs-audit): a route SOURCE of two kinds, and admit contract declarations Rename the "registrar" concept in affected-docs.mjs to a route SOURCE with two kinds -- a registration call site (the existing filename convention) and a spec contract declaration (admitted by evidence) -- and implement the evidence route behind a runtime-registration guard. The guard is the HTTP method the declaration answers on, read beside the path, rather than a hand-kept exclusion list: it keeps out the whole class of data payloads that merely carry a `path:` key, of which the connector-action input in packages/spec/src/conversions/registry.ts is today's only instance. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(docs-audit): pin the two route-source kinds and the guard between them Adds the #11857 self-test battery (21 cases): the guard admits a contract declaring its verb beside its path, declines a connector-action input, and the counterfactual proves the parser reads that payload's tail so the guard is what declines it. Live pins for the real registry.ts, the five admitted declarations, and the measured no-op of excluding route ledgers from kind (a). Roster 28 -> 29 batteries, floor raised with the cases. Renames the concept in scripts/docs-audit/README.md to match the script. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * test(docs-audit): floor the contract-kind population so a shape pin cannot pass vacuously Ablation B (evidence route disabled) left the battery green: `every()` over an empty list is true, so the pin that says WHAT kind (b) admits said nothing about WHETHER it admits. Adds the size floor and names the declaration the storage ledger's closure rests on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * docs(docs-audit): finish the rename in the anchor provenance string The `route` anchor's own `from` note still read "its registrar handler names it" -- the one place the old word reached a user-visible string rather than a comment. Found by running the tool in normal mode on a real diff. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2bb0614 commit e77635b

2 files changed

Lines changed: 516 additions & 120 deletions

File tree

scripts/docs-audit/README.md

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ Three anchor kinds, each exact:
4949
| anchor | what it is | how it is derived |
5050
|:--|:--|:--|
5151
| `symbol` | a documentable declaration the diff touched | the top-level declaration, or a member of a top-level **container** (class / interface / type / enum / schema object), enclosing each changed line — on **both** sides of the diff, so a removed export still anchors the pages naming it. A member that is a **data property** is additionally qualified by its declaring container against the authorable surface (see below) |
52-
| `route` | a wire path the change touched | a path literal on a changed line, plus every route whose **registrar handler** references a changed symbol |
52+
| `route` | a wire path the change touched | a path literal on a changed line, plus every route whose **route-source handler** references a changed symbol |
5353
| `sdk` | the client method bound to an anchor route | the declared `route``client` rows in the repo's route ledgers |
5454

5555
The `route` and `sdk` hops are what carry the derivation across the surface boundary the
5656
package graph cannot cross: `auditMetaItem` (changed) → `GET /api/v1/meta/:type/:name/audit`
57-
(`rest-server.ts` registrar) → `meta.getAudit` (`rest-route-ledger.ts`) → the token
57+
(`rest-server.ts`, a registration call site) → `meta.getAudit` (`rest-route-ledger.ts`) → the token
5858
`api/client-sdk.mdx` actually contains.
5959

6060
**A local variable is not documentable surface.** That one rule is what drops the measured
@@ -87,7 +87,7 @@ Each anchor kind names its own origin, from the same field the JSON publishes as
8787
| kind | the clause |
8888
|:--|:--|
8989
| `symbol` | `a field of interface MetaOverlayCacheKey` · `a method of class RestServer` · `a top-level function` |
90-
| `route` | `a path literal in RestServer` · `bridged from symbol enforceEnvironmentOwnership — its registrar handler names it` |
90+
| `route` | `a path literal in RestServer` · `bridged from symbol enforceEnvironmentOwnership — its route-source handler names it` |
9191
| `sdk` | `the route ledger binds it to GET /api/v1/ui/view/:object/:type` |
9292
| `literal` | `a string literal in cacheKeyOf` |
9393
| `command` | `read off packages/cli/src/commands/environments/bind.ts` |
@@ -181,7 +181,7 @@ whole anchor set, in every kind, after it is derived.)
181181
The first build of this derivation was, on some PRs, *noisier* than the proxy it replaced
182182
(134 rows where the old tool gave 26). Two guards fixed that, and both run **before** the
183183
route bridge — a name left in the set does not merely add a noisy row, it mints noisy route
184-
and SDK anchors from every registrar handler that mentions it:
184+
and SDK anchors from every route-source handler that mentions it:
185185

186186
1. **Shape** — an anchor must be code-shaped (camelCase / PascalCase / snake_case /
187187
dotted). `label`, `object`, `start`, `locale` and `sections` all arrived as real
@@ -200,7 +200,7 @@ entirely — a data table is consulted by handlers, it is not their implementati
200200

201201
### The `sdk` bridge reaches part of its own population, and says which part (#9572)
202202

203-
The `sdk` hop needs a registrar `path:` tail to select a route-ledger row. Measured on
203+
The `sdk` hop needs a route source's `path:` tail to select a route-ledger row. Measured on
204204
`9ff11921a`: **45 of the 221 client-bound ledger rows are reachable, 176 are not.** An
205205
unreachable row is not "unlisted this time" — no symbol change bridges to it, ever.
206206

@@ -210,15 +210,15 @@ drift — each move was measured row by row:
210210

211211
| `--bridge-coverage` | `a6eca9223` | `8f10a79f7a` | why |
212212
| --- | ---: | ---: | --- |
213-
| registrar files | 12 | 12 | two ADR-0049 ledger entries were admitted in between and are excluded again here; they produced **0 tails and 0 reachable rows**, so they never moved the figures below |
213+
| route sources | 12 | 12 | two ADR-0049 ledger entries were admitted in between and are excluded again here; they produced **0 tails and 0 reachable rows**, so they never moved the figures below |
214214
| route tails | 43 | 44 | `rest-server.ts` unrolled `for (const publishedPath of […])` into a literal `path:` — a variable path yields no tail, a literal one does |
215215
| client-bound rows | 222 | 219 | three `:type/:section/:name` rows deleted from `rest-route-ledger.ts`, all three already unreachable |
216216
| **reachable** | **45** | **47** | the one new tail `/:type/:name/published` selects `meta.getPublished` on the rest ledger *and* on the runtime ledger |
217217

218218
⭐ 45 → 47 is the bridge reaching **more** of its population, not losing track of it, so
219219
the figure stands at 47. ⛔ Do not "restore" 45: the only recognizer spelling that
220220
reproduces it drops ten of the fourteen matched files, including a tail-producing
221-
registrar — the control appears to recover exactly when the recognizer stops working.
221+
call site — the control appears to recover exactly when the recognizer stops working.
222222

223223
That number now travels with the answer. `bridgeCoverage` is emitted on every run whose
224224
change carried a bridgeable symbol (`{ measured: false, reason }` when it did not — never a
@@ -246,34 +246,96 @@ among them), so the silence is not an empty region.
246246

247247
`56 of 56` and `46 of 87` used to print in the same words, and they are not the same
248248
finding. Every unreachable row is now attributed against a **ceiling** — every `path:` any
249-
`packages/**` file declares, with `REGISTRAR_FILE_RE` ignored entirely, built by
250-
`maximalTailsFrom` from the same `parseRegistrarSource` over the same walk. Measured on
249+
`packages/**` file declares, with `CALL_SITE_FILE_RE` ignored entirely, built by
250+
`maximalTailsFrom` from the same `parseRouteSource` over the same walk. Measured on
251251
`589758d22`, the 177 unreachable rows partition as:
252252

253253
| cause | rows | what it means |
254254
| --- | --- | --- |
255255
| `discovery-gap` | 14 | an in-repo file declares this exact path; the filename convention did not scan that file. The JSON **names the witness**. |
256-
| `no-in-repo-registrar` | 56 | on a ledger where **not one** row is declared in-repo — declared upstream and catch-all-mounted. No discovery change reaches it. |
257-
| `undecided` | 107 | no in-repo declaration for the row, on a ledger that *has* in-repo registrars. Absence and an unreadable spelling are not distinguishable here, so neither is claimed. |
256+
| `no-in-repo-declaration` | 56 | on a ledger where **not one** row is declared in-repo — declared upstream and catch-all-mounted. No discovery change reaches it. |
257+
| `undecided` | 107 | no in-repo declaration for the row, on a ledger that *has* in-repo route sources. Absence and an unreadable spelling are not distinguishable here, so neither is claimed. |
258258

259-
Exactly **one** of the seven ledgers is `no-in-repo-registrar` today: `auth-route-ledger.ts`,
259+
Exactly **one** of the seven ledgers is `no-in-repo-declaration` today: `auth-route-ledger.ts`,
260260
whose own header has said so since #3656 — better-auth declares those routes inside
261261
`node_modules` and the plugin mounts them with a single ``rawApp.all(`${basePath}/*`)``,
262262
which `routeTailOf` cannot and should not turn into a tail. That is why widening
263-
`REGISTRAR_FILE_RE` to admit `auth-plugin.ts` was measured to move `registrar files
263+
`CALL_SITE_FILE_RE` to admit `auth-plugin.ts` was measured to move `route sources
264264
scanned` 12 → 13 and **nothing else**.
265265

266-
**This changes no discovery and moves no reach.** `REGISTRAR_FILE_RE` is byte-identical,
267-
the bridge still rides on `registrarByTail` alone, and `reachable` is 45 before and after —
266+
**This changes no discovery and moves no reach.** `CALL_SITE_FILE_RE` is byte-identical,
267+
the bridge still rides on `routeSourceByTail` alone, and `reachable` is 45 before and after —
268268
pinned in `--self-test`. The ceiling only explains the number; it never participates in it,
269269
and because it is a superset by construction a ceiling that misses a *reachable* row is a
270270
`brokenScan` verdict rather than a quieter result.
271271

272272
The classification is **derived, never listed**. Control on `589758d22`: adding one
273273
in-repo file that declares one auth route — under a filename the convention does not match
274-
— moves the auth ledger out of `no-in-repo-registrar` on its own (structural 56 → 0,
274+
— moves the auth ledger out of `no-in-repo-declaration` on its own (structural 56 → 0,
275275
`reachable` still 45), and removing it restores 56.
276276

277+
### A route SOURCE is two kinds, and one of them is admitted by evidence (#11857)
278+
279+
Maintainer ruling A, 2026-09-04 decision batch #31. Until this card the recognizer's name,
280+
docblock and `--self-test` all meant *the file that registers the route* — while the
281+
measured widening that motivated the card admits five `packages/spec` **Zod contract
282+
declarations**, which register nothing. The ruling made the **rename** a condition of
283+
admitting them: nothing here is called a "registrar" any more, because the word would
284+
otherwise denote two constructs.
285+
286+
A **route source** is a file whose source declares a route. Two kinds:
287+
288+
| kind | admitted by | today |
289+
| --- | --- | ---: |
290+
| registration **call site** | the `CALL_SITE_FILE_RE` filename convention (unchanged) | 12 |
291+
| spec contract **declaration** | **evidence**: a non-test `packages/**/*.ts` whose masked source declares a route beside the HTTP method it answers | 5 |
292+
293+
Why a declaration counts: the drift check exists to say *"the contract changed, re-verify
294+
the manual"*, and a Zod API declaration in `packages/spec` **is** the contract. No widening
295+
of the filename convention would ever reach it — `storage.zod.ts` is not going to be
296+
renamed `storage-routes.ts`.
297+
298+
**The guard is the HTTP method, not a file list.** A route declaration names the verb it
299+
serves; a data payload carrying a `path:` key does not. Measured on `460134af8`: of the 61
300+
literal `path:` sites in `packages/spec/src/conversions/registry.ts` — whose
301+
`/api/v1/health` is a connector-action **input** inside an automation fixture — **zero**
302+
carry an HTTP method, while the five contract declarations carry one at 61 of their 62
303+
sites. (The 62nd has its `method:` five *lines* but zero *properties* up, behind a JSDoc
304+
that `maskComments` blanks; the lookaround skips blank lines for exactly that reason.) The
305+
separation is total at every lookaround from 1 to 6, so the constant is a margin, not a
306+
threshold.
307+
308+
**The fixture and the benchmark are not re-excluded here.** The card also named
309+
`test/fixtures/*.ts` and `*.bench.ts`; `isTestFile` grew those arms in #12965, so the walk
310+
never offers them to either kind. `--self-test` **pins** that over the two real repo paths
311+
rather than restating the exclusion — the day either arm is loosened, they come back as
312+
contract declarations and the self-test says so.
313+
314+
Measured on `460134af8`, before → after:
315+
316+
| `--bridge-coverage` | before | after |
317+
| --- | ---: | ---: |
318+
| route sources scanned | 12 | **17** (12 call sites + 5 contract declarations) |
319+
| route tails produced | 44 | **78** |
320+
| client-bound rows reachable | 47 | **61** |
321+
| `discovery-gap` rows | 14 | **0** |
322+
| `storage` ledger unreachable | 7 of 7 | **0 of 7** |
323+
| `i18n` ledger unreachable | 3 of 3 | **1 of 3** |
324+
| `rest` ledger unreachable | 42 of 84 | **40 of 84** |
325+
| `runtime` ledger unreachable | 64 of 69 | **61 of 69** |
326+
| `plugin-auth` unreachable | 56 of 56 | 56 of 56 (unchanged — declared upstream, see above) |
327+
328+
No reach regression is possible **by construction**: `selectsFrom` is a `some()` over the
329+
tail list, so added tails can only add selections. `discovery-gap` reaching **0** is the
330+
strong reading — every row the ceiling said a widening *could* reach is now actually
331+
reached, and what remains unreachable is unreachable for a reason no discovery change
332+
touches.
333+
334+
⚠️ **The published `45 → 59` figure from the card is not what landed.** The card measured
335+
on `a6eca9223`; both endpoints have since moved with the tree (see the #9572 table above),
336+
and the guard admits five files rather than the card's eight. The figure this PR
337+
establishes is **47 → 61 on `460134af8`**, re-measured rather than copied.
338+
277339
### A PARTIAL ledger read is a verdict too (#9896)
278340

279341
The row recognizer reads **single-quoted** values only, and the `rowsParsed === 0` guard
@@ -429,7 +491,7 @@ How often it renders, re-derived over the 40 first-parent commits ending at `e43
429491
is a rare notice rather than a per-PR banner, which is what keeps it readable.
430492

431493
**Cost** (the card's open question): the anchor derivation reads the same 178-page corpus
432-
the old one did, plus the 18 route-registrar/ledger sources (~875 KB) and one `git show`
494+
the old one did, plus the 18 route-source/ledger files (~875 KB) and one `git show`
433495
per changed file per side. Measured end-to-end on the ten PRs above, `node affected-docs.mjs`
434496
went from 85-195 ms to 114-582 ms. The heaviest case is the widest diff; every case stays
435497
well under a second, against a job that already spends seconds checking out the repo and

0 commit comments

Comments
 (0)