fix(core,rest,services): fail loud when a permission-store read fails - #13475
Conversation
…#13279) `resolveAuthzContext`'s `tryFind` answered a THROWN read the same way it answered an EMPTY one — `[]` — so a permission-store outage resolved as an authenticated principal holding zero capabilities and the package door answered a 403 byte-identical to a genuine capability denial. `tryFind` now distinguishes the two facts and raises `AuthzStoreUnavailableError` (existing ADR-0112 code `SERVICE_UNAVAILABLE`, status 503) when a read is issued and throws. The `!ql` guard is untouched: an absent engine is not a failed read. Making the resolver loud is necessary but not sufficient, which was measured rather than assumed: six of the eight production transports wrap the call in a fail-closed `catch`, and with those untouched the door answered 401 — the outage had merely changed disguises. Each now re-raises the branded error and keeps its prior behaviour for every other fault. Maintainer ruling 2026-08-30, verbatim: 第一批其余同意 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift CheckThis PR changes 9 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 45 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 4d210ae2d9409e9eb324951b73b28cb8612041ca && git checkout 4d210ae2d9409e9eb324951b73b28cb8612041ca
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b2dea862cb2c7caf67bddf99ef8b5f7542e68f80 cfe54c80d08462121f7f51db504f4a5b9f70122d && git checkout -B drift-repro b2dea862cb2c7caf67bddf99ef8b5f7542e68f80 && git merge --no-ff cfe54c80d08462121f7f51db504f4a5b9f70122d
node scripts/docs-audit/affected-docs.mjs --json b2dea862cb2c7caf67bddf99ef8b5f7542e68f80
|
Declared-breaking changesets must answer the ledger question in writing. This change touches no metadata surface in either direction — no Zod schema, no spec declaration, no authorable key, no stored row, no object definition — so `objectstack migrate meta` has nothing to visit and no tombstone exists to mint. `SERVICE_UNAVAILABLE` is an existing `StandardErrorCode` member, so even the wire vocabulary is unchanged; only which declared code an outage selects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
The changeset claimed "embedders without a data plane are unaffected". Measured false: the pinned cases cover an UNWIRED engine and an EMPTY one, not a real engine whose sys_* tables were never created, where find is issued and throws `no such table`. That shape is currently treated as an outage and must not be — with no permission tables provisioned, zero capabilities is the true answer. Records the measured cost (client CRUD 503s, 400s becoming 503s, two silenced diagnostic channels) and why the boundary cannot be drawn inside core. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
|
PM review — no ACCEPT/REWORK verdict. This card is escalated to the maintainer as The ruled repair is implemented, measured and pinned. What blocks it is not defect quality: implementing the ruling faithfully falsified a premise the card was built on, and the remaining question — where the loud/quiet boundary sits, and which package owns the classifier that draws it — is a security-boundary and cross-package-structure call. That is a human floor, not a seat's. Reconciling the report against the CI record I collected — including one correction to my own readingI collected six red checks independently at the previous head
The scope question I made mandatory at dispatchWas each transport edit forced by the new signal, or opportunistic? — answered by measurement, not argument: with the resolver loud and the nets untouched, the package door answered 401. The outage changed disguise rather than disappearing, from "you hold no capability" into "you are not authenticated", which is byte-identical to a genuine anonymous caller. Each of the seven transport files carries the same three-line re-raise inside a pre-existing fail-closed catch, with no behaviour change for any other fault; Handling I want on the record⛔ No assertion was relaxed, re-baselined, allow-listed or deleted. The reds are left standing because they are the finding. The alternative — editing tests to match code on a security surface — is what I would have rejected the PR for. The changeset's own earlier overclaim, that "embedders without a data plane are unaffected", is retracted in writing by its author; recorded here so the retraction is not only in a superseded revision. Posture⛔ Draft. ⛔ Not enqueued. ⛔ Nothing merged, and nothing pushed to this branch by me at any point. #13279 moves Generated by Claude Code |
|
Addendum to In that comment I wrote that
Relayed to the implementing seat with the merge-base check first: if the census is already red on the base, this is not this PR's and the outcome is a comment, not a fix. ⛔ The PR stays draft and unenqueued regardless; the five
Generated by Claude Code |
… line shift Pure line rot from this PR's transport edits, not a population change — measured both ways before touching the page: merge base 71627f7 census EXIT=0, 109 sites / 145 anchors / 27 non-read this branch same 109 sites; rest-server.ts holds 6 read sites on BOTH sides, shifted [1266 4270 5633 5865 6210 6403] -> [1269 4281 5644 5876 6221 6414] Every re-anchored line was checked content-identical between base and head (+3 before the import insertion, +11 after the computeExecCtx expansion). `--fix` repaired package-routes.ts and sharing-plugin.ts mechanically. It REFUSED rest-server.ts, reporting "page anchors 7 distinct read line(s), census finds 6 -- the POPULATION changed". That refusal is a limitation of the --fix heuristic, not a population change: it counts the two ledger-excused non-read citations (`:1234`, `:1263`, the "never settable from inbound HTTP" pair) as read anchors. The census itself reports 6 reads on both sides. Those five anchors were therefore re-anchored by hand to lines proven byte-identical, and NON_READ_ANCHORS needed no edit — it locates rows by needle, so it re-resolved itself once the page pointed at the right lines. No row was written, deleted or reworded; only line numbers move. The gate's verdict line is now byte-identical to the merge base's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
|
On the census repair at Straightening the record, because My ⛔ rested on a premise: a What I verified myself, by content on the commit rather than from the report:
⇒ The distinction that matters: re-baselining is changing what the page asserts; this changed only where it points. A
⛔ PR remains draft and unenqueued. The fork on #13279 is untouched by any of this and still needs the maintainer. Generated by Claude Code |
|
Census repair at
The push landed at 14:40:14, so this is not a scheduling lag — no workflow was created for that sha at all. What it means concretely: the census ⛔ Deliberately not doing any of: an empty commit, a close-and-reopen, or a speculative re-run to kick CI. The situation resolves itself — #13279 is now ruled (option A, The fact is also handed forward in the R4 claim ( Generated by Claude Code |
…uinely unreachable store Implements the maintainer's 2026-08-30 option-A ruling on top of the existing loud-failure implementation. "The read failed" is two facts. A read also throws when the table was never PROVISIONED -- a real engine, wired and reachable, whose sys_* tables were never created. There "zero capabilities" is the true answer, not a fabrication; only an UNREACHABLE store leaves it unknown. Treating them alike turned four CI suites red. - Relocate isMissingTableError (and its sibling isSchemaAlreadyExistsError, which shares its matcher and cannot be separated from it) from @objectstack/metadata to @objectstack/types, the package @objectstack/core already depends on. @objectstack/metadata/errors still exports isMissingTableError by re-export, so no consumer of that published subpath changes. - tryFind raises AuthzStoreUnavailableError only for a read failure that is NOT positively identified as an unprovisioned table. - Record the signed-off false-positive risk beside the predicate, as the ruling requires, and pin both directions by name in authz-store-unavailable.test.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
Resolves one conflict, in content/docs/permissions/system-context.mdx. All four conflict hunks were PURE LINE-NUMBER ROT -- proven mechanically by comparing both sides with digits stripped: prose identical, no census row written, deleted or reworded on either side. Resolved to main's side and the anchors re-derived from the merged tree by check-system-context-census, since rest-server.ts moved on BOTH sides and therefore neither side's numbers were correct after the merge. No generated artifact was touched: the branch never edited an os-regen path, os-regen-pending is empty, and every generated path equals main's side byte for byte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
… merge `rest-server.ts` moved on BOTH sides of the merge, so neither side's anchors were correct afterwards. `--fix` repaired `package-routes.ts:92 -> :97` and then refused `rest-server.ts` with "page anchors 7 distinct read line(s), census finds 6 -- the POPULATION changed". That refusal is FALSE, and the same run says so: its own `[ledger-row-unused]` line reports NON_READ_ANCHORS excusing `rest-server.ts:1238` with no anchor pointing there. The page carries EIGHT anchors into that file -- six elevation reads plus the two `NON_READ_ANCHORS`-excused inbound seams -- and `--fix` counts the excused pair as read anchors. Filed as #13490. Re-anchored by hand, every target proven BY CONTENT rather than by arithmetic: :1235 -> :1238 holds the exact ledger needle '"authenticated". `isSystem` flags are never set on inbound HTTP' :1264 -> :1267 holds the exact ledger needle '`isSystem` is never set on inbound HTTP, so it cannot bypass.' :1267 -> :1270 row 17's READ anchor. The arithmetic could not see this: :1267 was simultaneously the second ledger seam's new home and row 17's stale anchor. :4271 -> :4282, :5634 -> :5645, :5866 -> :5877, :6211 -> :6222, :6404 -> :6415 row 50's five read anchors, each the census's own read site No row was written, deleted or reworded -- only where they point. Population proven unchanged against origin/main in a compare worktree: both verdict lines are byte-identical, "109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
|
Closing my own caveat: the census repair is now CONFIRMED by CI, and the CI silence is explained. I said twice on this PR that the census green was a local reading only ( The silence had a cause, and it is measured rather than assertedAt 16:09Z this PR was The conflict is now resolved — Results at head
|
| check | verdict |
|---|---|
Lint & Repo Gates (carries check-system-context-census) |
✅ success |
Test Core 2/6, 3/6, 4/6, 5/6, 6/6 |
✅ success |
Test Core 1/6 |
in progress |
| totals | 30 success · 1 skipped · 1 in progress · 0 red |
⇒ The census repair at 603c9cbb6b is green on the gate itself, not just locally. The hand re-anchoring — which overrode an explicit ⛔ of mine after measuring my premise false — was correct end to end, and #13490 stands as the gate defect that made --fix refuse falsely.
⇒ The four previously-red Test Core shards have recovered, which is what ruling A predicted.
Fences, re-checked on the current 23-file diff
No content/docs/releases/, packages/spec/, docs/adr/, .claude/ or skills/. ⭐ Worth naming: the docs-drift run flagged content/docs/releases/v17.mdx as carrying isMissingTableError — the very symbol this PR relocates — and the page was correctly left alone. That is a release-owned page; a code PR editing it is exactly what the guardrail exists to stop, and the tempting "fix" was declined.
⛔ Still draft, still unenqueued. This is 条款② and goes through the review chain; a PM ACCEPT is not the landing authority here.
Generated by Claude Code
|
Completing the count I deliberately left open: it is six of six. Head In
⇒ Ruling A's prediction is confirmed in full. All four previously-red signatures recovered — ⇒ And the census repair at Where this PR now stands⛔ It does not get flipped ready by this seat. The ruling files it 条款②档 with the PR going through the review chain, so a green CI plus a PM reading is not the landing authority here — contract review at Holding as draft. The implementing seat has not filed its closing report yet, so I am recording the measurement rather than issuing a verdict; a head that moves again re-opens both the CI reading and the review. Generated by Claude Code |
|
PM review — implementation ACCEPTED. Landing now waits on 条款② contract review, which I have dispatched at State: head What I verified myself
⭐ The check that a clean merge makes easiest to skip
Zone 2 — assumption A broke, in three ways worth recordingI assumed a mechanical single-function move. It was not:
What held: the signature moved byte-identical, the published The signed-off risk is enforced, not merely writtenThe note sits in The four reds, per the ruling's conditionAll four recovered, in CI and locally: Routed#13516 (three stale comment pointers in Generated by Claude Code |
条款② contract review — PASS WITH NOTESRun at Limb 1 — public surface: correct and minimalThe widening is forced, not opportunistic, and the reviewer measured it rather than accepting the framing:
Limb 2 — contract behaviour: as ruled, pinned in both directions
⭐ The reviewer ran both ablation directions, including the inverse one the implementing seat did not:
⇒ The two directions are genuinely independent tripwires; neither is gated on the other. That is a stronger statement than either party had before. Grants-cache joint-correctness — independently verified.
|
…rce the census claim Contract review (条款②) on PR #13475 found four `this.resolveExecCtx(environmentId, req)` sites still passing `.catch(() => undefined)` — the CONTINUATION-layout ones the census file's own docblock warns about in as many words ("a single-line grep counts 16 and misses those four"). The conversion had reached the 16 single-line sites only. serveMetaItemLayered, GET meta/:type, GET meta/:type/:name and GET meta/:type/:name/published each swallowed the loud failure, so during a permission-store outage they could serve an org-unscoped, env-wide 200 instead of the declared 503. Converted, same shape as the other 16. Measured after: 20 resolveExecCtx sites guarded (16 inline + 4 continuation), 0 remaining `() => undefined` after a resolver call, plus computeExecCtx's own net = 21 guarded sites. ⛔ The sharper half was a FALSE COMPLETENESS CLAIM this PR shipped in the census file: "every site now passes `rethrowAuthzStoreUnavailable`". It was false when written, in the very file that documents the trap, and no pin failed — the sibling ledger's per-transport check is PRESENCE-based, so one converted site satisfies it for the whole file and a PARTIAL conversion is invisible to it. So the sentence is not merely corrected; it is replaced by measurement: - §7 re-derives the catch ARGUMENT at every site from source, in both layouts, and fails on any `() => undefined` survivor or any local re-spelling of the shared guard. A CONTROL asserts it finds both layouts, so a regex that stopped matching cannot read as a clean pass. - §8 drives the doors: with the resolver rejecting, no route touching a continuation site answers 200, and each keeps the declared 503 or propagates. Its healthy leg is the anti-vacuity control — it proves those sites are reached AND that 200 is their healthy answer, which is exactly what the defect fabricated. - The docblock records the miss, and states the ledger's reach so nobody reads its green as covering this.⚠️ §8's OWN reach is recorded too, and it was measured by ablation rather than assumed: reverting one of the four sites turns §7 red and leaves §8 GREEN, because the `${metaPath}/:type` handler (4332-4794) resolves the context three times, so under a total outage a later guarded site still refuses. §8 pins what a DOOR answers; §7 pins what each SITE spells. The case neither covers — a PARTIAL outage where only the first read fails, which is where the org-unscoped 200 actually appears — is stated as not measured rather than implied away. Site lines are DERIVED, never transcribed: hardcoded numbers in this file are what went stale and hid the four sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
条款② delta review — PASS, now unconditional on
|
| ablation | result |
|---|---|
revert one continuation site to .catch(() => undefined) |
§7's two enforcement tests red, and the failure names the site: expected [ '2746 (continuation)' ] to deeply equal [] |
reformat so .catch( lands alone and the argument moves a line further down — invisible to the regexes while siteTable still counts it caught |
red anyway — the both-layouts CONTROL fails (continuation count drops below 4, length mismatch with CAUGHT), and §8's reachability CONTROL fails too |
⇒ Every evasion path the reviewer could construct fails toward detection, never toward a silent pass. And §8 stayed green under ablation 1 exactly as its own docblock predicts (the :type handler resolves three times, so a total outage still refuses via a later site) — §7 is the per-site tripwire, §8 the door-level pin, and the file says so rather than letting §8's green be over-read.
⇒ The false completeness claim did not merely get corrected; it became a machine-checked assertion on the blind spot that let it ship — the ledger's presence-based check.
Nothing else moved, and the base advance is clean
git log 0b71a8a13e..cfe54c80d084 is one commit; git diff --stat is two files. No types, metadata, core, spec or docs file touched. And main's own advance 4a24b96086..b2dea862cb touched none of packages/core/src/security/, rest-server.ts, packages/types/src/ or packages/metadata/src/ (empty log, measured) ⇒ no new auto-merge joint-correctness interaction of the kind the grants cache created earlier.
Limb 1 is untouched by the delta and carries forward. Limb 2 re-verified at the new head: core pin suite 37/37 (including the transport ledger's set-equality and both direction pins), the two rest suites 63/63.
The body now reads truthfully
"18 nets" is explicitly retracted as a +-line count, with the miss it produced, and replaced by the measured 21 guarded sites (20 call sites = 16 inline + 4 continuation, plus computeExecCtx's catch). The surface statement now declares six exports on @objectstack/core's root alongside the two on @objectstack/types.
Two residuals, recorded as residuals — not conditions
An outage against the grants cache turned ON (still #11971's contract, unpinned by anyone) and §8's declared not-measured partial-outage case. Both are stated in the PR itself rather than discovered later.
Landing posture
⛔ Still draft. CI on cfe54c80d084 is 19 success · 1 skipped · 11 running · 0 red. The two cancelled entries are superseded duplicates — each has a newer successful run of the same workflow on this same head (99296607297, 99296608523), so both guards did reach a verdict. On full green I flip ready and enqueue; ⛔ I do not merge it.
Generated by Claude Code
Part of #13279
⛔ DRAFT. Landing is the PM's call, not this branch's.
Implements the maintainer's second ruling of 2026-08-30 (第 5 场总监席决裁批 #9, verbatim
「同意」) — option A — on top of the loud-failure implementation this PR already carried.
The 7 transport guards, the
rest-server.tsnets, the inverted #13282 pin and the censusrepair are all retained unchanged.
rest-server.tsnets". That was a +-linecount (16 catch swaps +
computeExecCtx's catch + the import), i.e. exactly thesingle-line counting the census file warns against — and it is consistent with how four
sites came to be missed. The measured site count, after the fix below, is 21 guarded
sites: 20
resolveExecCtxcall sites (16 inline, 4 continuation) pluscomputeExecCtx'sown blanket catch.
What was blocking, and what A does about it
The previous revision made
tryFindfail loud on any thrown read. That was faithful tothe first ruling and it turned four CI suites red, because "the read failed" is not one fact.
A read also throws when the table was never provisioned — a real engine, wired and
reachable, whose
sys_*tables were never created. That is a supported, deliberately testeddeployment shape (
packages/runtime'snotifications.hono.integration.test.tsnames itABSENT_AUTHZ_TABLES). There "zero capabilities" is the true answer rather than afabrication: nothing is provisioned, so nothing was withheld. Only an unreachable store —
the ruling's own word 不可达 — leaves the answer unknown, and only an unknown answer may not
be reported as a capability denial.
Ruling, verbatim:
The relocation, and its real blast radius
@objectstack/corecannot import@objectstack/metadata— metadata depends on core — sothe one classifier moved to the package both sides already depend on. This is the repo's own
stated plan:
packages/metadata/src/errors.tsrecorded it as option 2, "architecturallyattractive and explicitly not precluded by this module", and kept its export as "a single,
greppable seam to delete if the maintainer later takes option 2".
attention:
isMissingTableErrorisSchemaAlreadyExistsErrormatchesDriverErrorwith the above — the module's own docblock says the sharing is the point. Leaving it behind meant either exporting the matcher as machinery, or re-rolling it inmetadata; the second is the duplication this module exists to retiregit mv, so the predicate and its pins stay in one placepackages/types/src/driver-error-classification.ts, exported from the packageindex. No new dependency edge, in either direction:
@objectstack/typesdepends only on@objectstack/spec, and the module's own front-exclusion has readisRelationSubObjectPhrasefrom
@objectstack/typessince [finding] The Postgres"x" of relation "y"phrase is now open-coded in three packages, each repairing the same superstring hole separately —@objectstack/typesis the established home #6615 — so the move actually removes a cross-package edge.@objectstack/metadata/errors— the published subpath — is unchanged in surface and nowre-exports from the new home. No out-of-repo consumer changes, and no call site of that
subpath was edited (
metadata-protocolandobjectqlimport it and were left alone).metadata's internalutils/schema-sync-errors.tsis deleted rather than left as aforwarding stub; its two in-package readers (
errors.ts,loaders/database-loader.ts) nowimport
@objectstack/typesdirectly. That is the only consumer edit the move required.@objectstack/typesgains two public exports, not one.declared:
@objectstack/core's root surface gains SIX exports viasecurity/index.ts— the error class, the two guards(
isAuthzStoreUnavailableError,rethrowAuthzStoreUnavailable) and three constants(status, code, message). That is the necessary mechanism — every guarded transport
imports the guard from
@objectstack/core— but six is the number, not two.The signed-off risk, and the two pins that make it auditable
The ruling approved gating loudness on a driver-error predicate with its false-positive
direction stated on the record:
Both obligations are discharged, and both are checked by tests rather than asserted here:
Written beside the predicate —
packages/core/src/security/resolve-authz-context.ts,in
tryFind'scatch, immediately aboveif (isMissingTableError(err, object)) return [];.A test reads that
catchbody back and fails if the risk note is removed.Both directions pinned by name, in
packages/core/src/security/authz-store-unavailable.test.ts:[#13279 option A] ⭐ THE OUTAGE DIRECTION — a read failure that is not an unprovisioned table stays LOUD[#13279 option A] ⭐ THE UNPROVISIONED DIRECTION — a never-provisioned table resolves QUIETLYThe loud table carries the shapes a naive predicate would get wrong: a permission denial on
the table, the [finding]
isMissingTableError同样把 Postgres 写路径的「缺列」措辞判为「缺表」——其 docblock 明说 42703 必须响亮失败,而消费点会据此「从 1 开始编号」 #6347 Postgres missing-COLUMN phrase that contains a legal missing-tablephrase, and the isMissingTableError never checks WHICH table the "no such table" names — a view over a missing base table is read as "this table is not provisioned yet" #13324 phrase naming a different relation. The quiet table carries every
dialect spelling plus the production wrapper shape, where the driver's phrase is on
causerather than the outer message.Also pinned: a store that loses one table mid-resolution is still loud (the quiet branch
must not leak past the table it is about), and an unprovisioned deployment resolves
byte-identically to a reachable-but-empty one, with a healthy store as the control that
keeps that comparison from being vacuous.
⛔ No second safety net was added to hedge the accepted risk. A hedge makes loudness
conditional on two classifiers agreeing, which is strictly more ways to fall silent.
The four reds, measured
All four recover, and none by touching an assertion. ⛔ Nothing was relaxed, re-baselined,
allow-listed, skipped or deleted anywhere in this PR.
bf297e4ea7)5c7c0b66b9@objectstack/clientordinary CRUD / batch 400s answering 503client.hono.test.tspackages/runtimenotifications answering 401trigger-record-changenoise guardplugin-approvalsstatus-mirror-cascadenoise guardThe two diagnostic channels recover for the mechanism the report predicted:
sys_positionisread at
resolve-authz-context.ts:562, after the concurrent batch, so once absent tablesanswer
[]again the later read is issued and both channels emit.Four surviving swallow sites, found by contract review
⛔ The conversion had reached the 16 single-line sites and none of the four
continuation-layout ones —
serveMetaItemLayered,GET meta/:type,GET meta/:type/:name,GET meta/:type/:name/published. Each carried.catch(() => undefined)on the line after the resolver call, so during a permission-storeoutage they could swallow the loud failure and serve an org-unscoped, env-wide
200insteadof the declared 503.
That is precisely the miss this repo's own census file warns about in as many words — "a
single-line grep counts 16 and misses those four" — committed by the author who had just
written that sentence down. All four are now converted, same three-line shape as the other 16.
The sharper half was a false completeness claim this PR had shipped in
execctx-consumer-census.test.ts: "every site now passesrethrowAuthzStoreUnavailable".It was false when written, and no pin failed, because the sibling ledger's per-transport
check is presence-based — it asks whether the file contains the guard at all, so one
converted site satisfies it for the whole file and a partial conversion is invisible to it.
On a surface whose entire method is measured claims, a false completeness sentence in the
census file is worse than the four sites, because it is what a future reader trusts instead
of re-measuring.
So it is not merely corrected — it is replaced by measurement, and the docblock now records
both the miss and the ledger's reach:
fails on any
() => undefinedsurvivor or any local re-spelling of the shared guard. ACONTROL asserts it finds both layouts, so a regex that quietly stopped matching cannot read
as a clean pass.
answers
200, and each keeps the declared 503 or propagates. Its healthy leg is theanti-vacuity control — proving those sites are reached and that
200is their healthyanswer, which is exactly what the defect fabricated.
reverting one of the four sites turns §7 red and leaves §8 green, because the
${metaPath}/:typehandler (lines 4332–4794) resolves the context three times, so underthe total outage §8 drives, a later guarded site still refuses. §8 pins what a door
answers; §7 pins what each site spells. The case neither covers — a partial outage
where only the first read fails, which is where the org-unscoped
200actually appears —is stated as not measured rather than implied away.
Verification
The local runs below ran on
5c7c0b66b9.origin/mainwas then merged in and the wholefarm re-ran in CI on the final head
0b71a8a13e— see CI at the end of this section,which is the authority. Exit codes were captured by redirecting to a file before
capture, never through a pipe.
Full workspace build:
turbo build --concurrency=2— 72 successful, 72 total. Therelocation compiles across every package.
Tests — 14 packages, all exit 0:
@objectstack/core@objectstack/rest@objectstack/types@objectstack/metadata@objectstack/runtime@objectstack/client@objectstack/metadata-protocol@objectstack/objectqltrigger-record-change/plugin-approvalsAblation — the new pins can actually fail. Implementation committed first, so restore had
a real reference. Mutation: the narrowing line replaced by a marker. Confirmed on disk, not
by an editor exit code — removed-text count 0, injected-marker count 1, and the blob hash moved.
No rebuild was needed and that is a measured fact rather than an omission:
packages/core/vitest.config.tsaliases
@objectstack/typesto../types/src/index.ts, so the suite reads both the mutatedresolver and the relocated predicate from source, and no stale
distcan sit between them.Result: 7 failed / 30 passed — all six UNPROVISIONED pins and the risk-note pin fell, and
every OUTAGE pin stayed green, which is the point: the two directions are independent, and
the loud path is not gated on the narrowing. Restore ran from an
EXIT INT TERMtrap with anabsolute repo root and is proven by a HEAD-blob hash match plus an empty whole-tree
git diff HEADandgit status --porcelain.Gates — family derived against the actual change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(25 paths; 62 distinctgate commands). 60 exit 0, including
check:authz-resolver,check:adr-0087-registration,check:empty-changeset,check:changeset-gate-self-tests,check:test-source-alias,check:type-source-resolution,check:cross-package-test-inputs,check:undeclared-dep-imports,check:published-files,check:engine-double-contract,check:route-envelope,check:dispatcher-error-vocabulary,check:nul-bytes, and — answering the open question fromreview —
check-system-context-censusexit 0, whose verdict line is byte-identical to thebase's.
check:type-check-debt --re-measureis green after rebuilding core: "29 ledger entriesre-measured, 1547 raw tsc errors total, none above its recorded number".
The other 2 are PREREQUISITE NOT MET, not reds, and say so in their own output:
check-test-completeness(exit 3 — it grades a savedturbo run testlog, and none existslocally) and
check-half-states(exit 3 — the container token is not a GitHub credential).Lint. The repo-wide farm is not measured locally:
pnpm lintdied with anout-of-memory abort (exit 134) at the 4096 MB ceiling on a shared box. That is a resource
failure, not a finding. The declared narrowing instead, with all three pieces of evidence:
the universe is 5468 files, read from eslint's own config via
isPathIgnoredovergit ls-filesrather than guessed; the narrowed run covered 21 files — every lintablefile in the diff — counted from
--format json, with 0 errors and 0 warnings; and thenarrowing excludes nothing, because this repo's single
eslint.config.mjsnever enablestype-aware linting for any file (no
parserOptions.project, no typed rules — stated atline 328 and measured there with a positive control), so no verdict on an untouched file is
movable by this diff. CI owns the full farm.
The merge, and why CI had been silent
For three consecutive pushes this branch produced zero CI runs — measured, not assumed:
21 workflow runs existed on it, all on
85d27f4f9fandbf297e4ea7, and none on603c9cbb6bor5c7c0b66b9, while Actions was demonstrably healthy repo-wide. The cause wasa merge conflict against
main(mergeable: false,mergeable_state: dirty): aconflicted PR has no computable merge commit for
pull_requestworkflows to run against.Merging
origin/main(27 commits) fixed it, and the confirmation is the strongest available— CI fired immediately on the very next push.
One conflict, in
content/docs/permissions/system-context.mdx. All four hunks were pureline-number rot, proven mechanically by comparing both sides with digits stripped: prose
identical, no census row written, deleted or reworded on either side.
rest-server.tsmoved on both sides, so neither side's anchors were correct afterwards.
--fixrepairedpackage-routes.ts:92 -> :97and then refusedrest-server.ts— "pageanchors 7 distinct read line(s), census finds 6 — the POPULATION changed".
false, and the same run says so: its own
[ledger-row-unused]line reportsNON_READ_ANCHORSexcusingrest-server.ts:1238with no anchor pointing there. The pagecarries eight anchors into that file — six elevation reads plus the two excused inbound
seams — and
--fixcounts the excused pair as read anchors. That is #13490, reproduced.Re-anchored by hand, every target proven by content, never by arithmetic — the same
handling already reviewed and accepted on this PR. The arithmetic could not have found one of
them:
:1267was simultaneously the second ledger seam's new home and row 17's stale readanchor. Population proven unchanged against
origin/mainin a compare worktree — both verdictlines byte-identical.
CI on the final head
0b71a8a13e— 10 of 10 workflows greenSo the four previously-red suites are green in CI, not merely locally, and the census gate
is CI-confirmed for the first time on this branch. Its own verdict line from the job log,
byte-identical to my local run and to
origin/main's:not re-run the package suites locally. The shared verify lock was held ~24 minutes by
another agent's run and two 9-minute waits both timed out. CI was already running the full farm
on this exact commit, which is strictly stronger evidence than my local re-run would have been,
so I read that instead. The build state was refreshed per AGENTS.md §9
(
pnpm install --frozen-lockfile,rm -rf packages/runtime/.objectstack, full rebuild72/72 green) before any of it.
mainalso editedresolve-authz-context.tsand itauto-merged.
mainadded the #11971 cross-request grants cache toresolveUserAuthzGrants.Checked rather than assumed: the cache's
commit(...)sits after every read, so a throw fromtryFindcan never commit an envelope — an outage is not negatively cached — and the cache isOFF by default (
OS_AUTHZ_GRANTS_CACHE_TTL_MS=0). Test Core is green with both changespresent. Not measured, and stated as such: the behaviour of an outage against a cache that is
turned on is #11971's TTL/epoch contract and I added no pin for it.
Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
Scope
⛔ Untouched, as fenced:
content/docs/releases/,packages/spec/,docs/adr/,.claude/,skills/,AGENTS.md,CLAUDE.md. Out of scope and carried by their own cards, not addressedhere: #13476 and #13490 both remain open.
Generated by Claude Code