Skip to content

feat(gate): fail a rawApp auth mount that carries no ledger row (#10534 follow-up 4) - #11149

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10534-mount-ledger-gate
Aug 22, 2026
Merged

feat(gate): fail a rawApp auth mount that carries no ledger row (#10534 follow-up 4)#11149
os-warren merged 2 commits into
mainfrom
claude/issue-10534-mount-ledger-gate

Conversation

@os-warren

@os-warrenos-warren commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Part of #10534 — follow-up 4 only. This card keeps remaining scope after this PR (follow-up 3, the REGISTRAR_FILE_RE bridge; and the ninth ledger row, #10975), so this PR must not carry a closing keyword. It does not.

The hole

auth-plugin.ts mounts routes directly on the raw Hono app, ahead of the better-auth catch-all. The catch-all never sees them, so the vendor's own route table cannot account for them, and auth.api's enumeration — which is what auth-route-ledger.conformance.test.ts drives — cannot see them either. A mount with no row in either half of the ledger was therefore invisible to every check in this tree. That state produced #9941 and #10050, and #10534's census found it was not a one-off: nine of the seventeen mounts were in neither half.

The pin the conformance suite already carries asserts the source: 'objectstack' set exactly, so it fails when a row disappears. What it structurally cannot do is fail when a mount appears with no row, because both of its sides are hand-written: the mount list in that assertion is a copy of the truth, not a reading of it, and nobody who adds route 18 has to touch it.

pnpm check:auth-mount-ledger supplies the missing half: it enumerates the mounts from auth-plugin.tssource and diffs them against both halves of the ledger.

Clause-② — declared no, and it holds

A gate adds no runtime surface and changes no route's mounting, behaviour, or accept/reject set. The only packages/** edit is a comment-only change to the conformance pin, whose text still told readers this gate did not exist. Zero packages/spec, zero content/docs/releases/**.

Nothing in the gate's failure text or its requirements can force a runtime change to make main green — the ninth mount is carried by a declared, issue-bearing PENDING_DISPOSITION entry, not by demanding a row that does not yet exist.

Population re-measured on current main

bbe643c08, --report: still 17 — 12 accounted for by a reviewed ledger row, 4 shadowing a vendor-declared path (BETTER_AUTH_MOUNTED_SURFACE), and 1 pending. That reproduces #10534's census a third time, across a third commit.

The four measured constraints

1. The match carries a right boundary. Accounting is exact string equality on METHOD /full/wire/path. #10534's own leg 1 read "5 undocumented" when the truth was 6, because it matched by substring and /admin/sso/register is a strict prefix of /admin/sso/register-saml — the shorter route was scored accounted-for on the strength of its longer sibling's URL. When an unaccounted mount does stand in a prefix relation to a ledgered route, the failure text says so, so the property is observable in the output rather than implicit in a comparison operator.

2. rawApp.all and rawApp.use are excluded — the catch-all and the IP gate are lanes, not routes. Counted and reported, so the exclusion is visible rather than silent.

3. A row alone does not satisfy it. A disposition cannot be inferred mechanically, and a gate that accepts a pasted row teaches the next author to paste a row. An source: 'objectstack' row must carry the evidence its disposition claims — client: for sdk, a substantive note: for server-only/disabled/public. The failure text names the peer-group discriminator and the set-initial-password precedent. The note floor is a floor, not a proof, and the script says so; what it buys mechanically is that adding three words is no longer the cheapest path.

4. A partial read is never reported as a complete one. Any rawApp mounting form the census cannot read per-route — an unrecognised verb (rawApp.on(...)), a non-literal path, an unresolved interpolation — is a finding. A missing file, a moved anchor, an underivable basePath, or a parse that yields zero of anything exits 2 and prints NOT MEASURED. Exit 2 is a refusal, not a pass.

That refusal earned its keep during development: the first spelling of the array parser looked for ]; at column 0, and both arrays in auth-route-ledger.ts close as ,]; on the last row's own line. The gate refused (exit 2) rather than reporting an empty population as a clean tree.

