Skip to content

fix(core,rest,services): fail loud when a permission-store read fails - #13475

Merged
zhuangjianguo merged 8 commits into
mainfrom
claude/issue-13279-authz-read-failure-fails-loud
Aug 30, 2026
Merged

fix(core,rest,services): fail loud when a permission-store read fails#13475
zhuangjianguo merged 8 commits into
mainfrom
claude/issue-13279-authz-read-failure-fails-loud

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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.ts nets, the inverted #13282 pin and the census
repair are all retained unchanged.

⚠️ An earlier revision of this body said "18 rest-server.ts nets". That was a +-line
count
(16 catch swaps + computeExecCtx's catch + the import), i.e. exactly the
single-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 resolveExecCtx call sites (16 inline, 4 continuation) plus computeExecCtx's
own blanket catch.

What was blocking, and what A does about it

The previous revision made tryFind fail loud on any thrown read. That was faithful to
the 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 tested
deployment shape (packages/runtime's notifications.hono.integration.test.ts names it
ABSENT_AUTHZ_TABLES). There "zero capabilities" is the true answer rather than a
fabrication: 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:

裁定:采选项 A —— 把 isMissingTableError@objectstack/metadata 迁至
@objectstack/types(core 已依赖),metadata 保留 re-export 兼容;tryFind 仅对**未被判定为
「表未 provision」**的读失败抛 AuthzStoreUnavailableError(SERVICE_UNAVAILABLE / 503)。

The relocation, and its real blast radius

@objectstack/core cannot import @objectstack/metadata — metadata depends on core — so
the one classifier moved to the package both sides already depend on. This is the repo's own
stated plan: packages/metadata/src/errors.ts recorded it as option 2, "architecturally
attractive 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".

⚠️ The move is not the single-function move it looks like, and that is worth review
attention:

what movedwhy
isMissingTableErrorthe ruling names it
isSchemaAlreadyExistsError⚠️not named by the ruling. It shares one matchesDriverError with 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 in metadata; the second is the duplication this module exists to retire
the whole module + its 600-line test filemoved together, as git mv, so the predicate and its pins stay in one place
  • New home: packages/types/src/driver-error-classification.ts, exported from the package
    index. No new dependency edge, in either direction: @objectstack/types depends only on
    @objectstack/spec, and the module's own front-exclusion has read isRelationSubObjectPhrase
    from @objectstack/types since [finding] The Postgres "x" of relation "y" phrase is now open-coded in three packages, each repairing the same superstring hole separately — @objectstack/types is the established home #6615 — so the move actually removes a cross-package edge.
  • @objectstack/metadata/errors — the published subpath — is unchanged in surface and now
    re-exports from the new home. No out-of-repo consumer changes, and no call site of that
    subpath was edited (metadata-protocol and objectql import it and were left alone).
  • metadata's internalutils/schema-sync-errors.ts is deleted rather than left as a
    forwarding stub; its two in-package readers (errors.ts, loaders/database-loader.ts) now
    import @objectstack/types directly. That is the only consumer edit the move required.
  • @objectstack/types gains two public exports, not one.
  • ⚠️ And the surface statement was incomplete in the other direction, which contract review
    declared: @objectstack/core's root surface gains SIX exports via
    security/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
:

签字在案:基于 driver 错误码的表缺失判定获准在安全路径上门控响亮性;其假阳方向
(误判「表缺失」⇒ 静默恢复安静 403)是本裁定接受的已知风险,须在谓词旁注释写明并以测试钉住
两个方向(真 outage ⇒ 响;真未 provision ⇒ 零能力为真答案,不响)。

Both obligations are discharged, and both are checked by tests rather than asserted here:

⛔ 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.

redbefore (reported at bf297e4ea7)now, at 5c7c0b66b9
@objectstack/client ordinary CRUD / batch 400s answering 5033 failed / 1 passed in client.hono.test.tswhole package green: 30 files, 405 passed
packages/runtime notifications answering 40111 failed / 2957 passedwhole package green: 201 files, 2968 passed
trigger-record-change noise guardfile failed, 9 tests passed7 files, 78 passed
plugin-approvalsstatus-mirror-cascade noise guardsame guard, same payload34 files, 632 passed

The two diagnostic channels recover for the mechanism the report predicted: sys_position is
read at resolve-authz-context.ts:562, after the concurrent batch, so once absent tables
answer [] 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-store
outage they could swallow the loud failure and serve an org-unscoped, env-wide 200 instead
of 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 passes rethrowAuthzStoreUnavailable".
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:

  • §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 quietly 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 — proving those sites are reached and that 200 is their healthy
    answer, which is exactly what the defect fabricated.

⚠️§8's own reach, measured by ablation rather than assumed, and recorded in the file:
reverting one of the four sites turns §7 red and leaves §8 green, because the
${metaPath}/:type handler (lines 4332–4794) resolves the context three times, so under
the 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 200 actually appears —
is stated as not measured rather than implied away.

Verification

The local runs below ran on 5c7c0b66b9. origin/main was then merged in and the whole
farm 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. The
relocation compiles across every package.

Tests — 14 packages, all exit 0:

packageresult
@objectstack/core44 files, 1097 passed (was 1081; +16 new pins)
@objectstack/rest160 files, 2698 passed
@objectstack/types16 files, 473 passed
@objectstack/metadata38 files, 623 passed
@objectstack/runtime201 files, 2968 passed
@objectstack/client30 files, 405 passed
@objectstack/metadata-protocol145 files, 2017 passed (10 skipped, pre-existing)
@objectstack/objectql248 files, 4287 passed
trigger-record-change / plugin-approvals78 / 632 passed
service-datasource / settings / storage / plugin-sharing / mcp / cloud-connection591 / 519 / 485 / 678 / 266 / 340 passed

Ablation — 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.ts
aliases @objectstack/types to ../types/src/index.ts, so the suite reads both the mutated
resolver and the relocated predicate from source, and no stale dist can 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 TERM trap with an
absolute repo root and is proven by a HEAD-blob hash match plus an empty whole-tree
git diff HEAD and git status --porcelain.

Gates — family derived against the actual change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (25 paths; 62 distinct
gate 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 from
review — check-system-context-census exit 0, whose verdict line is byte-identical to the
base's. check:type-check-debt --re-measure is green after rebuilding core: "29 ledger entries
re-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 saved turbo run test log, and none exists
locally) and check-half-states (exit 3 — the container token is not a GitHub credential).

Lint. The repo-wide farm is not measured locally: pnpm lint died with an
out-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 isPathIgnored over
git ls-files rather than guessed; the narrowed run covered 21 files — every lintable
file in the diff — counted from --format json, with 0 errors and 0 warnings; and the
narrowing excludes nothing, because this repo's single eslint.config.mjs never enables
type-aware linting for any file (no parserOptions.project, no typed rules — stated at
line 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 85d27f4f9f and bf297e4ea7, and none on
603c9cbb6b or 5c7c0b66b9, while Actions was demonstrably healthy repo-wide. The cause was
a merge conflict against main (mergeable: false, mergeable_state: dirty): a
conflicted PR has no computable merge commit for pull_request workflows 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 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.rest-server.ts
moved on both sides, so neither side's anchors were correct afterwards.

--fix repaired package-routes.ts:92 -> :97 and then refused rest-server.ts"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 excused inbound
seams — and --fix counts 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: :1267 was simultaneously the second ledger seam's new home and row 17's stale read
anchor. Population proven unchanged against origin/main in a compare worktree — both verdict
lines byte-identical.

CI on the final head 0b71a8a13e — 10 of 10 workflows green

workflowresult
CI (Build Core, Build Docs, Test Core 1–6/6, Dogfood Regression 1–3/3, Dogfood Verify CLI, Temporal Conformance live PG + MySQL)success
Lint & Type Check (Lint & Repo Gates, Type Check source/workspace/consumer gates, debt-ledger re-measure)success
Spec Liveness Check · Docs Drift Check · PR Automation · Governed Surface Guard · Single-Claim Path Guard · Duplicate Fix Guard · Part-of Closing-Keyword Guard · Check Linkssuccess

So 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:

check-system-context-census --self-test: all cases passed
check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.

⚠️Declared narrowing, so it is not mistaken for a full local re-run: after the merge I did
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 rebuild
72/72 green) before any of it.

⚠️One joint-correctness note, since main also edited resolve-authz-context.ts and it
auto-merged.
main added the #11971 cross-request grants cache to resolveUserAuthzGrants.
Checked rather than assumed: the cache's commit(...) sits after every read, so a throw from
tryFind can never commit an envelope — an outage is not negatively cached — and the cache is
OFF by default (OS_AUTHZ_GRANTS_CACHE_TTL_MS=0). Test Core is green with both changes
present. 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 addressed
here: #13476 and #13490 both remain open.


Generated by Claude Code

…#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
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 30, 2026
@github-actions

github-actionsBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 9 package(s): @objectstack/cloud-connection, @objectstack/core, @objectstack/metadata, @objectstack/plugin-sharing, @objectstack/rest, @objectstack/service-datasource, @objectstack/service-settings, @objectstack/service-storage, @objectstack/types, touching 44 documentable anchor(s). ⚠️5 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/metadata/src/errors.ts, packages/metadata/src/loaders/database-loader.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via getBookTree (sdk), meta.getBookTree (sdk), meta.publishItem (sdk), meta.rollbackItem (sdk), publishItem (sdk), rollbackItem (sdk))
  • content/docs/concepts/metadata-lifecycle.mdx(via sys_metadata_history (literal))
  • content/docs/deployment/cli.mdx(via sys_metadata_history (literal))
  • content/docs/kernel/contracts/metadata-service.mdx(via /:type/:name/publish (route), /:type/:name/rollback (route))
  • content/docs/permissions/administrator-guide.mdx(via sys_team (literal))
  • content/docs/permissions/sharing-rules.mdx(via sys_team (literal))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via sys_metadata_history (literal))
  • content/docs/releases/v14.mdx(via /book/:name/tree (route))
  • content/docs/releases/v17.mdx(via isMissingTableError (symbol), sys_metadata_history (literal), sys_team (literal), /:type/:name/publish (route))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/metadata/src/errors.ts, packages/metadata/src/loaders/database-loader.ts, …) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: rethrowAuthzStoreUnavailable (4 routes)
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 45 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b2dea862cb2c7caf67bddf99ef8b5f7542e68f80packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4d210ae2d9409e9eb324951b73b28cb8612041ca — the merge of head cfe54c80d08462121f7f51db504f4a5b9f70122d into base b2dea862cb2c7caf67bddf99ef8b5f7542e68f80, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b2dea862cb2c7caf67bddf99ef8b5f7542e68f80 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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
@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

PM review — no ACCEPT/REWORK verdict. This card is escalated to the maintainer as needs-user-decision.

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 reading

I collected six red checks independently at the previous head bf297e4ea7 and classified them into at least three distinct causes. That was wrong on the cause count, and the error is mine. All four functional reds reproduce to one root cause — no such table on sys_user / sys_member / sys_user_position / sys_user_permission_set. I had treated a different signature (the @objectstack/client shard: ordinary CRUD 200 → 503, batch validation 400 → 503) as a different cause; it is the same conflation seen from a different door. The severity reading attached to that shard stands unchanged, and it is what makes this fork blocking rather than cosmetic: validation 400s answering 503 is a main-data-path correctness defect.

  • Check Changeset — the missing ADR-0087 disposition, the one red I called a one-line fix — is green at 85d27f4f9f.
  • Lint & Repo Gates, which I reported as not measured rather than diagnosed, is still in flight. It is outside the fork either way.
  • The two silenced diagnostic channels are the sharpest reading in the report, and I agree with its framing: a change made to stop a failed read being silent made two other channels silent, because the eager throw aborts the resolution before sys_position is read at resolve-authz-context.ts:562. That is a consequence of the same over-firing, not a second defect.

The scope question I made mandatory at dispatch

Was 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; rest-server.ts needed 18 sites because computeExecCtx's blanket catch is the first and only net that fires. Answer accepted.

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 pm:dispatchedneeds-user-decision. The fork, its four options and the four-prism recommendation are in the os-dev-report comment on #13279; I am carrying it to the maintainer in this round's report.


Generated by Claude Code

@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

Addendum to 5469273585 — one correction, and it changes what the maintainer is looking at.

In that comment I wrote that Lint & Repo Gates was still in flight and "outside the fork either way". It has now completed, red, and I have measured it on the job log (99271396665) rather than leaving it as an assumption. Both halves of my sentence need splitting:

  • True: it is independent of the fork. It has nothing to do with where the loud/quiet boundary is drawn.
  • Wrong in what it implied: I wrote it in a way that reads as "nothing to act on". It is real work on this PR, and it needs doing whichever way the fork resolves.

check-system-context-census reports 18 problems over 145 anchors and 109 census sites. Every one names a file this PR edits — packages/rest/src/rest-server.ts, packages/rest/src/package-routes.ts, packages/plugins/plugin-sharing/src/sharing-plugin.ts. content/docs/permissions/system-context.mdx pins the isSystem elevation read sites by exact line number, and the transport edits moved them; the errors arrive as matched pairs of "the page anchors X, which is not a read site" and "line Y reads ctx.isSystem and no row anchors it".

⚠️ Stated as a hypothesis, not a diagnosis: I paired anchored lines to read lines by proximity in the log, and I did not run the census. The gate itself is the arbiter — --fix rewrites pure line rot and refuses a population change rather than guessing. If it refuses, that refusal is a finding about a security-documented surface, not something to hand-edit away.

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 Test Core reds on this head remain the deliberately-standing finding and no assertion in them should move.

⚠️ Also stated as unmeasured rather than folded into the above: I have not independently mapped Test Core shards 1/2/3/6 onto the four packages the report names. That mapping is consistent with the report; I did not verify it and I am not claiming it.


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
@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

On the census repair at 603c9cbb6b — the hand-edit was right, and my ⛔ was the thing that was wrong.

Straightening the record, because 5469304075 publicly told the implementing seat "⛔ if --fix refuses, do NOT hand-edit", and it then hand-edited five anchors. That reads as a violation in the thread; it is not one.

My ⛔ rested on a premise: a --fix refusal means the population of elevation read sites changed. That premise was measured and found false here. --fix refused rest-server.ts with "page anchors 7 distinct read line(s), census finds 6 — the POPULATION changed", but the page carries eight anchors into that file — the six reads plus the NON_READ_ANCHORS-excused pair that inbound HTTP can never set — and --fix counts the excused two as read anchors. The same run's own [ledger-row-unused] line, naming rest-server.ts:1237, is the corroboration sitting in its own output. The gate defect is filed as #13490 (routed pm:retriage).

What I verified myself, by content on the commit rather than from the report:

  • Merge base first, as the instruction required — the census is EXIT=0 on 71627f7b4e, so the red was this PR's. (Its first base run exited 1 on ERR_MODULE_NOT_FOUND and was correctly read as never entered the gate body, not as a red.)
  • The diff is 7 additions / 7 deletions in one filecontent/docs/permissions/system-context.mdx, which is not a fenced path. Every hunk moves a line number and nothing else: rest-server.ts:1234,:1263 → :1237,:1266 (+3), row 17 :1266 → :1269, row 36 sharing-plugin.ts:1073 → :1077, row 50's five rest-server anchors +11, rows 52 and 54 package-routes.ts +5. No row was written, deleted or reworded. That is the substance my ⛔ existed to protect, and it held.
  • Population measured both ways rather than argued: 109 sites on base and on branch, 6 read sites in rest-server.ts on both sides, each re-anchored line checked content-identical before the edit.

⇒ The distinction that matters: re-baselining is changing what the page asserts; this changed only where it points. A --fix refusal is evidence, not a verdict — and when its stated reason is measurably false, following it literally would have meant reporting a population change that did not happen. That is the second time this round a seat has falsified one of my stated premises with a measurement instead of working around it silently, which is the arrangement working as intended.

⚠️Still open, and not mine to close: the census's own verdict at the new head. The dev reports EXIT=0 with a verdict line byte-identical to the base's; CI has not yet produced any check run on 603c9cbb6b, so that is a report, not yet a reading. I am not re-running anything or drawing a conclusion from line arithmetic — the gate is the arbiter and I will read it when it lands.

⛔ PR remains draft and unenqueued. The fork on #13279 is untouched by any of this and still needs the maintainer.


Generated by Claude Code

@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

Census repair at 603c9cbb6b is still unverified by CI — measured twice, 21 minutes apart.

whencheck runs on 603c9cbb6bworkflow runs on this branch
14:50021, every one on 85d27f4f9f
15:11021, every one on 85d27f4f9f

The push landed at 14:40:14, so this is not a scheduling lag — no workflow was created for that sha at all.⚠️ Why, I have not measured, and I am not going to name a mechanism I have not run.

What it means concretely: the census EXIT=0 and the byte-identical verdict line reported for that commit are the implementing seat's local readings. They are well-evidenced and I reviewed the commit's content directly (7+/7−, one file, line numbers only) — but no gate has confirmed them, and this PR now travels the 条款② review chain, where an unconfirmed green is exactly the kind of thing that should not pass silently.

⛔ 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, 5469399533) and dispatched as R4 on this same branch, so the next push carries the census page with it and CI checks it then. This note exists so that if the gate comes back red at that point, nobody reads it as a new regression: it is the same repair, finally measured.

The fact is also handed forward in the R4 claim (5469431530) rather than left for the next seat to rediscover.


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
@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

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 (5469356877, 5469491073). That is now settled, so the record should not end on the doubt.

The silence had a cause, and it is measured rather than asserted

At 16:09Z this PR was mergeable: False / mergeable_state: dirty — base b6d3d76b590c against a main that had moved to 4a24b96086. Control: Actions was healthy repo-wide at that moment (claude/issue-13464-… ran at 16:03:09, claude/issue-12771-… at 16:07:06), so the silence was branch-specific, not a platform outage. I relayed the conflict rather than pushing to the branch.

The conflict is now resolved — mergeable: True, base 4a24b96086, 7 commits — and workflow runs resumed immediately. That is the hypothesis tested rather than restated: conflict cleared ⇒ runs created.

Results at head 0b71a8a13e

checkverdict
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/6in progress
totals30 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. ⚠️ Not yet complete: shard 1/6 is still running, so this is five of six, and I am not calling it six. The ruling's instruction stands either way — 若 A 落地后仍有红,按实报告不得放松断言.

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

@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

Completing the count I deliberately left open: it is six of six. Head 0b71a8a13e is fully green.

In 5470002104 I reported five shards recovered and said explicitly that I would not call it six while Test Core (1/6) was still running. It has finished:

Test Core 1/6 · 2/6 · 3/6 · 4/6 · 5/6 · 6/6 + rollup✅ all success
Lint & Repo Gates (carries check-system-context-census)✅ success
totals32 success · 1 skipped · 0 red · 0 running

Ruling A's prediction is confirmed in full. All four previously-red signatures recovered — @objectstack/client CRUD, the runtime notification 401s, and the two silenced diagnostic channels — with the ruling's condition never invoked: nothing was relaxed to get here, because nothing needed to be. 若 A 落地后仍有红,按实报告不得放松断言 — there was no red to report.

⇒ And the census repair at 603c9cbb6b is green on the gate itself, which closes the caveat I raised twice while CI was silent.

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 CONTRACT_REVIEW_TIER = 'claude-fable-5' is the gate, and it belongs on the final head.

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

@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

PM review — implementation ACCEPTED. Landing now waits on 条款② contract review, which I have dispatched at CONTRACT_REVIEW_TIER = 'claude-fable-5' against this exact head.

State: head 0b71a8a13e, 35 success · 1 skipped · 0 red, mergeable_state: clean, draft, auto_merge: null. ⛔ This seat does not flip it ready — the ruling files it 条款②档 and green CI plus a PM reading is not the landing authority.

What I verified myself

  • Fences held on the full 23-file diff: no content/docs/releases/, packages/spec/, docs/adr/, .claude/, skills/. ⭐ The docs-drift run named content/docs/releases/v17.mdx as carrying isMissingTableError — the very symbol this PR relocates — and it was correctly left alone. A release-owned page made stale by a code change is filed, not edited.
  • The census gate is green on the gate's own verdict line, not on an exit code, and byte-identical to main's: "109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read." That closes the caveat I raised twice while CI was silent.
  • The CI silence is explained and the explanation is now confirmed from both sides. I measured the conflict at 16:09 with a repo-wide control showing Actions healthy; the implementing seat merged main (27 commits) and CI fired on the very next push. Hypothesis tested, not restated.

⭐ The check that a clean merge makes easiest to skip

main independently added the #11971 grants cache to resolveUserAuthzGrantsthe same function this PR changes — and git merged it without a conflict. That is the "individually fine, jointly wrong" shape, and the seat checked it rather than trusting the clean merge: openUserGrantsCache() and its hit-return sit before any read (459-460), grantsCache.commit(...)after every read (830), so a throw propagates before the commit and an outage can never be negatively cached; the cache is off by default. It also declares plainly that it added no pin for an outage against a cache turned ON, calling that #11971's own TTL/epoch contract. I have asked the contract reviewer to verify this independently — it is the highest-consequence claim in the PR.

Zone 2 — assumption A broke, in three ways worth recording

I assumed a mechanical single-function move. It was not:

  1. Two functions moved — isSchemaAlreadyExistsError came along because both are signatures over one matchesDriverError; splitting them meant exporting the matcher as machinery or re-rolling it in metadata, which is the duplication the ruling rejected as option B. So @objectstack/types gains two public exports.
  2. The whole module and its ~600-line test file moved, as git mv.
  3. metadata's internal file was deleted, not stubbed, so its two in-package readers were edited.

What held: the signature moved byte-identical, the published @objectstack/metadata/errors subpath is unchanged, and assumption B held more strongly than the ruling's parenthetical — the move removes a dependency edge rather than adding one, because the classifier already imported isRelationSubObjectPhrase from @objectstack/types. All three points are in the reviewer's brief; a widened public surface is exactly what limb 1 of clause ② exists to judge, and it is the reviewer's call, not mine.

The signed-off risk is enforced, not merely written

The note sits in tryFind's catch immediately above the predicate, quotes both the 裁定 and the 签字在案 paragraph, and a test reads that catch body back and fails if the note is removed. Both directions are pinned by name — the outage direction and the unprovisioned direction — and the ablation shows they are independent: the six unprovisioned pins fell while every outage pin stayed green.

The four reds, per the ruling's condition

All four recovered, in CI and locally: @objectstack/client CRUD/batch, packages/runtime notifications (measured at package level on purpose, since the notifications file alone holds 5 tests and could not have accounted for 11 failures), and both integration noise guards. ⛔ No assertion was relaxed, re-baselined, allow-listed, skipped or deleted. 若 A 落地后仍有红 never triggered — there was no red.

Routed

#13516 (three stale comment pointers in driver-sql ×2 and service-analytics) → pm:retriage. #13490 reproduced a second time on this merge, and this occurrence is sharper than the first: the false "POPULATION changed" refusal hid a real trap, because the correct target :1267 was simultaneously another anchor's new home — pure numeric shifting would have produced a page pointing one row at the wrong line. That is evidence about #13490's priority and I have added it there rather than re-filing.


Generated by Claude Code

@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

条款② contract review — PASS WITH NOTES

Run at CONTRACT_REVIEW_TIER = 'claude-fable-5' against head 0b71a8a13e. Both limbs pass; two findings must be resolved before landing. ⛔ Still draft, still unenqueued.

Limb 1 — public surface: correct and minimal

The widening is forced, not opportunistic, and the reviewer measured it rather than accepting the framing: git diff -M shows a genuine rename (R086 module / R098 test), and a code-only comparison of old vs new module shows exactly one changed code line — the isRelationSubObjectPhrase import flipping to a sibling. Everything else is comments, so the verdicts are byte-identical to what @objectstack/metadata shipped, the #13324readObject narrowing included. The sibling export was unavoidable: @objectstack/types publishes only . and ./node, and after the internal module's deletion metadata/src/loaders/database-loader.ts is an out-of-package consumer of isSchemaAlreadyExistsError — the only alternatives were a stub that still needs the sibling public, or re-rolling the matcher, which is the option-B duplication the ruling rejected.

