77// "ratchets completeness over a CURATED table of HTTP/transport entry points"
88// and that "a new ungated route there is UNCLASSIFIED ... and breaks CI". That
99// promise is true only for the entry points a probe can actually mint a key
10- // for. This module measures, for EVERY one of the 11 files the `PROBES` table
10+ // for. This module measures, for EVERY one of the 13 files the `PROBES` table
1111// names, how far that reach extends — and records the result so it cannot rot.
1212//
13+ // ⭐ Since 2026-08-31 two of those files are the ROUTE LEDGERS, and they are
14+ // the reason the matrix header now claims family/domain completeness rather
15+ // than route completeness. Their rows read `blindSpot: 0` for a reason no
16+ // other row here has — every ledger row is reachable by a mintable key — and
17+ // that must NOT be read as "the route blind spot is closed". It is not:
18+ // `BLIND_SPOT_TOTAL_STATIC` / `_RUNTIME` below are UNCHANGED at 75 / 80.
19+ //
1320// ⛔ WHAT IS **NOT** CLAIMED HERE. Nothing in this file asserts that any route
1421// is unguarded, unauthenticated or exploitable. Route-level enforcement is
1522// mostly in-handler (`enforceAuth` / `shouldDenyAnonymous` / per-object
8188// protocol). The static reading is the one pinned because it is the one this
8289// package can re-derive without depending on `@objectstack/rest`.
8390//
84- // ── THE POPULATION SOURCE: measured, and still an OPEN CONTRACT DECISION ──
91+ // ── THE POPULATION SOURCE: measured, then DECIDED (2026-08-31) ────────── ──
8592//
8693// The obvious repair is to source this ratchet's route population from the
8794// route ledgers instead of from a regex table, and that was measured before
145152// from the one this matrix header states, and adopting it is a contract
146153// decision rather than a repair.
147154//
148- // ⇒ ⛔ NOT DECIDED HERE, and deliberately not worked around: widening the
149- // regex is the rot this instrument already has, and inventing a syntactic
150- // "gated" reading would convert a visible gap into a written-down false
151- // assurance — strictly worse than an honest UNCLASSIFIED. What this file does
152- // instead is close the mechanism that was SILENT (the dead probe), leave the
153- // population untouched, and hand the decision on with the reading attached.
155+ // ⇒ ⭐ DECIDED 2026-08-31, and the three blockers above are why the answer is
156+ // what it is rather than the obvious one. The ledgers now supply the
157+ // POPULATION at FAMILY / DOMAIN granularity — 19 REST families + 21 dispatcher
158+ // domains = 40 keys — and nothing else. They do NOT supply the
159+ // CLASSIFICATION: blocker 1 stands, so "is it gated" remains a reviewed matrix
160+ // row, never a ledger disposition. Blocker 2 stands as the reason no syntactic
161+ // reading was attempted anywhere. Blocker 3 is not fixed and is not hidden: a
162+ // route added inside an existing family mints nothing and this gate stays
163+ // green, which is stated at that exact granularity in the matrix header with
164+ // no caveat attached to a wider claim.
165+ //
166+ // What the change buys is the FILE-SELECTION layer rather than the route
167+ // layer: a family or domain can no longer be silently absent, because both
168+ // ledgers are enumerated from a running server and guarded in both directions.
169+ // 6 of the 40 keys are classified by rows that already pinned the same
170+ // surface; the other 34 are enumerated, dated and pinned shrink-only in
171+ // `authz-ledger-population.baseline.ts`. Before that date those 34 surfaces
172+ // minted no key at all — no UNCLASSIFIED, no STALE, nothing.
173+ //
174+ // The DIRECTION half went to the producer: a ledger row may declare the authz
175+ // posture it has been reviewed to have (`authz:`, phased exactly like
176+ // `responseSchema` — optional, no coverage no fill, never mass-produced), and
177+ // the companion test resolves every declaration against an `enforced` matrix
178+ // row. That is what eventually makes blocker 3 answerable at the ledger review
179+ // point, where a new route is already being read.
180+ //
181+ // ⛔ Two readings stay REJECTED and are recorded here so they are not
182+ // re-proposed: deriving "gated" from source syntax (73% false-ungated), and
183+ // taking a ledger disposition as an authorization fact (blocker 1).
154184
155185import { readFileSync } from 'node:fs' ;
156186import { fileURLToPath } from 'node:url' ;
@@ -200,7 +230,7 @@ export interface ProbeTableReading {
200230 keys : number ;
201231}
202232
203- export const PROBE_TABLE : ProbeTableReading = { entries : 16 , files : 11 , keys : 9 } ;
233+ export const PROBE_TABLE : ProbeTableReading = { entries : 18 , files : 13 , keys : 15 } ;
204234
205235/**
206236 * The probe count `authz-conformance.matrix.ts`'s header states.
@@ -219,9 +249,61 @@ export const PROBE_TABLE: ProbeTableReading = { entries: 16, files: 11, keys: 9
219249 * in a third file. ⛔ Do not re-point this at a hand-written number — it is
220250 * read out of the matrix header's own text.
221251 */
222- export const MATRIX_HEADER_PROBE_CLAIM = 16 ;
252+ export const MATRIX_HEADER_PROBE_CLAIM = 18 ;
223253
224254export const PROBE_FILE_CENSUS : readonly ProbeFileReading [ ] = [
255+ // ── the two LEDGER files: the population source since 2026-08-31 ───────
256+ //
257+ // ⭐ These two rows read differently from every other row here, and the
258+ // difference is the point of the change that added them: `blindSpot` is 0
259+ // NOT because the file mounts nothing (the reading four rows below give) but
260+ // because EVERY row in the table is reachable by a mintable key. Each ledger
261+ // row carries a `family` / `domain`, and each distinct value mints a key, so
262+ // there is no row the ratchet cannot name.
263+ //
264+ // ⚠️ `population` here counts LEDGER ROWS, not registrar call sites, and the
265+ // two are not interchangeable. A `blindSpot` of 0 says every ledger row is
266+ // covered by some key at FAMILY / DOMAIN granularity; it does NOT say every
267+ // route mints its own key, and it does not touch the 75/80 route-level blind
268+ // spot recorded below — that number is unchanged by this change and Leg A of
269+ // the reverse verification is what proves it unchanged.
270+ {
271+ file : 'packages/rest/src/rest-route-ledger.ts' ,
272+ kinds : [ 'ROUTE_ENUMERATION' ] ,
273+ probes : 1 ,
274+ keys : 19 ,
275+ population : 94 ,
276+ reachable : 94 ,
277+ blindSpot : 0 ,
278+ populationRule : 'ledger rows inside REST_ROUTE_LEDGER; reachable = rows carrying a `family` (each distinct value mints a key)' ,
279+ controls : { "route: '" : 94 , "family: '" : 94 , RestRouteLedgerEntry : 2 } ,
280+ note :
281+ 'The audited disposition of every route @objectstack/rest mounts, enumerated through ' +
282+ 'RestServer.getRoutes() on a booted server and guarded per route by rest-route-ledger.conformance.test.ts. ' +
283+ 'That guard is why this file can be a population source and a regex table cannot: a mounted route with no ' +
284+ 'row here is already RED in another package, so a new family cannot be silently absent from this file, ' +
285+ 'and therefore cannot be silently absent from the authz ratchet either. 19 families; 1 classified by a ' +
286+ 'matrix row (metadata), 18 enumerated in the shrink-only baseline.' ,
287+ } ,
288+ {
289+ file : 'packages/runtime/src/route-ledger.ts' ,
290+ kinds : [ 'ROUTE_ENUMERATION' ] ,
291+ probes : 1 ,
292+ keys : 21 ,
293+ population : 80 ,
294+ reachable : 80 ,
295+ blindSpot : 0 ,
296+ populationRule : 'ledger rows inside ROUTE_LEDGER; reachable = rows carrying a `domain` (each distinct value mints a key)' ,
297+ controls : { "route: '" : 80 , "domain: '" : 80 , RouteLedgerEntry : 2 } ,
298+ note :
299+ 'The dispatcher half. Its machine contract is DOMAIN-level by live registry introspection ' +
300+ '(domainRegistry.list()), guarded in BOTH directions by route-ledger.conformance.test.ts: every ' +
301+ 'registered domain needs a row, and every ledger domain must be a live prefix or a pinned legacy / ' +
302+ 'non-dispatch branch. That two-way guard is what settles the FILE-SELECTION layer by ' +
303+ 'construction — all 16 DomainRoute prefixes declared across the 15 domain files that declare one are ' +
304+ 'ledger domains today, including the 11 files no probe has ever named. 21 domains; 5 classified ' +
305+ '(/meta, /actions, /automation, /packages, /mcp), 16 in the shrink-only baseline.' ,
306+ } ,
225307 {
226308 file : 'packages/rest/src/rest-server.ts' ,
227309 kinds : [ 'ROUTE_ENUMERATION' , 'TRIPWIRE' ] ,
@@ -432,6 +514,40 @@ export function deriveProbeFileCensus(): {
432514} {
433515 const files = new Map < string , { population : number ; reachable : number ; controls : Record < string , number > } > ( ) ;
434516
517+ // ── the two ledger files (the population source) ────────────────────────
518+ //
519+ // Scoped to the exported array literal, exactly as the probes are: the
520+ // patterns are the ledger's own row vocabulary, so a doc-comment or a type
521+ // declaration spelling the same tokens outside the table would inflate the
522+ // reading. `controls` stay WHOLE-FILE counts, like every other row here —
523+ // they answer "is this still the file I think it is", which is a question
524+ // about the file and not about the table.
525+ for ( const [ rel , marker , keyField ] of [
526+ [ 'packages/rest/src/rest-route-ledger.ts' , 'REST_ROUTE_LEDGER' , 'family' ] ,
527+ [ 'packages/runtime/src/route-ledger.ts' , 'ROUTE_LEDGER' , 'domain' ] ,
528+ ] as ReadonlyArray < readonly [ string , string , string ] > ) {
529+ const src = read ( rel ) ;
530+ const from = src . indexOf ( `export const ${ marker } ` ) ;
531+ const to = from < 0 ? - 1 : src . indexOf ( '\n];' , from ) ;
532+ // A marker that has moved reads as an EMPTY table, never as the whole file:
533+ // a silently wider scope would still produce plausible numbers.
534+ const table = from < 0 || to < 0 ? '' : src . slice ( from , to ) ;
535+ const rowRe = / r o u t e : ' / g;
536+ const keyRe = new RegExp ( `${ keyField } : '` , 'g' ) ;
537+ const entryName = keyField === 'family' ? 'RestRouteLedgerEntry' : 'RouteLedgerEntry' ;
538+ files . set ( rel , {
539+ population : occurrences ( table , rowRe ) ,
540+ // Every row carrying the key field is reachable: each distinct value
541+ // mints a key. A row that ever loses it shows up as a blind spot here.
542+ reachable : occurrences ( table , keyRe ) ,
543+ controls : {
544+ "route: '" : occurrences ( src , / r o u t e : ' / g) ,
545+ [ `${ keyField } : '` ] : occurrences ( src , new RegExp ( `${ keyField } : '` , 'g' ) ) ,
546+ [ entryName ] : occurrences ( src , new RegExp ( entryName , 'g' ) ) ,
547+ } ,
548+ } ) ;
549+ }
550+
435551 // ── rest-server.ts ──────────────────────────────────────────────────────
436552 {
437553 const src = read ( 'packages/rest/src/rest-server.ts' ) ;
0 commit comments