test(qa): source the authz ratchet population from the two route ledgers (family/domain), and declare authz posture at the producer - #13773
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 30ef9382ff22885ec205141aa04cc9c706af18ee && git checkout 30ef9382ff22885ec205141aa04cc9c706af18ee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8ab4ace4dd9b17b6e75b90b1cc77193685a748d1 1c24f47fdb04e57c2e61e5bf3c91109e729572e5 && git checkout -B drift-repro 8ab4ace4dd9b17b6e75b90b1cc77193685a748d1 && git merge --no-ff 1c24f47fdb04e57c2e61e5bf3c91109e729572e5
node scripts/docs-audit/affected-docs.mjs --json 8ab4ace4dd9b17b6e75b90b1cc77193685a748d1
|
Fixes #13411
Implements the maintainer's ruling of 2026-08-30 (批 #7): option F direction + B
executed now, with the matrix header sentence narrowed in the same change.
⛔ Nothing here asserts that any route is unguarded, unauthenticated or
exploitable. Route-level enforcement is mostly in-handler and is not measured
anywhere in this change. What is repaired is a defect in a guarantee — what the
instrument can see — and the change makes the gate see strictly more.
B — the population now comes from the two route ledgers, at family/domain granularity
authz-conformance.test.tsgains twoROUTE_ENUMERATIONprobes, one per ledger,each scoped (
within:) to that ledger's exported array so the patterns read thetable rather than the file that carries it.
Re-derived, not carried over. The ruling's estimate was ~40 keys / ~31 new
UNCLASSIFIED. Keys: 40, exactly. UNCLASSIFIED: 34, not 31 — and the
difference is worth stating. 31 was
40 − 9, subtracting today's nine minted keys.Three of those nine are not HTTP route families at all: the realtime
publishfan-out pin and the MCP stdio principal pin mount no HTTP route, and the second
MCP key is a co-key on the same
/mcpdomain as the first. The nine keys map ontosix distinct family/domain surfaces, so
40 − 6 = 34.Why the ledgers and not a wider regex: both are enumerated from a running server
and guarded in both directions by their own conformance tests (
RestServer.getRoutes();domainRegistry.list()). A new family or domain cannot be silently absent from them,so it cannot be silently absent from the ratchet. Widening the regex rots on the next
added file — that is the mechanism being replaced, not repeated.
The 34, and why this baseline shape is not a weakening
packages/qa/dogfood/test/authz-ledger-population.baseline.ts— enumerated key bykey, dated, shrink-only, with the reason in the file. Four rules, all mechanical:
LEDGER_POPULATION_BASELINE_MAX = 34);be deleted in the same change (this is the burn-down that makes it shrink);
⭐ The load-bearing point: before this change those 34 surfaces were not merely
unclassified, they were unmintable — no key, no UNCLASSIFIED, no STALE, nothing to
be silent about. Enumerating them turns an invisible absence into a dated list that can
only get shorter, and a 41st family or domain is red on the day it lands. Bulk
classification was refused (34 unreviewed
coversclaims is the "declared butunverified" surface these ratchets exist to remove); silencing by count, pattern or
prefix was refused (none of those can tell a new gap from an old one).
6 keys are classified, all on rows that already pinned the same surface through a
probe, each with its own dated justification comment:
rest-family:...:metadataanddispatcher-domain:...:/meta(anonymous-deny-meta),/actions,/automation,/packages(the domain-wide gates),/mcp(mcp-http-identity). ⛔ Deliberately notclassified: the REST
packagesfamily (a different registrar on a different server)and
/mcp/skill(a second handler body the classifying row does not name).F — the producer-side
authzdeclaration, phasedRestRouteLedgerEntryandRouteLedgerEntrygain an optionalauthznaming thematrix row that classifies the route — phased exactly like
responseSchemain thosesame two files (optional, "no coverage, no fill", never mass-produced), with the
resolver in the guard that can import the vocabulary. It refuses a name that is not a
row id, and refuses a row that is not
enforced.5 rows of 174 filled. The rule used to pick them, stated in the field's own doc
comment: the named matrix row must be
enforced, its cited dogfood proof must drivethat route at a literal wire path (nothing inferred from a sibling), and its
enforcement text must name the site that serves it.
GET /api/v1/metaandPOST /api/v1/meta/_migrate-stored(REST) and theirdispatcher twins —
showcase-anonymous-deny-surfaces.dogfood.test.tsdrives thoseexact paths on a booted showcase, anonymous 401 with an authenticated not-401 as the
positive control.
* /mcp/**— the cleanest case in either ledger: a one-row domain, so thefamily-granular classification is route-granular there with nothing inferred.
⛔ The 19 sibling metadata rows are left blank on purpose. The family-wide gate is a
real property, but writing it onto 19 rows in one change is the mass production the
ruling forbids.
The matrix header sentence, narrowed — and no caveat
It now claims completeness at route-family and dispatcher-domain granularity, and
says the bound out loud in the same breath:
That is a known, accepted bound stated as the claim itself, not a caveat hung off a
wider one. ⛔ This change does not close the route-level blind spot and does not claim
to:
BLIND_SPOT_TOTAL_STATIC/_RUNTIMEare unchanged at 75 / 80, and Leg A belowis the proof they are unchanged.
The file-selection layer, verified by measurement rather than assumed
15 of the 17
runtime/src/domains/files declare a route prefix. The probe table names4. All 15 now mint a ledger key ⇒ 11 files no probe has ever named now mint
one. Controls both ways: a prefix that does not exist is correctly absent; the domain
key set is non-empty at 21. So the file-selection layer is settled by construction, at
the domain layer — never the route layer.
Verification
All readings on final head
1c24f47f, after mergingorigin/mainand re-deriving thegate union with
--repo objectstack-ai/objectstack(stderr banner confirms commit andcheckout; the assertion holds).
GREEN. dogfood
authz-conformance.test.ts+authz-probe-blind-spot.test.ts—Test Files 2 passed (2) / Tests 80 passed (80)(62 before this change).rest-route-ledger.conformance.test.ts7/7 ·route-ledger.conformance.test.ts6/6 ·client
rest-route-ledger-coverage+route-ledger-coverage+route-ledger-response-schema+client-url-conformance9/9.typecheckexit 0 fordogfood, rest, runtime, client.
Not vacuous:
tsc --noEmit --listFilesin dogfood lists 1282 files and contains allfive edited/added files at 1 hit each, with a negative control at 0. The rest/runtime
typecheck readings carry to the final head by proof, not assumption:
git diff f3158899f0 1c24f47fdb -- packages/rest packages/runtimeis 0 lines, andboth ledger conformance suites were re-run at the final head regardless.
pnpm lintrepo-wide (eslint . --no-inline-config) exit 0 — 5567 files, 0errors, 0 warnings. No narrowing was used and none needs declaring.
Ablations — direction predicted in writing before each run
Every leg ran under
trap ... EXIT INT TERMwith absolute paths, restored viagit checkout HEAD -- ...(never bare), and proved mutation and restore on disk byblob hash plus occurrence counts in both directions. No rebuild leg applies: both
instruments are pure source-text scanners (
readFileSyncover the files under test,no import of the packages under test), so nothing resolves through
dist/and a stalebuild cannot produce a false green.
Leg A — predicted GREEN, and IS GREEN. One ungated route injected into
registerUiEndpoints. Mutation proven both ways:this.routeManager.register(80 to81 and the marker 0 to 1, blob
bc216e78to239e80a8. Result:authz-conformance.test.ts47 passed, 0 failed — green with an ungated route ondisk, reproducing round 1's reading unchanged;
authz-probe-blind-spot.test.tsREDwith
expected 81 to be 80. ⭐ This negative result is reported as the accepted bound,not smoothed over.
Leg B — predicted RED, IS RED. A row carrying a brand-new
family: 'webhooks'added to
REST_ROUTE_LEDGER(marker 0 to 1,family: '94 to 95). Result:UNCLASSIFIED surface — add a ledger row (ADR-0060): rest-family:rest-route-ledger.ts:webhooks.The rot the probe table had is closed: nobody has to remember to add a probe.
Leg C — predicted RED, IS RED. One line deleted from the baseline (entries 34 to
33). Result:
UNCLASSIFIED surface ...:search, plusexpected 33 to be 34andexpected 39 to be 40. ⭐ This is the proof that the 34 keys really are in thepopulation and the baseline holds them rather than hiding them.
Restore proven on all three legs: restored blob identical to the non-empty HEAD blob,
git diff HEAD0 lines,git status --porcelainempty, marker count back to 0.Gate union
NAMED reconciled against RAN, mechanically — union named 39, ran 39, unreconciled 0.
A family list is not a green, so the harvested list was used as a checklist rather than
only as a diff between derivations:
Two things that reconciliation caught, and neither would have shown up in a prose claim:
check:type-check-debtwas genuinely unrun (I had cap-killed it and written it off),and
check:slot-lookup's first log was a partial from the batch the foregroundceiling killed mid-command — its completed rerun is the one matched, and the partial is
excluded from
ran.listby name. Both derivations were harvested (31 families before thechangeset landed, 39 after) and the reconciliation is against the superset.
Each gate read from its own printed verdict line, exit code captured before any pipe.
38 green, 1 NOT MEASURED, 0 unrun. Green includes
check:doc-authoring,check:type-check-debt,check:objectql-double-limit,check:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:type-check-coverage,check:query-options-erasure,check:test-source-alias,check:type-source-resolution,check:slot-lookup,check:dual-build-cjs-loads,check:empty-changeset,check:changeset-gate-self-tests,check:objectui-changeset,check:adr-0087-registrationand the four spec families.check:type-check-debtis GREEN and it was the one worth chasing.packages/runtimecarries a TEST_DEBT entry (recorded 217) and its
tsconfig.jsonexcludes**/*.test.ts,so its
typecheckexit 0 said nothing about the six hidden test files that importroute-ledger— exactly the shape that reddens a shrink-only ratchet after a green localrun. Re-measured under the verify lock: "29 ledger entr(ies) re-measured in 258.1s, 1542
raw tsc error(s) total, none above its recorded number", with "surplus: none — every
entry sits exactly at its measurement, so any new error is red." The earlier
exit 143was wall-clock only; serialised under the lock it finishes in 259s. ⛔ No TEST_DEBT entry
was raised.
@objectstack/restand@objectstack/dogfoodcarry no DEBT/TEST_DEBT entryat all (checked against the ledgers in the gate script), and rest's own
check:test-typecheckratchet is green at 1 file / 2 errors / 1 pinned signature.check:objectql-double-limitis green and non-vacuously so: this diff introduces noengine double — every
.find(it adds isArray.prototype.findover a plain array —and the gate's own line confirms "baseline key set verified against adf4bf4: no files
added."
check:doc-authoringinitially went RED on a tracker id inside a runtime string inthe census note. Repaired by stripping the id (⛔ not by a baseline entry, which that
gate marks maintainer-only); it is now green and demonstrably non-vacuous — its
sibling-package leg reads 80806 strings across 1096 parsed sources over 831 pinned
sites and reports no growth, with no baseline entry added.
NOT MEASURED — reported separately and ⛔ not folded into the pass list:
check:test-completeness— exit 3, its own words: "Nothing was measured: this gateexited before parsing a single summary line ... ⛔ It is NOT a finding."
That is the only one.
check:type-check-debtwas on this list in an earlier draftand is not any more — it was run to completion and is green (above).
Clause ② — NO, with the measurement
Neither ledger type reaches a published surface.
RestRouteLedgerEntryandRouteLedgerEntryappear 0 times in all four built type entry points(
packages/rest/dist/index.d.ts/.d.cts,packages/runtime/dist/index.d.ts/.d.cts); positive control on the same greps:RestServer18,HttpDispatcher32.Both modules are package-internal by their own headers and are not re-exported from
either index. No published surface is widened and no accept/reject behaviour changes.
Changeset
patchfor@objectstack/restand@objectstack/runtime. The alternative was theskip-changesetlabel — this PR releases nothing observable — and it was not takenbecause the diff reaches publishable
packages/*/src/, which sits outside the closedlist that route is defined over. A wrong changeset is release noise; a wrong
skip-changesetlabel is a gate bypass, so the conservative route was taken. Happy toswap it for the label if the seat prefers.
Fences honoured
⛔ No landed edit in
packages/rest/src/rest-server.ts(held by #13406) orpackages/runtime/src/http-dispatcher.ts(held by #13408).rest-server.tswasmutated temporarily for Leg A and restored with the proofs above; nothing was
committed. Both were read. Nothing lands in
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.mdorcontent/docs/releases/**. All 23 open PRs were checkedagainst the merge base for the files touched here: no collision.
⛔ Not re-litigated: option E (73% false-ungated) and "just use today's ledger as the
population" stay rejected and are recorded in the census so they are not re-proposed.
PR #13436's landed half is untouched. #13214 is not reopened, re-graded or re-judged.
#13376 keeps its own card state — the reading above is a measurement, not a state change.
Generated by Claude Code
Generated by Claude Code