⚠️ One precision correction to the record, worth keeping: the "removes a dependency edge" claim is true at the module-import level, not the manifest level — the packages/*/package.json diff is empty, so the package graph is unchanged in both directions. Nobody should read it as a manifest change.

Limb 2 — contract behaviour: as ruled, pinned in both directions

SERVICE_UNAVAILABLE pre-exists at base (packages/spec/src/api/errors.zod.ts:113, 503 mapping at :184) and packages/spec is untouched — measured, empty diff. No envelope gains or loses a key.

⭐ The reviewer ran both ablation directions, including the inverse one the implementing seat did not:

ablationresult
narrowing line → marker (seat's)7 failed / 30 passed — six unprovisioned pins + the risk-note pin fell; every outage pin stayed green
throw → the old return [] (new)10 failed / 27 passed — every loud pin fell; every quiet pin stayed green

⇒ 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.openUserGrantsCache at 459 with its hit-return at 460, before any read; no catch of any kind between 460 and the single commit( at 830 (grep over the range; the one hit is a comment). An outage rejects the whole resolution before the commit is reachable, so it can never be negatively cached. Default-off confirmed. ⚠️ Unpinned by anyone and declared by both parties: an outage against a cache turned ON — that is #11971's contract, not this PR's.

⚠️ The two findings — relayed to the implementing seat

  1. Four surviving swallow sites in rest-server.ts2746-47, 4387-88, 5225-26, 6758-59. Each is resolveExecCtx(...) with .catch(() => undefined) on the continuation line: exactly the trap the census file's own pre-existing docblock names — "a single-line grep counts 16 and misses those four." 16 were converted. Reachability verified statically for the :type door: the swallowing site is that handler's first resolver touch, so during a store outage those doors serve an org-unscoped, env-wide 200 instead of a 503.
    PM ruling: convert them. Same 3-line shape, applied 16 times already. This is not widening — the ruling's all-transport clause is what an unconverted site violates. Bounded severity (no capability bypass, no 403/401 disguise, identical to base) is why it is fix-before-landing rather than stop-and-report.

  2. A false completeness claim shipped in this PR — the new [#13279] block in execctx-consumer-census.test.ts states "every site now passes rethrowAuthzStoreUnavailable". Four do not. ⭐ This is the sharper half: on a surface whose whole methodology is measured claims, a false sentence in a census file is worse than the sites themselves, because it is the artefact a future reader trusts instead of re-measuring. And the ledger cannot catch it — its per-file check is presence-based, so no pin fails while those four exist.

Also corrected for the record: the body's "18 nets" is a +-line count (16 swaps + the computeExecCtx catch + the import) — the very single-line counting the census warns against, and consistent with how the four were missed. And @objectstack/core's root surface gains six exports via security/index.ts, not the two the body implies; limb 1 passes regardless, but the number should be stated correctly.

Process

The fix moves the head, which voids this review as it stands. Rather than a cold re-run I will resume the same reviewer for a delta review on the new head — the change is four mechanical call sites plus comment text and touches no contract surface. ⛔ The PR stays draft until that delta returns.


Generated by Claude Code

…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
@zhuangjianguoClaude

Copy link
Copy Markdown
Collaborator

条款② delta review — PASS, now unconditional on cfe54c80d084

Same reviewer, same tier, resumed against the new head rather than re-run cold — the delta is one commit and two packages/rest files and touches no contract surface. Both notes from the first pass are discharged.

The four conversions — and the half my own check could not see

Each of the four sites is a one-token swap of the catch argument to rethrowAuthzStoreUnavailable, byte-for-byte the same form as the 16 inline sites, with nothing else touched in any hunk; every non-branded fault still returns undefined, so no other fault's behaviour moved.

⭐ The reviewer then verified what I had not: that the re-raise actually escapes at all four sites rather than dying in an enclosing catch. Sites 2 and 3 terminate in handleRouteError (:4778, :5583); site 1's helper is awaited inside two handlers whose enclosing catches both call it; site 4 sits inside the #5532 overlay catch, which re-throws any error carrying a numeric status — and AuthzStoreUnavailableError.status = 503 qualifies. All four doors answer the declared 503 during an outage. "Converted" and "reaches the renderer" are different claims, and only the second one matters.

§7 discriminates — ablated twice, including the evasion I asked about

I flagged §7 as new test logic on the exact surface under review and said that if it could not catch the shape it was written for, that would be a BLOCK, not a note. It catches it:

ablationresult
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 caughtred 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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@zhuangjianguo@claude