Remedy authority (#8435) — and why marked, not refused

CI at b86bd9e76 was red on check:ratchet-remedy-authority, correctly. Reproducing it locally showed two findings, and the first is the one that matters:

• UNMARKED: …hands the author a remedy that EXPANDS a shrink-only registry, and neither
marks that path ⛔ MAINTAINER-ONLY nor turns it down outright (#8435).
remedy: …add ` ‹X› ` to PENDING_DISPOSITION in ‹X› scripts/check-auth-mount-ledger.mjs with…
registry: PENDING_DISPOSITION (testimony: shrink)

That is not a paperwork gap. The gate was telling an author who could not decide a disposition to add their own route to a shrink-only exemption list — the parking-space move this card has already ruled against once, offered by the gate itself as a co-equal option.

The classification is a real question and was answered against the convention's own vocabulary, not by picking the quietest word:

  • The main population's remedy — ledger the mount, with evidence — is the landing author's, freely. Its target is AUTH_ROUTE_LEDGER, which grows as routes are added and is not a ratchet at all. The detector agrees: it anchored only on PENDING_DISPOSITION. So there is no split the vocabulary is failing to express — the convention classifies only the ratchet-expanding path.
  • Both paths that touch PENDING_DISPOSITION — adding a route, and raising PENDING_MAX — expand a shrink-only exemption list. Neither is the landing author's.
  • refused is the stronger shape and would be false here: the list holds a legitimate entry, granted by the maintainer ruling of 2026-08-22 on this card. There is a real act, with a real owner. Naming the owner is honest; denying the act is not.

check-skills-token-ratchet.mjs (#10473) records exactly this reading for exactly this shape — a new gate with two remedy paths, marked because "there IS a legitimate act, with an owner". Reached independently here, then checked against it.

Both author-facing messages now carry ⛔ MAINTAINER-ONLY, and CONTROL records the gate as marked with the split written into its why. Observed, not assumed: the sweep moved unmarked (1) → unmarked (0), marked (7) → marked (8).

⚠️The cheapest fix that turns CI green is a false declaration, and the ablations show it. The farm sweep checks token presence, delegating placement to per-gate self-tests ("Presence here, placement there"). Stripping the token from both author-facing messages — leaving only the const — leaves check:ratchet-remedy-authority at exit 0, still counting this gate among the 8 marked, while the author reads nothing. Only the four new placement pins catch it. Adding the const and the CONTROL row alone would have satisfied CI and declared something untrue.

Both directions pinned

Predictions were written before every mutation; all seven matched.

#Mutation (on the real tree)PredictedObserved
Aadd a real rawApp.post for /admin/zzz-ablation-probeRED, 1 unaccounted-mount, scope 18exit 1, exactly that, scope: 18 rawApp mount(s)
Badd a real rawApp.all + rawApp.use under basePathGREEN, mounts still 17, lanes 2→4exit 0, 17 ... mount(s), 4 lane(s)
Cdelete the real POST /api/v1/auth/admin/sso/register row, leaving register-samlRED, naming it, with the prefix blockexit 1, NOT CREDITED, DELIBERATELY: ... is a strict PREFIX of POST /api/v1/auth/admin/sso/register-saml
Dadd a real ledger row for set-initial-password (simulating #10975 landing)RED, pending entry must be deletedexit 1, "Delete the entry — that is how the ratchet comes down"
Emutate reconcile to credit by substring--self-test RED on the boundary assertionsexit 1, a strict-prefix mount was CREDITED to its longer sibling
Fstrip the token from the demand block onlymy --self-test RED on 1 pin; farm sweep greenexactly that (sweep exit 0)
Gstrip it from both messages, leaving the constmy --self-test RED on 2 pins; farm sweep still greenexactly that (sweep exit 0, still "8 mark")

E and G are the two worth reading. Under substring accounting the production run of check:auth-mount-ledger stays green on a clean tree — only --self-test catches it. Under a stripped token the farm sweep stays green — only the per-gate pins catch it. In both cases the instrument that fails is the self-test, which is why this repo's --self-test-first convention is load-bearing rather than ceremonial.

Restores proven byte-identical by git hash-object, then re-run to a real verdict: auth-plugin.ts56c783b059d81e058c540079db786c204bc8351f, auth-route-ledger.ts123e1a87dcedf63dc188cf680c2fc3ed08417913, conformance test d4c9cb0dfffc25cbef248afa78195c60c2b1647b, check-auth-mount-ledger.mjs496075b88cb1b4d116d7574d93280afc16bcd14a (at the new head); git diff --quiet exit 0; self-test 42/42 and both gates green afterwards.

No rebuild is involved, and that is argued from the files, falsifiably. The gate readFileSyncs two .ts paths under packages/plugins/plugin-auth/src/ — no import of the package, no exports resolution, no dist/. The single occurrence of the string dist in the script is the header sentence saying so. Demonstrated in both states: green when packages/plugins/plugin-auth/distdid not exist at all, and green again after the full workspace closure was built.

Gate union — derived on the final head 47ced12bf

node scripts/pm/dispatch-gates.mjs with no path arguments: 6 paths, 28 families. Re-derived after the second commit, and the second commit changed the unioncheck:ratchet-remedy-authority is the one added family (27 → 28). All exit codes captured before any pipe.

All 30 run, all exit 0: check:ratchet-remedy-authority · check:auth-mount-ledger · check:route-envelope · check:dispatcher-error-vocabulary · check:nul-bytes · check:entry-guard · check:parse-guard · check:changeset-gate-self-tests · check:objectui-changeset · check:node-version · check:pnpm-filter-targets · check:required-contexts · check:shard-attestation · check:slot-lookup · check:test-source-alias · check:type-source-resolution · check:workflow-status-functions · check:cross-package-test-inputs · check-adr-0087-registration · check-aggregator-roster · check-changeset-no-major · check-ci-filter-parity · check-empty-changeset · check-plugin-teardown-shape · check-step-collectors · docs-audit/check-affected-docs · check:query-options-erasure · check:engine-double-contract · check:where-matcher · check:type-check-coverage, plus check:type-check-debt.

Verdict lines, as printed on this head:

  • check-auth-mount-ledger --self-test: 42 assertions OK (right boundary, lane exclusion, rationale, pending ratchet).
  • check-auth-mount-ledger: OK -- 17 ObjectStack auth mount(s), all accounted for (12 by a reviewed ledger row, 4 shadowing a vendor-declared path, 1 pending a disposition).
  • OK check-ratchet-remedy-authority: 122 scripts swept (scripts/*.{mjs,mts}); 8 mark the expanding remedy ⛔ MAINTAINER-ONLY, 4 turn it down outright, 110 hand out no ratchet-expanding remedy. Control corpus: 21 hand-classified scripts, set-equality audited both ways.
  • check-nul-bytes: OK (scanned 6454 text file(s) ... no raw ASCII control bytes).
  • check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 247.3s, 1895 raw tsc error(s) total, none above its recorded number.
  • pnpm --filter @objectstack/plugin-auth testTest Files 69 passed (69) / Tests 1449 passed (1449); typecheck exit 0.

Class #10309, seventh confirmation — and the first that cost a red. The 27-family union at b86bd9e76 did not name check:ratchet-remedy-authority, and nobody named it by hand, so it ran for the first time in CI. It is named now only because this PR edits the sweep's own file; measured on 47ced12bf, dispatch-gates still returns 0 hits for it against scripts/check-auth-mount-ledger.mjs or any hypothetical new gate path, with a positive control confirming the expression works (check:entry-guard is named for both). --residue puts it in the undetermined bucket — its sources name no path at all, because its population is built by readdirSync and its only filename literals are bare CONTROL basenames, which extractWatchHints refuses as too generic. A gate that sweeps every script in the repo is derivable for none of them. Filed as #11155, unassigned, with the remedy the residue note itself names; not fixed here, being a different defect class with its own landing surface.

Recorded, not claimed as coverage: check:driver-memory-census and check-test-completeness are unreachable by construction on this tree.

TEST_DEBT (#10615): untouched. Number not edited, --lower not run. The ratchet reports @objectstack/plugin-auth: TEST_DEBT records 109, tsc now reports 97 (-12) -- the entry can be lowered. Not an error — pre-existing surplus, left exactly as found.

The test-file trap was checked explicitly.packages/plugins/plugin-auth/tsconfig.json excludes **/*.test.ts, so the package typecheck cannot see the only file this PR touches under packages/**. A test-inclusive tsc program was run on both sides: 97 errors with this branch's file, 97 with origin/main's — identical, and the diff of that file contains zero non-comment changed lines.

One measurement error caught and corrected rather than reported: a first pass of the union on the new head showed five gates red with ERR_MODULE_NOT_FOUND. The worktree had been recreated without pnpm install. Reinstalled, re-run, all green — recorded here because a red read from an uninstalled tree is exactly the kind of number that gets reported as a finding.

Not touched

scripts/docs-audit/affected-docs.mjs — follow-up 3 is a design fork on its own card. #10975 and #10974 are separately tracked and this PR neither anticipates nor blocks them; PENDING_MAX was not raised.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

The PENDING_DISPOSITION paths expand a shrink-only exemption list; both now
name their owner in the author-facing message, and the control corpus in
check-ratchet-remedy-authority.mjs records the gate as 'marked'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32601247997 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 4.08s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 101 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 26dea14Aug 22, 2026
36 checks passed
@os-warren
os-warren deleted the claude/issue-10534-mount-ledger-gate branch August 22, 2026 22:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude