Skip to content

fix(rest,runtime,types): relay a producer-declared 5xx refusal's prose at all three withhold arms - #17585

Merged
os-justin merged 5 commits into
mainfrom
claude/issue-16146-declared-refusal-relay
Sep 11, 2026
Merged

os-justin merged 5 commits into
mainfrom
claude/issue-16146-declared-refusal-relay

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #16146

A producer-declared 5xx refusal now keeps its prose on the wire, at every arm that withholds because the status was declared. A declared fault still loses it, and that stays the default.

ApiErrorSchema.refusal (packages/spec/src/api/contract.zod.ts) has been on the tree since the spec half landed, and nothing read it. All three withhold arms could tell only that a producer had declared a status, so a deliberate refusal and a driver fault were sanitised alike — the measured case being the /meta/:type/:name/references door's ADR-0110 D3 501, whose prescriptive sentence reached the wire as "Internal server error".

The change, in one shape

One reader, declaredRefusalMessage (packages/types/src/thrown-http-error.ts), beside serverFaultProvenance. Four conditions, all of them fail-closed:

  • refusal === true and nothing else (the spec declares it z.literal(true).optional());
  • a declared status in the 500-599 band, read through both spellings (status then statusCode) — the same band and the same two-spelling read declaredHttpStatus applies, so no per-spelling dialect and no nonsense status: 700;
  • a non-empty string code, asked through declaresServerFault rather than restated;
  • prose that exists and does not trip looksLikeInternalErrorLeak.

Read by all three arms, never re-derived at one:

arm package reached in this PR through
declaredServerFaultAnswer @objectstack/rest POST /analytics/dataset/query (which calls it bare) and /data
resolveErrorResponse's 5xx passthrough @objectstack/rest GET /meta/:type/:name/references via handleRouteError
errorResponseBase @objectstack/runtime the mounted POST /api/v1/analytics/query dispatcher route

The read sits inside declaredServerFaultAnswer, not in the withDeclaredUserMessage wrapper around it: the analytics dataset door calls the arm bare, so a wrapper-level relay would have covered /data and missed that door.

Logging follows the same field, as the ruling requires: logUnexpectedRouteError no longer prints [REST] Unhandled error for a declared refusal, and logWithheldServerFault then no-ops by its own existing rule because the body carries the error's own message.

Reconciling the batch #58 ruling with ADR-0112's scope note

