Skip to content

[finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146

Closed
1 of 1 issue completed

Description

@os-litant

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 on origin/main while 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 /references throw actually takes, since it spells status); errorResponseBase in @objectstack/runtime, outside @objectstack/rest entirely, measured by this card's sub-issue #17153; and a fourth, rest-server.ts line 11349, the analytics dataset door's generic-500 branch, which reads declaresServerFault and 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 field item 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

⚠️ Not independently re-derived by this seat — this is the dev agent's measurement on #15685, relayed. The 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

  1. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊:domain:cli / bug + finding / needs-user-decision / priority:p2

    ⛔ needs-user-decision 不与 pm:* 并存(先例 #15854 / #15617 / #15542),故不挂 pm:queue。

    卡片明确要求「acted on 之前先复核」——本席已独立复核,逐条如下

    卡片主张(转述自 dev 席,未经填卡席复核) 本席在 origin/main 的实读 结论
    gate 是 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 :576 error: INTERNAL_ERROR_MESSAGE,,无条件 ✅
    它是 #11718 抬出来的单一中继 :1397 // [#11718] The arm's BODY now lives in {@link declaredServerFaultAnswer} / :1403 const 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:

    body: {
        error: INTERNAL_ERROR_MESSAGE,
        ...(declaresServerFault({ status: declaredStatus, code: error?.code })
            ? thrownCodeFields(error, declaredStatus)
            : {}),
    },

    即 declaredServerFaultAnswer 已经在用 declaresServerFault({status, code}) 这个"status/code 对"判别器——但只用它决定要不要带 code 字段,没用它决定要不要留住 message。

    而且这不是疏漏,是当时明写下的取舍。同文件 :559-564 的档注逐字:

    ⚠️ Its gate is declaredHttpStatus(...) >= 500, NOT declaresServerFault … Gating on declaresServerFault instead would …

    (:564 后半句给了理由,取它的人应当先读完这一整段再动。)

    ⇒ 问题因此从「发明一个判别器」缩成一个更小、更可答的问题:

    既然 declaresServerFault 已经在这个函数里被信任到足以决定 code 字段的去留,为什么同一个判断不足以决定 message 的去留?:559-564 给出的理由,在 #16143 的 /references 501 这类为调用者撰写的 refusal 面前还成立吗?

    这个改写不构成裁决——它只是把决定箱从开放式设计题收敛成一个对着 :559-564 现有论证的是/否。


    ⚠️ 顺带更正卡片 dedup 段里的一行

    卡片写:"#13242 is about declaresServerFault having 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 在下游一条路由上局部补掉。

    ② 分叉

    • A. 改中继:让 message 的保留与否也走 declaresServerFault(或另一个明确判别器),一次覆盖所有门。
    • B. 维持现状 + 逐路由补丁:每个 producer-declared 5xx refusal 各自像 rest/meta: the /references door answers both 501 refusals in one ADR-0112 envelope #16143 那样在路由内补。
    • C. 第三条:引入 producer 侧的显式 refusal 声明(例如 status/code 上一个 published allow-list),不复用 declaresServerFault。

    ③ 各支的代价

    ④ 需要裁决者提供的东西
    一句话::559-564 记录的「不用 declaresServerFault 当 gate」这个取舍,是否只适用于 status 的透传,而不适用于 message 的保留? 答「是」⇒ 走 A(且 :559-564 需补一句划清两者);答「否」⇒ 走 B 或 C,并应当在 error-response.ts 里明写「producer-declared 5xx refusal 的 prose 由路由自己负责保住」,好让下一个作者不必靠踩坑发现。


    分诊侧的其余判断


    ⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable)。


    Generated by Claude Code

  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    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"; declaredServerFaultAnswer keeps 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) is status >= 500 && typeof code === 'string'. A deliberate refusal such as the /references 501 declares both, so gating the message on declaresServerFault withholds the same prose the gate withholds now. The :559-564 note is therefore not the obstacle; the missing declaration is.

    Execution (this card = the relay half, blocked on #16335).

    Labels: needs-user-decision → pm:blocked (Blocked-by: #16335). Ledger on #12708 (batch #58).


    Generated by Claude Code

  3. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    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 「同意」):

    Execution (this card = the relay half, blocked on #16335).

    #16335 is closed completed. ⛔ Not taken from its state alone — 「closed completed is not coverage」 — the field itself was measured on origin/main at a36b526f:

    • packages/spec/src/api/contract.zod.ts declares the producer-side refusal field on ApiErrorSchema, 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:90 pins the shape end-to-end: ApiErrorSchema.parse({ code: 'NOT_IMPLEMENTED', message: REFERENCES_REFUSAL, httpStatus: 501, refusal: true }), then 「The declaration rides BESIDE message; 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: userMessage matches across 13 files in packages/spec/src on 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; and declaresServerFault keeps its live production caller at error-response.ts:577 — an ADR-0049 remove pass must ⛔ not delete it.

    ⚠️ One thing the next taker must not read past

    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: errorResponseBase in packages/runtime/src/dispatcher-plugin.ts, gated by serverFaultProvenance, which replaces the message of every producer-declared 5xx and never consults declaredServerFaultAnswer. It is a production door — mounted by objectstack serve and by plugin-dev, answering POST {prefix}/analytics/query among 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

  4. self-assigned this
    on Sep 10, 2026
  5. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    Claim: PM loop round R72
    Session: session_01DapQyvYrFb1MxSYe7BL2nt
    Branch: claude/issue-16146-declared-refusal-relay
    Worktree: objectstack-issue-16146
    Domain: domain:cli
    File surface: packages/rest/src/error-response.ts (declaredServerFaultAnswer, and resolveErrorResponse'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.ts only 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.ts 0 holders, dispatcher-plugin.ts 0, thrown-http-error.ts 0, error-leak.ts 0, rest-server.ts 0, and 0 rows anywhere under packages/rest/, packages/runtime/ or packages/types/. Positive controls fired (22 packages/spec/, 7 packages/cli/ rows); a fabricated path returned 0.

    Contract-text: — the declaration this relay is being built to honour, measured on origin/main a36b526f:

    • packages/spec/src/api/contract.zod.ts — the producer-side refusal field on ApiErrorSchema, 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 BESIDE message; 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: 「负边界:运行时权限/安全行为变更不是条款②,归人工地板安全/权限边界类」.

    ⚠️ But that same negative boundary is a redirection, ⛔ not a dismissal. This change alters what prose leaves the server on a 5xx. That is the leak surface, and the sentence above routes it to the manual security/permission floor rather than out of scope. The dev must raise it explicitly and say what bounds the kept message.

    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:

    In 5xx sanitisation a demoted code … is withheld along with the prose; an author-declared declaredCode survives. Implemented once at the shared resolver layer so all doors inherit one rule; ⛔ no per-registrar variants.

    ⭐ :150 names the one definition and where it is anchored: 「serverFaultProvenance (packages/types/src/thrown-http-error.ts, anchored in scripts/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.

    ⭐ :152 is the line that matters most, because it scopes the ruling away from this card's axis:

    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 … it is the 'declared' limb of the same serverFaultProvenance function and deliberately not applied here.

    ⇒ 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 — refusal did 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.

    ⚠️ NOT MEASURED: GET /issues/12281 returns 404 in this repository, while [#12281] pins are live in packages/runtime/src (including one that records 「⚠️ REVERSED, deliberately. Until #12281 this case asserted the opposite」). ⇒ that card lives somewhere I cannot read from this session; ⛔ I am recording its absence as unmeasured, not as nonexistent.

    Controls: 88 docs/adr/* files match error; a fabricated term returns 0.

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


    Generated by Claude Code

  6. os-justin commented on Sep 11, 2026

    @os-justin
    Collaborator

    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

  7. github-actions commented on Sep 11, 2026

    @github-actions
    Contributor

    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.

    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 schedule

    Generated by Claude Code

  8. os-justin commented on Sep 11, 2026

    @os-justin
    Collaborator

    Landed — verified by measurement on origin/main, ⛔ not from the merge event

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

    packages/types/src/thrown-http-error.ts:396   export function declaredRefusalMessage(…)   ← the ONE definition
    packages/rest/src/error-response.ts:58        import  ·  :773 call (arm 1)  ·  :2570 call (arm 2's guard)
    packages/runtime/src/dispatcher-plugin.ts:4   import  ·  :742 call (arm 3)
    

    ⇒ one definition, three call sites, one per arm. A fabricated symbol over the same three trees returns 0.

    Card disposition: closed completed at 01:55:07Z by the PR's own Fixes #16146. ⚠️ pm:dispatched was already gone when I went to strip it — the label DELETE answered 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. :152 is 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:

    const outward = declaresServerFault(error) || looksLikeInternalErrorLeak(msg)
        ? INTERNAL_ERROR_MESSAGE
        : clientMsg.slice(0, 500);

    …and the docblock immediately above it explains why a declared refusal can never reach it: 「it reads error.status alone and is not bounded above, so a nonsense status: 700 with a code still reaches here (declaredHttpStatus requires < 600) and must keep the withhold it has had since #5367」. ⇒ correct as it stands, unreachable by construction, and now pinned by exactly that status 700 case.

    A fifth door — plugin-hono-server/src/adapter.ts:269 — was measured heuristic-only and is therefore outside the declaration-gated set.

    ⚠️ Three consecutive rounds have each found one more arm. Triage predicted it on #17153 and asked for the check; whoever touches this family next should assume the count is a floor, ⛔ not a closed set.

    ⚠️ A ruled constraint is half-executed, by design — only a maintainer can finish it

    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 the check:route-envelope ratchet. 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 /references 501 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 question 5627546373 · #17153's disposition 5627537252.

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


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions