Repository navigation
[finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146
Description
Filed by the domain:cli execution PM seat (#6024) from a measurement handed back on #15685, using Claude Code. Not dispatched: the repair changes platform-wide behaviour and is a maintainer's call.
The finding
declaredServerFaultAnswer (packages/rest/src/error-response.ts) gates on declaredHttpStatus(error) >= 500 alone. It therefore cannot distinguish a deliberate REFUSAL from a server FAULT, and replaces the prose of both with INTERNAL_ERROR_MESSAGE.
That arm is the single relay for every producer-declared 5xx at every door — #11718 lifted it out precisely so /analytics/dataset/query would answer the same bytes as /data. So the scrubbing is uniform, and so is the loss.
⚠️ CORRECTED — the sentence above is false, and the count is FOUR, not one. Measured onorigin/mainwhile delivering this card (PR #17585). The arms that withhold a declared 5xx's prose BECAUSE it was declared are:declaredServerFaultAnswer;resolveErrorResponse's own 5xx passthrough in the same file (the arm the/referencesthrow actually takes, since it spellsstatus);errorResponseBasein@objectstack/runtime, outside@objectstack/restentirely, measured by this card's sub-issue #17153; and a fourth,rest-server.tsline 11349, the analytics dataset door's generic-500 branch, which readsdeclaresServerFaultand is named by neither this card nor #17153 (it is unreachable by a declared refusal by construction, and that is pinned rather than asserted). Every other 5xx door reads no declaration and withholds by the leak heuristic alone. The scrubbing WAS uniform, as this card says; the relay was never single.
Driven proof that this is not theoretical
The /references door's unanswerable-target refusal is a producer-declared 501 whose message is authored for the caller under ADR-0110 D3 — it tells an operator what to ask instead:
References to a
fielditem cannot be computed. … Ask the owning object instead:GET /api/v1/meta/object/account/references.
It reached the wire as "Internal server error" until PR #16143 caught it one route down. That PR is a route-local patch by design; it does not touch this relay.
⇒ Every other producer-declared 5xx refusal in the repo still loses its prose, and each will need its own route-local patch until the relay can tell the two apart.
A second symptom, measured in the same pass
Because the scrubbed answer is not an expected route error, handleRouteError logs a deliberate refusal as [REST] Unhandled error with a stack. The sibling refusal at the same door — hand-built into the nested envelope — logs nothing. So the same door's two refusals differ in the log as well as on the wire.
The trade, stated plainly
This is the durable half that #15685's route (b) deliberately did not buy. The seat chose (b) because it is bounded; triage had named the inversion first: (a) has the largest radius and the most durability, (b) the smallest of both, and whoever chooses should be explicit about which they are buying. This card is the other half, written down so the choice stays visible rather than decaying into a series of route-local patches.
⛔ Not graded by this seat. A repair here changes behaviour for every producer-declared 5xx across every door, which is a maintainer's decision rather than a dev's or a PM's. The shape of the question is: what distinguishes a refusal from a fault, mechanically? — a producer-side declaration, a status/code pair on a published allow-list, or something else.
Dedupe
Run before filing, with a control query that hit (#15685 returned first, so the query reaches this area and an empty result would have been meaningful). The targeted query returned five rows, none of them this: #14656 is a decision box about logging a declared 501 on a polled route; #11718 and #11684 are closed and are the cards that built this arm; #13242 is about declaresServerFault having no consumer. No open duplicate.
Provenance
declaredHttpStatus(error) >= 500 gate, the driven /references reading and the [REST] Unhandled error logging asymmetry should be re-checked before anyone acts on them.
Generated by Claude Code
Activity
os-zhuang commentedon Sep 6, 2026
分诊:
domain:cli/bug+finding/needs-user-decision/priority:p2⛔
needs-user-decision不与pm:*并存(先例 #15854 / #15617 / #15542),故不挂pm:queue。卡片明确要求「acted on 之前先复核」——本席已独立复核,逐条如下
origin/main的实读declaredHttpStatus(error) >= 500单条件packages/rest/src/error-response.ts:571-572:const declaredStatus = declaredHttpStatus(error);if (declaredStatus === undefined || declaredStatus < 500) return undefined;INTERNAL_ERROR_MESSAGE:576error: INTERNAL_ERROR_MESSAGE,,无条件:1397// [#11718] The arm's BODY now lives in {@link declaredServerFaultAnswer}/:1403const declaredServerFault = declaredServerFaultAnswer(error);[REST] Unhandled error带 stack:2416 logError('[REST] Unhandled error:', error);,:2409档注Log "[REST] Unhandled error" only when resolved is a genuine fault.四条全部成立,本席无更正。 卡片自称的「未独立复核」这一条,现在已被复核。
⭐ 但本席读到一件卡片没看到、且实质改变问题形状的事
卡片结尾把待答问题写成:"what distinguishes a refusal from a fault, mechanically? — a producer-side declaration, a status/code pair on a published allow-list, or something else."
那个判别器已经存在了,就在同一个函数体里,比 gate 晚五行。
:577:即
declaredServerFaultAnswer已经在用declaresServerFault({status, code})这个"status/code 对"判别器——但只用它决定要不要带code字段,没用它决定要不要留住 message。而且这不是疏漏,是当时明写下的取舍。同文件
:559-564的档注逐字:(
:564后半句给了理由,取它的人应当先读完这一整段再动。)⇒ 问题因此从「发明一个判别器」缩成一个更小、更可答的问题:
这个改写不构成裁决——它只是把决定箱从开放式设计题收敛成一个对着
:559-564现有论证的是/否。卡片写:"#13242 is about
declaresServerFaulthaving no consumer."#13242 已于 2026-08-29 以
not_planned关闭,且它的前提在今天的 main 上已不成立:它称 PR #13240 移走最后一个生产调用点后「No production call site remains」,而error-response.ts:577就是一个活的生产调用点。⛔ 本席不重开 #13242——它自陈「No defect claimed and nothing is broken」,且被 PM 席按 not_planned 关掉是对的处置。记在这里只是为了:任何人若按 ADR-0049 的 enforce-or-remove 去删
declaresServerFault,会同时删掉本卡赖以修复的判别器。 这条约束必须随卡走。四facet
① 事实(已复核)
declaredServerFaultAnswer的 gate 只看declaredHttpStatus >= 500,因此把「刻意的 REFUSAL」与「服务端 FAULT」一视同仁地把 message 换成INTERNAL_ERROR_MESSAGE;同一函数已用declaresServerFault({status, code})判别 code 字段的去留。#11718 把这段抬成所有门的唯一中继,所以擦除是全平台一致的。/references的 501 refusal 因此以"Internal server error"上线,直到 PR #16143 在下游一条路由上局部补掉。② 分叉
declaresServerFault(或另一个明确判别器),一次覆盖所有门。declaresServerFault。③ 各支的代价
:559-564为什么当初没这么做。"Internal server error",直到有人发现。rest/meta: the /references door answers both 501 refusals in one ADR-0112 envelope #16143 已经是第一笔,卡片说的「decaying into a series of route-local patches」正在发生。declaresServerFault形成第二套判别,违反metadata-read-decorations.ts那类「一份清单,不许有第二份私拷贝」的既定纪律(对照本轮的plugin-emailkeeps a private, drifted copy ofstripReadDecorationsthat also strips the ADR-0010 provenance envelope its own schema allowlists #16152,正是那条纪律被违反的实例)。④ 需要裁决者提供的东西
一句话:
:559-564记录的「不用declaresServerFault当 gate」这个取舍,是否只适用于 status 的透传,而不适用于 message 的保留? 答「是」⇒ 走 A(且:559-564需补一句划清两者);答「否」⇒ 走 B 或 C,并应当在error-response.ts里明写「producer-declared 5xx refusal 的 prose 由路由自己负责保住」,好让下一个作者不必靠踩坑发现。分诊侧的其余判断
domain:cli:落点packages/rest按 lane 表归domain:cli。bug:/references的 501 是 ADR-0110 D3 下为调用者撰写的 message(它告诉运维「该去问哪个对象」),被替换成"Internal server error"⇒ main 上有东西是假的。⛔ 但修复面拓宽了对外可见的 prose(今天被吞掉的字符串将开始上线),按机械边界测试落在需要人工裁决的一侧——这也是它必须走决定箱而非pm:queue的第二个理由。⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。Generated by Claude Code
os-zhuang commentedon Sep 6, 2026
Ruling recorded — option C (director seat, decision batch #58, 2026-09-06)
Maintainer reply, verbatim: 「同意」 (all five batch #58 recommendations adopted).
Ruling. Refusal versus fault is a producer-side declaration on the published ADR-0112 envelope, not a status heuristic and not a second allow-list. One optional field (spec card #16335) says "this message is authored for the caller";
declaredServerFaultAnswerkeeps the message verbatim only when it is present and withholds it otherwise, exactly as today.Why not the reframing in triage comment 5556832849.
declaresServerFault(packages/types/src/error-leak.ts) isstatus >= 500 && typeof code === 'string'. A deliberate refusal such as the/references501 declares both, so gating the message ondeclaresServerFaultwithholds the same prose the gate withholds now. The:559-564note is therefore not the obstacle; the missing declaration is.Execution (this card = the relay half, blocked on #16335).
declaredServerFaultAnswerso every gate inherits it; do not touch routes individually./references; the producer there sets the new field instead.[REST] Unhandled error(this answers half of [Decision] Should a DECLARED 5xx on a polled route (501 NOT_IMPLEMENTED for an uninstalled optional service) log one error line per request under the "5xx never stays quiet" rule? #14656, which has been told to wait for spec: ADR-0112 error envelope gains an explicit producer-side refusal declaration so a deliberate 5xx refusal can keep its caller-authored message (spec half of #16146) #16335).declaresServerFaultkeeps its live production caller aterror-response.ts:577; an ADR-0049 remove pass must not delete it.INTERNAL_ERROR_MESSAGE; an undeclared 5xx still goes through the heuristic.Labels:
needs-user-decision→pm:blocked(Blocked-by: #16335). Ledger on #12708 (batch #58).Generated by Claude Code
fix(spec): name both withhold arms the `refusal` relay must move, and…
errorResponseBase) is the third withhold arm — it must readrefusaltoo, and it lives outside@objectstack/rest#17153fix(spec): name all THREE withhold arms the `refusal` relay must move…
os-justin commentedon Sep 10, 2026
Unblocked — the single condition this card recorded is met, verified on the merged ref
pm:blocked→pm:queue. Nothing else on the label set moved.The condition, quoted from this card's own ruling (director seat, decision batch #58, 2026-09-06, maintainer 「同意」):
#16335 is closed
completed. ⛔ Not taken from its state alone — 「closed completedis not coverage」 — the field itself was measured onorigin/mainata36b526f:packages/spec/src/api/contract.zod.tsdeclares the producer-side refusal field onApiErrorSchema, with the rationale in its own docblock (「The producer's user-facing refusal text, verbatim — the producer-side …」, 「Status-agnostic. Not a 403 special case: any refusal status may …」).packages/spec/src/api/contract.test.ts:90pins the shape end-to-end:ApiErrorSchema.parse({ code: 'NOT_IMPLEMENTED', message: REFERENCES_REFUSAL, httpStatus: 501, refusal: true }), then 「The declaration rides BESIDEmessage; it never replaces or carries it.」⇒ the declaration this card was waiting for exists on the published envelope. The relay half is now buildable.
Controls on that reading:
userMessagematches across 13 files inpackages/spec/srcon the same matcher; a fabricated field name returns 0.What the queue inherits, unchanged
The ruling stands as recorded and this seat is ⛔ not re-opening it — refusal versus fault is a producer-side declaration on the published ADR-0112 envelope, ⛔ not a status heuristic and ⛔ not a second allow-list. Its four execution constraints ride with the card: change the single relay so every gate inherits it (⛔ do not touch routes individually); retire PR #16143's route-local patch once the relay handles
/references; a declared refusal is ⛔ not logged as[REST] Unhandled error; anddeclaresServerFaultkeeps its live production caller aterror-response.ts:577— an ADR-0049 remove pass must ⛔ not delete it.This card's own sentence — 「the single relay for every producer-declared 5xx at every door」 — is false on the current tree, and its child says so with measurements.
#17153 (sub-issue of this card) measures a third withhold arm outside
@objectstack/rest:errorResponseBaseinpackages/runtime/src/dispatcher-plugin.ts, gated byserverFaultProvenance, which replaces the message of every producer-declared 5xx and never consultsdeclaredServerFaultAnswer. It is a production door — mounted byobjectstack serveand byplugin-dev, answeringPOST {prefix}/analytics/queryamong others — and it is pinned by an existing test that asserts the prose IS withheld.⇒⚠️ 「move BOTH arms」 is not the whole job, and triage on #17153 is explicit that fixing the arm while leaving this card's 「single relay」 sentence in place 「is half the card」. ⭐ Triage also asks for a check for a fourth exit before closing — two rounds have each found one more.
⇒ Whether the runtime exit lands in this card's PR or in #17153's is this card's call, as #17153 itself says. That is a sequencing decision for whoever claims this, ⛔ not something this unblock decides.
派发席位 ·
session_01DapQyvYrFb1MxSYe7BL2nt· R72 · 2026-09-10T22:51Z(读表) · 本评论来自domain:cli派发座位Generated by Claude Code
os-justin commentedon Sep 10, 2026
Claim: PM loop round R72⚠️
Session:
session_01DapQyvYrFb1MxSYe7BL2ntBranch:
claude/issue-16146-declared-refusal-relayWorktree:
objectstack-issue-16146Domain:
domain:cliFile surface:
packages/rest/src/error-response.ts(declaredServerFaultAnswer, andresolveErrorResponse's own 5xx arm) + its tests.packages/types/src/thrown-http-error.ts(serverFaultProvenance) only if the one-reader route is taken — see the anchor warning below.packages/runtime/src/dispatcher-plugin.tsonly if this card absorbs #17153's arm, which is this card's call.Container & model:
M,mode:subagent,model: opus— default judgment tier.Clause-②: no
Thread-read: 5626498196
Serial constraints cleared: derived 2026-09-10T22:52Z across 22 of 22 open PRs / 234 (PR,file) rows:
error-response.ts0 holders,dispatcher-plugin.ts0,thrown-http-error.ts0,error-leak.ts0,rest-server.ts0, and 0 rows anywhere underpackages/rest/,packages/runtime/orpackages/types/. Positive controls fired (22packages/spec/, 7packages/cli/rows); a fabricated path returned 0.Contract-text:— the declaration this relay is being built to honour, measured onorigin/maina36b526f:packages/spec/src/api/contract.zod.ts— the producer-side refusal field onApiErrorSchema, whose own docblock states 「The producer's user-facing refusal text, verbatim」 and 「Status-agnostic. Not a 403 special case: any refusal status may …」.packages/spec/src/api/contract.test.ts:90—ApiErrorSchema.parse({ code: 'NOT_IMPLEMENTED', message: REFERENCES_REFUSAL, httpStatus: 501, refusal: true }), with 「The declaration rides BESIDEmessage; it never replaces or carries it.」⇒ the relay reads a field the published envelope already declares and behaves as that field's own text prescribes ⇒ a pull-back, ⛔ not a widening. Also
SKILL.md's negative boundary applies and is quoted rather than assumed: 「负边界:运行时权限/安全行为变更不是条款②,归人工地板安全/权限边界类」.Prior-ruling search — run BEFORE this claim, and it found a maintainer ruling the card does not cite
⭐ ADR-0112
:146, ruled by the maintainer 2026-08-27, on this exact family:⭐
:150names the one definition and where it is anchored: 「serverFaultProvenance(packages/types/src/thrown-http-error.ts, anchored inscripts/adr-anchors/) is the ONE definition … which is what makes "all doors inherit" a construction rather than five suites agreeing about literals.」⇒⚠️ If the one-reader route runs through
serverFaultProvenance, it touches an ADR-anchored symbol. That is a real constraint on the route #17153 suggests, and ⛔ it is not a reason to re-derive per door — the same ruling forbids exactly that.⭐
:152is the line that matters most, because it scopes the ruling away from this card's axis:⇒ the prose withhold this card now proposes to partially reverse for declared refusals was itself a deliberate, separately-carded decision.⚠️ The batch #58 ruling on this card and ADR-0112's scope note are probably compatible —
refusaldid not exist when the prose axis was decided — but that compatibility must be argued from both texts, ⛔ not assumed because the newer ruling is newer.GET /issues/12281returns 404 in this repository, while[#12281]pins are live inpackages/runtime/src(including one that records 「Controls: 88
docs/adr/*files matcherror; a fabricated term returns 0.派发席位 ·
session_01DapQyvYrFb1MxSYe7BL2nt· R72 · 2026-09-10T22:52Z(读表) · 本评论来自domain:cli派发座位Generated by Claude Code
os-justin commentedon Sep 11, 2026
os-dev-report
{ "issue": 16146, "status": "done", "branch": "claude/issue-16146-declared-refusal-relay", "pr": "https://github.com/objectstack-ai/objectstack/pull/17585", "premise_still_valid": true, "summary": "The card's core premise holds and was re-derived on origin/main: declaredServerFaultAnswer gated on declaredHttpStatus at or above 500 alone and replaced the prose of a deliberate refusal and a server fault alike. One reader, declaredRefusalMessage in @objectstack/types beside serverFaultProvenance, now answers the producer's declared refusal prose (refusal === true, a declared in-band 5xx in either spelling, a non-empty code asked through declaresServerFault, and prose that does not trip looksLikeInternalErrorLeak); all three withhold arms call it and none re-derives it, and REST logging follows the same field. #17153's runtime arm is ABSORBED here. Two sub-premises were falsified and are reported rather than papered over: (1) the card's own 'single relay' sentence is false and the count is FOUR, not three -- rest-server.ts:11349 is a declaration-gated withhold site named by neither the card nor #17153, unreachable by a refusal by construction and pinned as such; (2) the ruling's 'retire the route-local patch from PR #16143' rests on the premise that the patch's only effect is the prose relay, and it has a second, independently-pinned effect -- the NESTED ADR-0112 envelope on that door's A exit -- which the flat relay cannot restore, so the prose half is retired and the envelope half remains as a position adapter over the shared answer. Envelope position is a separate line from vocabulary by ADR-0112's own #9232 amendment. RECONCILIATION (batch #58 vs ADR-0112 around line 152): COMPATIBLE, argued from both texts. The ADR's 'Scope, exactly.' note 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; it is the declared limb of the same serverFaultProvenance function and deliberately not applied here.' So there is no ADR-0112 holding about 5xx prose to contradict; the prose axis was carded separately and landed at the dispatcher exit. Batch #58 does not reverse it -- the published field's own docblock says 'This field adds the third row; the first two are unchanged, and the second is still the DEFAULT', and absent the flag this diff is byte-identical at all three arms. They also share one argument: the ADR says 'The discriminator is the STATUS channel, and it 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 rather than strained. SECURITY FLOOR (what bounds the kept message): four things, the first three structural -- (1) the flag is set only by the producer at throw time, platform and driver code never set it, and any rewrap that composes a new Error drops it so the 5xx is withheld as a fault (measured for metadata-protocol's two overlay-delete rewraps, which carry status, code and userMessage and carry no flag; no carryRefusal is added there, per the #9934 refusal); (2) the flag alone is not enough -- it qualifies an already-declared in-band 5xx status beside a non-empty code, a shape a bare driver error can never satisfy because it declares no status; (3) looksLikeInternalErrorLeak stays unconditional, so a producer cannot buy leaky prose past the driver/SQL filter by declaring a refusal -- this is what stops an undeclared internal string riding the channel, and it is driven at both REST and the dispatcher exit; (4) length -- in packages/rest the kept message goes through truncateClientMessage at CLIENT_MESSAGE_MAX 500, which is what 'bounded exactly as a 4xx message is' means in that package, while at the dispatcher exit a caller-addressed message is unbounded today so a kept refusal is treated exactly as that door already treats a 4xx. The withhold's compensation survives: the untouched error still reaches the operator via __obsRecordedError and logWithheldServerFault, pinned.", "tests": "WIRE-DRIVEN at all three arms, every case through a real mounted route. New: packages/types/src/thrown-http-error-refusal.test.ts (12), packages/rest/src/rest-declared-refusal-relay.test.ts (12, arm 1 via POST /analytics/dataset/query which calls the arm BARE, arm 2 via GET /meta/:type/:name/references through handleRouteError, plus the [REST] Unhandled error log assertion with its own control); extended packages/runtime/src/dispatcher-plugin.declared-5xx-prose-withhold.test.ts (11 to 18, the four [#12281] cases untouched and still red-proof). ABLATION (commit-first; five source files checked out at a36b526f, rebuilt, marker proven ABSENT from packages/types/dist via scripts/ablation-dist-preflight.mjs --absent, then restored and blob-identity-verified against HEAD for all five, then rebuilt again and the marker proven PRESENT): types 12 failed/12, rest 4 failed + 8 passed/12, runtime 2 failed + 16 passed/18. The 8 and the 16 that survive ablation are the negative halves -- a declared fault still withheld, an undeclared 5xx still heuristic-judged -- i.e. red-proof pins for behaviour this PR does not move. SUITES (exit codes captured by redirect before any pipe, all exit 0): @objectstack/types 21 files/624 tests + typecheck; @objectstack/metadata-protocol 173 files/2487 tests + typecheck; @objectstack/runtime 254 files/3571 tests + typecheck; @objectstack/rest 190 files/3177 tests + typecheck. GATES: floor derived from the WHOLE change set including the changeset and both ledger files, re-derived after merging origin/main f721ef0f and diffed against the pre-merge list -- identical, 72 families; all 72 run with per-family exit codes; '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 reds resolved, not 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 via --write. pnpm lint over the WHOLE repo: 6595 files, 0 errors, 0 warnings, exit 0 -- file count read from eslint --format json, no narrowing claimed. FOURTH-EXIT SWEEP with controls on the merged tree 8b38cf2f: two sweeps over non-test packages/*/src and packages/*/*/src (every substitution site, then every declaration reader); closed set is FOUR declaration-gated withhold sites (the three plus rest-server.ts:11349) and a FIFTH door #17153 did not enumerate, plugin-hono-server/src/adapter.ts:269, measured heuristic-only and therefore out of the set; positive controls = the arm-1 and arm-3 call sites, both found; a fabricated symbol and a fabricated literal each returned 0.", "mcp_calls": "2 - one search_issues (dedupe for the finding below) and one issue_write create. Every other GitHub read and write went through container REST.", "open_questions": [ { "question": "The ruling's constraint 'retire the route-local patch from PR #16143' cannot be executed literally without regressing the envelope half of #15685. Its prose half is retired; notImplementedRefusalAnswer survives as a pure position adapter (nested ADR-0112 envelope) gated on the shared reader. Should the /references A exit keep the nested envelope, or should the door converge on the flat relay envelope and let the #15685 positional pin move?", "options": [ "A - keep it as delivered: the arm holds no refusal/fault opinion, only the envelope position, and that position is owned by the check:route-envelope ratchet as a separate line.", "B - delete the arm entirely and move the #15685 positional pin to the flat shape, accepting that this door regresses to body.error.code === undefined on its A exit while its B exit stays nested.", "C - converge the whole handleRouteError family on the nested envelope, which is the route-envelope ratchet end state and far outside this card." ], "recommendation": "A, because ADR-0112's #9232 amendment says in as many words that vocabulary and position are two decisions and that position belongs to the route-envelope ratchet, so folding a position change into a prose ruling would decide one line inside another. B re-opens half of a landed, pinned fix for no gain the ruling asked for." } ], "out_of_scope_findings": [ "filed as #17584: the /references 501 back-loads its ADR-0110 D3 prescription, so the #5423 bound this PR routes that door through silently cuts the remedy once object plus field names total roughly 72 characters (412 chars at account.owner, 511 at 40/40) - repair is the producer sentence order and moves two content pins", "noted, not filed: rest-server.ts:11349 is a FOURTH declaration-gated withhold site (declaresServerFault || looksLikeInternalErrorLeak) that neither the card nor #17153 enumerates - correct as it stands and unreachable by a declared refusal by construction, pinned by the status 700 case in thrown-http-error-refusal.test.ts; successor: the next author of this family, who will otherwise re-derive the count of three", "noted, not filed: ApiErrorSchema.refusal docblock enumerates four doors that read no declaration and plugin-hono-server/src/adapter.ts:269 is a fifth in the same class - packages/spec is read-only for this card and a missing list member is completeness, not a defect; successor: whoever next edits that docblock or takes a 5xx-door card", "noted, not filed: the ADR anchor for thrown-http-error.ts said the #12281 prose limb was deliberately not applied YET, which stopped being true when that card landed at the dispatcher exit - corrected in this PR because the anchor governs the file this change edits" ] }Generated by Claude Code
github-actions commentedon Sep 11, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.c3ebe4a5cb, merged intomain.bug,priority:p2,domain:cli,finding— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34552285557 · trigger
scheduleGenerated by Claude Code
os-justin commentedon Sep 11, 2026
Landed — verified by measurement on
origin/main, ⛔ not from the merge eventCommit:
c3ebe4a5cbedb286723bf91642d5821718a60e9f— 「fix(rest,runtime,types): relay a producer-declared 5xx refusal's prose at all three withhold arms (#17585)」, merged 2026-09-11T01:55:06Z.Shape:
git rev-list --parents -n 1 c3ebe4a5⇒ 2 fields = single parent = squash.The one-reader construction, read line by line rather than counted:
⇒ one definition, three call sites, one per arm. A fabricated symbol over the same three trees returns 0.
Card disposition: closed⚠️
completedat 01:55:07Z by the PR's ownFixes #16146.pm:dispatchedwas already gone when I went to strip it — the labelDELETEanswered 404, and the timeline says why:unlabeled pm:dispatched by github-actions[bot] 2026-09-11T01:55:37Z, thirty seconds after the close. ⇒ a 404 there is the label already being absent, ⛔ not a failed write, and the actor was automation, ⛔ not a concurrent seat. Reading the timeline before re-hanging is what tells those apart.The reconciliation this card was blocked on, settled
The ruling's execution line said 「this card = the relay half, blocked on #16335」. That field landed, this seat unblocked the card on the measurement, and the delivery then had to reconcile batch #58 against ADR-0112
:152— which I dispatched as a possible stop-and-report.⭐ COMPATIBLE, argued from both texts.
:152is a scope disclaimer, not a prose ruling: 「This rules the CODE channel only. The PROSE axis … is #12281, a separate card with its own measurement-first step」. ⇒ there was no ADR-0112 holding about 5xx prose for the newer ruling to contradict. And the newer one does not reverse the older: absent the flag, the diff is byte-identical at all three arms — which is what makes 「exactly one exception」 a measurement rather than a phrase. The ADR's own 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 it.⭐ This card's own sentence was false, and the count is FOUR
「the single relay for every producer-declared 5xx at every door」 — corrected in this PR, as triage on #17153 required (「fixing the arm and leaving the sentence is half the card」).
The sweep then found a fourth declaration-gated withhold site that neither this card nor #17153 enumerates. Verified at source myself —
packages/rest/src/rest-server.ts:11349:…and the docblock immediately above it explains why a declared refusal can never reach it: 「it reads
error.statusalone and is not bounded above, so a nonsensestatus: 700with a code still reaches here (declaredHttpStatusrequires < 600) and must keep the withhold it has had since #5367」. ⇒ correct as it stands, unreachable by construction, and now pinned by exactly thatstatus 700case.A fifth door —
plugin-hono-server/src/adapter.ts:269— was measured heuristic-only and is therefore outside the declaration-gated set.Batch #58's second line: 「Retire the route-local patch from PR #16143 once the relay handles
/references」. The prose half is retired. The envelope half is not, because that patch also re-dresses the answer into the nested ADR-0112 envelope, which #15685 pins independently and a flat relay cannot restore.Ruled A at PR comment
5627546373— keep it as a pure position adapter over the shared answer — on the ground that ADR-0112's #9232 amendment makes vocabulary and position two separate lines, and position belongs to thecheck:route-enveloperatchet. Accepted under 「验收判据本身也可被证伪…但推翻过程必须写在 PR 正文并附实测」, which the PR body satisfies in its own section.⇒ batch #58's execution list should not be read as fully discharged. That remainder is the maintainer's.
Still open and ⛔ not closed by this landing: #17584 (filed by this card's dev — the
/references501 back-loads its ADR-0110 D3 prescription, so the bound this PR routes that door through cuts the remedy once object + field names reach ~72 characters).Records: review
5627999791· seat answer on the open question5627546373· #17153's disposition5627537252.派发席位 ·
session_01DapQyvYrFb1MxSYe7BL2nt· R72 · 2026-09-11T02:18Z(读表) · 本评论来自domain:cli派发座位Generated by Claude Code
feat(spec): ADR-0112 error envelope gains a producer-side `refusal` d…
fix(metadata-protocol): front-load the ADR-0110 D3 prescription in th…