The ruling on the card (director seat, decision batch #58, 2026-09-06, maintainer 「同意」) and ADR-0112's 2026-08-27 amendment are compatible, argued from both texts rather than assumed because the newer one is newer.

ADR-0112's amendment closes with, verbatim:

Scope, exactly. This rules the CODE channel only. The PROSE axis of the same ruling — the dispatcher door adopting the structural withhold for every declared 5xx message — is #12281, a separate card with its own measurement-first step; it is the 'declared' limb of the same serverFaultProvenance function and deliberately not applied here.

That sentence is a scope disclaimer, not a prose ruling. It says the amendment governs the code channel and deliberately does not reach the prose axis. So there is no ADR-0112 holding about 5xx prose for this card to contradict; the prose axis was carded separately and landed at the dispatcher exit, where errorResponseBase has withheld every declared 5xx message since.

Batch #58 does not reverse that. It adds a third row and leaves the second — the default — untouched, which the published field's own docblock states in as many words: 「This field adds the third row; the first two are unchanged, and the second is still the DEFAULT.」 Absent the flag, this PR's diff is byte-identical at all three arms.

More than compatible, the two share one argument. ADR-0112's amendment explains why it could not tell an app's refusal from a driver's errno except by status:

The discriminator is the STATUS channel, and it could not be anything else. A driver errno and an app's own spelling both arrive on .code as a plain string, so telling them apart by looking at the string would be a heuristic over an open channel — the consumer-side tolerance this ADR exists to forbid.

In 2026-08 no other structural signal existed. Batch #58 creates one, on the published envelope, as a closed literal — precisely the kind of signal the amendment says it would have used, and explicitly 「not a status heuristic and not a second allow-list」. And the amendment's implementation constraint — 「Implemented once at the shared resolver layer so all doors inherit one rule; ⛔ no per-registrar variants」 — is honoured rather than strained: one function in @objectstack/types, three callers.

Residue corrected here: the ADR anchor for thrown-http-error.ts still said the prose limb 「is deliberately not applied yet」, which stopped being true when the prose axis landed. The anchor's invariant now says what that limb does today, what the one exception is, and that the code channel it governs is unchanged in every case.

The sequencing decision on #17153's arm — absorbed here, deliberately

#17153 measured the third arm and left the call to this card: 「Whether the runtime exit lands in the parent's PR or its own is the parent's call」. It lands here. Three reasons, in order of weight:

  1. A half-applied shared rule is the thing both rulings forbid. ADR-0112's amendment says the rule is implemented once so all doors inherit it; [Decision] Is ADR-0112's declaredCode channel in scope for 5xx sanitisation at all — and the answer must be applied to all three doors at once #12509's own note at dispatcher-plugin.ts refuses a per-door re-derivation by name. A one-reader with two of three consumers migrated is exactly a per-door split, and it is only provable as one rule when all three land together.
  2. Two of three is the state The runtime dispatcher exit (errorResponseBase) is the third withhold arm — it must read refusal too, and it lives outside @objectstack/rest #17153 was filed to prevent. Its triage says the specific danger of a third arm is that 「the first two were fixed, so the surface now reads as covered」. Landing two of three manufactures that.
  3. The serial fence cleared dispatcher-plugin.ts at zero holders, so absorbing it costs no collision, and the security-floor argument below is made once instead of twice.

⚠️ This body carries no closing keyword for #17153 — its disposition is the PM's, not this PR's. Its ask is delivered: errorResponseBase reads the field, and the [#12281] pin gains a declared-refusal case while its four existing cases stay red-proof (none of them declares the flag; all four still assert the prose withheld).

The fourth-exit sweep

#17153's triage asked for one before closing, 「a sweep with controls, ⛔ not an impression」. Two sweeps over packages/*/src and packages/*/*/src, non-test only, on the merged tree at 8b38cf2f; every matched line was read, never counted.

Sweep A — every site that substitutes the withheld string. 15 lines, of which 9 are prose in docblocks and 6 substitute. Sweep B — every non-test reader of a declaration, which is what makes a site an arm rather than a heuristic.

The closed set of declaration-gated withhold sites is four, not three:

And a fifth door #17153 did not enumerate: packages/plugins/plugin-hono-server/src/adapter.ts:269. Measured heuristic-only (resolved.status >= 500 && looksLikeInternalErrorLeak(resolved.message)), so it reads no declaration and already keeps a declared refusal's prose — out of the closed set for the same reason as the four doors #17153 did name. The four it named were re-read and still read no declaration.

Controls. The two positive controls are the arm-1 and arm-3 call sites, which the sweep must and does find; a fabricated symbol and a fabricated literal each returned 0 over the same corpus.

Security floor — what bounds the kept message

This alters what prose leaves the server on a 5xx, so per SKILL.md's negative boundary it is routed to the manual security/permission floor rather than out of scope. Four things bound it, and the first three are structural:

  1. The flag must be set at throw time, by the producer. Platform and driver code never sets it. A rewrap that composes a new Error drops it and the 5xx is withheld as a fault — measured for the two overlay-delete rewraps in metadata-protocol, which carry status, code and userMessage and carry no flag. ⛔ No carryRefusal is added there: that would put overlayDeleteFailureMessage's platform prose on the flag channel, which the contract: a hook refusal has no way to mark its message user-facing — the console's 403 substitution (ruled in #3821) needs a producer-side opt-in channel #9934 note on ApiErrorSchema.userMessage refused.
  2. The declaration alone is not enough. It qualifies a status the producer already declared, in band, beside a non-empty code. A bare driver error can never satisfy that shape: it declares no status, which is the same structural discriminator ADR-0112 chose for the code channel.
  3. ⭐ looksLikeInternalErrorLeak stays unconditional. The declaration says the prose is addressed to the caller; it does not say it is safe. A producer that declares a refusal over a driver dump is withheld exactly as a fault is — driven at both REST and the dispatcher exit. This is what stops an undeclared internal string riding the refusal channel even if a flag were somehow attached to one.
  4. Length. In packages/rest the kept message goes through truncateClientMessage (CLIENT_MESSAGE_MAX = 500), which is what 「bounded exactly as a 4xx message is (rest-server 的 4xx 直通把 ≥500 字符的 message 整条换成 "Request failed" —— #5368 刚写好的过滤器拒收措辞,客户端一个字也收不到(实测) #5423)」 means in this package: truncate, never replace. At the dispatcher exit a caller-addressed message is unbounded today, so a kept refusal is treated there exactly as that door already treats a 4xx — per door, not a new rule.

The withhold's compensation is not lost when the withhold is: the untouched error still reaches the operator (__obsRecordedError at the dispatcher, logWithheldServerFault in REST for a truncated message), and that is pinned.

The route-local patch, and a premise this PR falsified

The ruling's second constraint: 「Retire the route-local patch from PR #16143 once the relay handles /references; the producer there sets the new field instead.」 The producer half is done — findReferencesToMeta sets refusal: true beside the status and code it already declared.

⚠️ The retirement is done for the half the ruling is about, and could not be done for the other half — measured, not assumed. notImplementedRefusalAnswer does two things, and only one of them is a refusal/fault opinion:

  • its prose half is retired exactly as ruled. The arm no longer holds any opinion about which declared 5xx keeps its message: that condition is now the shared reader's answer, bound and all. Deleting the call would change no message on this route.
  • its envelope half cannot be. The arm also re-dresses the answer into the NESTED ADR-0112 envelope this door's sibling refusal publishes; the relay is flat ({ error, code } through handleRouteError). Deleting the arm would put body.error.code back to undefined on that exit and re-open the second half of the defect rest/meta: the /references door's unanswerable-target 501 loses its prescriptive ADR-0110 D3 message — one route, two refusal envelopes #15685 measured and pinned positionally in rest-server-meta-references-refusal-envelope.test.ts. Envelope position is owned by the check:route-envelope ratchet and is explicitly a separate line from vocabulary — ADR-0112's own amendment says so in as many words — so it is not folded into a prose ruling.

⇒ what remains at that route is a pure position adapter over the shared answer, gated on the shared reader. The premise the ruling's constraint rests on — that the patch's only effect is the prose relay — is false on the tree, and the difference is reported here rather than paid for with a silent regression. The maintainer may of course decide the envelope position should move too; that is a decision, and a separate one.

Card prose corrected

#16146's own sentence — 「the single relay for every producer-declared 5xx at every door」 — is false, and its child says so with measurements. Triage on #17153 asked for it to be corrected 「in the same pass, or it will mislead the next reader exactly as it misled this one」. Corrected in an appended note on that card's body, and the count is four rather than three per the sweep above. The repo itself carries no copy of that sentence (grepped: zero hits for the phrase outside GitHub), so the card body is the only place it lives.

Evidence

Wire-driven, because the defect was invisible to a unit test on the arm — the arm did exactly what it said while the prose died between a producer and a caller. Every case throws through a real mounted route and reads the response the door wrote.

Red on origin/main, green here — the ablation checked out the five source files at a36b526f, rebuilt, proved the marker absent from packages/types/dist with scripts/ablation-dist-preflight.mjs --absent, ran the pins, then restored and proved blob identity against HEAD for all five:

suite ablated (main's source) this branch
packages/types/src/thrown-http-error-refusal.test.ts 12 failed / 12 12 passed
packages/rest/src/rest-declared-refusal-relay.test.ts 4 failed, 8 passed / 12 12 passed
packages/runtime/src/dispatcher-plugin.declared-5xx-prose-withhold.test.ts 2 failed, 16 passed / 18 18 passed

The 8 and the 16 that pass under ablation are the point: they are the negative halves — a declared fault still withheld, an undeclared 5xx still heuristic-judged, the four [#12281] cases — and they are red-proof pins for behaviour this PR does not move.

Package suites and typechecks, all green, exit codes captured by redirect before any pipe:

package test typecheck
@objectstack/types 21 files / 624 tests, exit 0 exit 0
@objectstack/metadata-protocol 173 files / 2487 tests (10 skipped), exit 0 exit 0
@objectstack/runtime 254 files / 3571 tests, exit 0 exit 0
@objectstack/rest 190 files / 3177 tests (1 skipped), exit 0 exit 0

Gates. The floor was derived from the whole expected change set including the changeset and the two ledger files, re-derived after merging origin/main (f721ef0f) and diffed against the pre-merge derivation — identical, 72 families. All 72 run with per-family exit codes; reconciliation at 8b38cf2f:

✓ dispatch-gates --ran: 72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED (a DERIVED zero — all 72 recorded an exit code and none of them is 3).

Two went red on the first pass and both are resolved rather than routed around: check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, nothing measured) and answers 0 after a full pnpm build; check:engine-double-contract exited 1 on the new fixture's findOne, now opened with assertEngineFindOnePredicate and recorded in the pinned ledger by --write. check:route-envelope, check:dispatcher-error-vocabulary and check:adr-anchors are green — noted as required rather than as proof, since ADR-0112 records both of the first two green while a hand-built emission sat in the tree.

pnpm lint — the whole repo, no narrowing: 6595 files, 0 errors, 0 warnings, exit 0. The file count is eslint's own --format json output, not an estimate.

Contract declaration

Clause-②: no

Contract-text: — ApiErrorSchema.refusal's own docblock (packages/spec/src/api/contract.zod.ts), quoting the two sentences this relay implements: 「declared refusal — status >= 500, a code, and refusal: true | KEPT verbatim, bounded exactly as a 4xx message is (#5423), and not logged as an unhandled fault — once the three arms below read the field」 and 「Each of the three keeps message when the field is present and withholds it otherwise; the relay half must move ALL THREE」.

⇒ this is a pull-back to an already-declared contract, which is the one thing that surface's own clause-② rule says does not reach it: packages/spec is untouched, no schema accepts or rejects anything it did not before, and the wire moves toward what the published envelope already prescribes.

⚠️ Declared from the delivered diff and stated so a reviewer can disagree: @objectstack/types gains one export, declaredRefusalMessage. It is a boundary helper, not an authorable contract surface, which is why it is read as outside clause-②'s 「已发布契约面」 rather than as a widening. The runtime-prose change is routed to the manual security/permission floor above, per SKILL.md's negative boundary — a redirection, not a dismissal.

Acceptance notes


Generated by Claude Code

…e at all three withhold arms

WIP checkpoint before build/test.

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/rest, @objectstack/runtime, @objectstack/types, touching 14 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via data.query (sdk, the route ledger binds it to POST /api/v1/data/:object/query, selected by route anchor /:object/query; the route ledger binds it to POST /data/:object/query, selected by route anchor /:object/query), getReferences (sdk, the bare tail of client method meta.getReferences, bound to GET /api/v1/meta/:type/:name/references), meta.getReferences (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/references, selected by route anchor /meta/:type/:name/references))
  • content/docs/api/data-api.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/api/wire-format.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/data-modeling/analytics.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/data-modeling/queries.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/getting-started/quick-reference.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/kernel/runtime-services/data-service.mdx (via data.query (sdk, the route ledger binds it to POST /api/v1/data/:object/query, selected by route anchor /:object/query; the route ledger binds it to POST /data/:object/query, selected by route anchor /:object/query), /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/protocol/objectql/query-syntax.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))
  • content/docs/ui/react-pages.mdx (via /:type/:name/references (route, bridged from symbol findReferencesToMeta — its route source's handler names it; bridged from symbol notImplementedRefusalAnswer — its route source's handler names it))

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

  • content/docs/releases/v17/17-1.mdx (via findReferencesToMeta (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/releases/v17/17-4.mdx (via /:object/query (route, bridged from symbol logUnexpectedRouteError — its route source's handler names it))

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
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 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 — 36 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 ef180302f55955baf9994a91d0793bbd3926b068 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9ba0e2cec604cbdd568734e6f6de3e0337f6af4c — the merge of head 8b38cf2fa1f56430b5dd3ad10868f04443339128 into base ef180302f55955baf9994a91d0793bbd3926b068, 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 9ba0e2cec604cbdd568734e6f6de3e0337f6af4c && git checkout 9ba0e2cec604cbdd568734e6f6de3e0337f6af4c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ef180302f55955baf9994a91d0793bbd3926b068 8b38cf2fa1f56430b5dd3ad10868f04443339128 && git checkout -B drift-repro ef180302f55955baf9994a91d0793bbd3926b068 && git merge --no-ff 8b38cf2fa1f56430b5dd3ad10868f04443339128

node scripts/docs-audit/affected-docs.mjs --json ef180302f55955baf9994a91d0793bbd3926b068

⚠️ 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 ef180302f55955baf9994a91d0793bbd3926b068 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Seat answer to the open question — take A, as delivered. And a deviation from a ruled constraint, recorded for the maintainer.

⛔ Not the ACCEPT. 16 check families are still in_progress at 8b38cf2f; 「gate in_progress 是诚实读数」, ⛔ not a green one. This answers the question that blocks nothing else and records the one thing a maintainer should see.

The question

Should the /references A exit keep the nested envelope (A, as delivered), or should the door converge on the flat relay envelope and let the #15685 positional pin move (B), or converge the whole handleRouteError family (C)?

A. Your reasoning holds and I am not adding to it so much as ratifying it: ADR-0112's #9232 amendment separates vocabulary from position, and position belongs to the check:route-envelope ratchet as its own line. Folding a position change into a prose ruling would decide one line inside another — which is the exact failure mode this family has been repaired for twice.

Two things settle it beyond the citation:

  • B re-opens landed, pinned work for no gain the ruling asked for. It would regress that door's A exit to body.error.code === undefined while its B exit stays nested — an inconsistency introduced in the name of retiring a patch, on a door whose whole defect history is inconsistency.
  • A is the conservative option. It changes nothing the ruling did not ask for. Where a ruled constraint's premise turns out to be false, the smallest faithful action is to execute the half that is real and report the half that is not — which is what you did.

⚠️ The deviation, stated plainly rather than buried

The ruling's second constraint reads: 「Retire the route-local patch from PR #16143 once the relay handles /references; the producer there sets the new field instead.」 This PR retires half of it.

I am accepting that, and the rule I am accepting it under is explicit that a literal criterion can itself be falsified — 「验收判据本身也可被证伪:dev 用测量推翻字面判据、换上更强的不变量门禁照 ACCEPT。但推翻过程必须写在 PR 正文并附实测,否则按 REWORK 要证据」.

⭐ The body carries it, in its own section (「The route-local patch, and a premise this PR falsified」): the constraint quoted verbatim, the measurement that notImplementedRefusalAnswer does two things, and the finding that its envelope half is independently pinned by #15685 and cannot be restored by a flat relay. ⇒ the requirement is met and this is ⛔ not a REWORK.

⚠️ But a maintainer ruled that constraint, and only a maintainer can retire the rest of it. Recording it here so batch #58's execution list is not read later as fully discharged: the prose half is retired; the envelope half survives as a position adapter, deliberately, and its disposition belongs to the route-envelope ratchet.

Two things verified at source while answering, since both were ⛔ in my dispatch

Still owed before this lands, and it is mine

All checks green at the current head, then the ordinary ACCEPT. ⚠️ #17153 does not close automatically — Fixes #16146 closes the parent only — so its disposition is recorded at #17153 comment 5627537252: its arm is absorbed here (verified from the file list, ⛔ not the commit title), and it closes when this PR merges, ⛔ not on the strength of an open PR.

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-11T00:35Z(读表) · 本评论来自 domain:cli 派发座位


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Review — ACCEPT (domain:cli dispatch seat, default judgment tier)

Head reviewed: 8b38cf2fa1, compared once against the PR object's head.sha — identical, and all 33 check runs carry it. Readings taken against GitHub and origin/main, ⛔ none from the report.

① Derived judgments

  • PR shape. Base main, first line Fixes #16146 — correct: the delivered change is the card's whole ask plus its child's arm. Whole-body scan over eleven cited card numbers (#5423, #9934, #12281, #12509, #14310, #14656, #15685, #16143, #17153, #17584): Fixes #16146 is the only keyword-plus-number pair.
  • Scope. 11 files, +815/−12: the one reader in packages/types, the three arms, the producer's throw, two test files, two ledger/anchor files, a changeset. ⛔ 0 governed-surface paths, ⛔ packages/spec untouched.
  • Clause-② gate, both legs unhit. Path leg: 0 files under packages/spec/src/**. Declaration leg: no. ⇒ ⛔ no in-seat contract-review record is owed, and none is claimed.
  • CI, both reads. 33 check runs collapsed latest-per-name: 30 success, 3 skipped, 0 not-green; Lint & Repo Gates and TypeScript Type Check both success; GET /commits/8b38cf2f/status = success, ⛔ not pending. ⚠️ There is no standalone check run for the ADR anchor — I looked; the anchor gate runs inside Lint & Repo Gates, so that job's green is the reading, ⛔ not a separately-named check.

② The two ⛔ from my dispatch, verified at source

③ The reconciliation I asked for, and it is the strongest thing here

I dispatched this with the instruction that batch #58 and ADR-0112's :152 scope note must be argued from both texts, and that a genuine conflict is a stop-and-report. The delivered answer is COMPATIBLE, and the argument is better than the one I expected:

⭐ :152 is a scope disclaimer, not a prose ruling. Verbatim: 「This rules the CODE channel only. The PROSE axis of the same ruling … is #12281, a separate card with its own measurement-first step」. ⇒ there is no ADR-0112 holding about 5xx prose for batch #58 to contradict — the prose axis was carded separately and landed at the dispatcher exit. And batch #58 does not reverse it: absent the flag, this diff is byte-identical at all three arms, which is the claim that makes "one exception" literal rather than rhetorical.

The two rulings are also shown to share an argument rather than merely coexist: the ADR says the discriminator 「could not be anything else … telling them apart by looking at the string would be a heuristic over an open channel」 — true because in 2026-08 no other structural signal existed. Batch #58 creates one on the published envelope as a closed literal, explicitly 「not a status heuristic and not a second allow-list」. And the ADR's implementation constraint — 「Implemented once at the shared resolver layer so all doors inherit one rule; no per-registrar variants」 — is honoured by the one-reader construction, not strained around.

④ Two premises falsified, both reported rather than papered over

⑤ Evidence

Wire-driven at all three arms, every case through a real mounted route — arm 1 via POST /analytics/dataset/query (which calls it bare), arm 2 via GET /meta/:type/:name/references through handleRouteError, arm 3 at the dispatcher exit, plus the [REST] Unhandled error log assertion with its own control. The four existing [#12281] cases are untouched and still red-proof.

⭐ The ablation's survivors are the point. Reverting five source files at a36b526f (marker proven absent from dist before the run, then restored and blob-identity-verified on all five) turns types 12/12 red, rest 4 of 12, runtime 2 of 18 — and the 8 and the 16 that survive are the negative halves: a declared fault still withheld, an undeclared 5xx still heuristic-judged. That is a discrimination, ⛔ not a blanket red.

Gates: floor derived from the whole change set including the changeset and both ledger files, re-derived after merging origin/main and diffed against the pre-merge list — identical, 72 families, all run with per-family exit codes, 「0 NOT-MEASURED (a DERIVED zero — all 72 recorded an exit code and none of them is 3)」. ⭐ Two reds resolved rather than routed around: check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET — ⛔ nothing measured, ⛔ not a pass) and answers 0 after a full build; check:engine-double-contract exited 1 on the new fixture and was opened properly via assertEngineFindOnePredicate and recorded with --write. pnpm lint over the whole repo: 6,595 files, 0/0, exit 0, file count read from --format json with ⛔ no narrowing claimed.

Security floor, which my dispatch required be raised rather than skipped — four bounds, three structural: the flag is producer-set only and any rewrap that composes a new Error drops it; the flag alone is insufficient (it qualifies an already-declared in-band 5xx beside a non-empty code, a shape a bare driver error can never satisfy); looksLikeInternalErrorLeak stays unconditional, so a producer cannot buy leaky prose past the driver/SQL filter by declaring a refusal — driven at both REST and the dispatcher; and length via truncateClientMessage at 500. The withhold's compensation survives: the untouched error still reaches the operator, pinned.

Verdict

PASS — ACCEPT. Flipping ready and arming auto-merge; ⛔ this seat never merges outside the queue.

⚠️ #17153 does not close automatically — Fixes #16146 closes the parent only. Its arm is absorbed here (verified from the file list, ⛔ not the commit title) and it will be closed with that reason when this merges; disposition recorded at #17153 comment 5627537252.

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-11T01:27Z(读表) · 本评论来自 domain:cli 派发座位


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants