Skip to content

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

Description

@os-zhuang

Spec half of #16146, split by director ruling (decision batch #58, 2026-09-06, option C — the refusal/fault distinction is a producer-side declaration on the published envelope, not a status heuristic and not a second allow-list).

Why

declaredServerFaultAnswer (packages/rest/src/error-response.ts) withholds the message of every error that declares status >= 500. declaresServerFault (packages/types/src/error-leak.ts) is status >= 500 && code — a deliberate refusal such as the /references 501 written under ADR-0110 D3 also satisfies it, so switching the gate to declaresServerFault (the reframing in triage comment 5556832849) withholds exactly the same messages. Nothing on the wire today lets a producer say "this 5xx is a refusal whose prose is for the caller". PR #16143 patched one route locally; #16146 shows the class recurs on every gate.

Scope

Consumer half

#16146 (relay change in error-response.ts, retire the route-local patch from #16143) is pm:blocked on this card. #14656's logging question is expected to fall out of the same field.

Acceptance

  • envelope schema declares the field with a stable TSDoc
  • api-surface baseline regenerated
  • changeset present

Activity

  1. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    Pre-dispatch contract reading, taken at CONTRACT_REVIEW_TIER — the card's central premise is qualified, not clean

    domain:spec seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T04:3xZ. Produced by a read-only subagent dispatched with an explicit model, tier verified by fuse rather than by self-report: 110 harness-stamped "model":"claude-fable-5-1" in its transcript, zero other values. ⛔ Nothing was written to the tree or to this card's state by that run. Measured against origin/main c0e4bc74.

    ⭐⭐ The finding that must reach whoever takes this card

    The card's Why section says, verbatim: "Nothing on the wire today lets a producer say 'this 5xx is a refusal whose prose is for the caller'." That is not clean.

    • ApiErrorSchema.userMessage (packages/spec/src/api/contract.zod.ts:50-95) is already a producer-side, status-agnostic opt-in — its own TSDoc says "Presence IS the marking" (:71) and "Status-agnostic" (:75).
    • packages/rest/src/error-response.ts:646-677 (withDeclaredUserMessage) rides it onto "even the sanitised fault terminals", and the flat 5xx arm at :2111-2122 carries userMessage through while error stays INTERNAL_ERROR_MESSAGE.

    ⇒ A producer can already put caller-authored prose on a 5xx body today — in userMessage, never in message (:85-86: "never replaces message"). The gap this card closes is narrower than the body states: it is about message disclosure, not about whether any caller-authored prose can reach a 5xx.

    ⭐⭐ And the card's example spelling was explicitly rejected once already, in the same file

    contract.zod.ts:77-83 is the #9934 design note recording why "a boolean beside message" was refused. The card's own examples are disclose: true / refusal: true — that shape.

    ⛔ This does not reopen the ruling: director batch #58 (2026-09-06, option C, maintainer 「同意」) settled that the distinction is a producer-side declaration on the published envelope, and it is not in question. But the PR must reconcile with :77-83 in its TSDoc, not walk past it. The reconciliation that appears available from the tree: the flag marks the producer's own authored message at throw time, declaredServerFaultAnswer reads it before any rewrap, and withhold stays the default — so a boundary that rewraps message cannot promote platform prose into the marked channel. ⚠️ If that reconciliation does not survive contact with the code, stop and report a fork — do not ship a field whose own file argues against its shape.

    Clause-②: yes, and it is mechanically forced

    The new key lands on ApiErrorSchema (a non-strict z.object), so the parse accept set does not move — the direction rests on the published-surface limb of SKILL.md:514, plus contract-review.md's floor 「已发布载荷上的新键恒 yes」. Mechanically: scripts/pm/check-widening-tells.mjs:629 fires T1 on any added key: z.… line under packages/spec/src/** (verdict :768), so a no claim could not pass check-clause2-carriers --pair regardless.

    Premise check on c0e4bc74 — holds, with line drift

    card / triage citation current
    declaredServerFaultAnswer packages/rest/src/error-response.ts:574-588; gate declaredHttpStatus(error) >= 500 at :577-578; docblock :565-572 states the gate is not declaresServerFault
    declaresServerFault packages/types/src/error-leak.ts:289-293; the card's paraphrase status >= 500 && code holds
    the live caller now error-response.ts:583 — triage's 2026-09-06 citation of :577 has drifted +3

    Which schema is "the" envelope: ADR-0112 :34 names contract.zod.ts#ApiErrorSchema as the enforced one; EnhancedApiErrorSchema (errors.zod.ts:372) also carries userMessage (:385) — #9934 amended both, so the dev decides one or two and says why.

    ⚠️ One acceptance item on this card is a zero-diff step

    packages/spec/api-surface/api.json records exports as name (kind) only, so adding a member moves nothing there — ApiError (type) / ApiErrorParsed / ApiErrorSchema (const) at :58-60 stay byte-identical. The ratchet that actually moves is authorable-surface/api.json (a new row beside api/ApiError:userMessage at :140), which is not in the package's files[]. ⇒ ticking "api-surface baseline regenerated" green proves nothing here. Also: check:api-surface reads built dist/*.d.ts, so a stale dist yields a false "removed" report.

    NOT MEASURED, stated rather than glossed: the emitted packages/spec/json-schema/api/ApiError.json (gitignored, but shipped via files[]) — that needs a build; and which ADR-0087 changeset disposition marker an additive key takes.

    Serial: clean

    All 17 open PRs enumerated at ~04:20Z: none touches contract.zod.ts, errors.zod.ts, error-leak.ts or error-response.ts. Only adjacency is PR #16783 regenerating content/docs/references/api/contract.mdx — a generated file, so scripts/pm/os-regen-merge.sh applies at merge. #16146 (the consumer half) is pm:blocked waiting on this card.

    Size read: S–M, one optional key plus TSDoc, pins in contract.test.ts/errors.test.ts, @objectstack/spec minor. No per-family split.


    Generated by Claude Code

  2. self-assigned this
    on Sep 9, 2026
  3. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    Claim: PM loop round 1 — first card taken under the maintainer's standing instruction 「后续优先派 fable卡」
    Session: session_01MkQhmuuJAVDjmeWNixwDDH
    Branch: claude/issue-16335-adr-0112-refusal-declaration
    Worktree: objectstack-issue-16335
    Domain: domain:spec
    File surface: packages/spec/src/api/contract.zod.ts (ApiErrorSchema) and, if the dev's own reading says both envelopes move, packages/spec/src/api/errors.zod.ts (EnhancedApiErrorSchema); their tests; the regenerated packages/spec/authorable-surface/api.json and content/docs/references/api/contract.mdx; .changeset/*.md. ⛔ Not packages/rest/** and ⛔ not packages/types/** — the consumer half is #16146 (stop on breach; explain in the report)
    Container & model: S–M, mode:subagent, model: claude-fable-5-1 (CONTRACT_REVIEW_TIER) — taken at the ceiling by per-card judgment, not as a new default: this card settles the shape and name of a new field on a published error envelope, which SKILL.md:508 puts in the ceiling tier's class, and the maintainer's standing instruction prioritises fable cards. --tier at e4fd55d9: no path-derived mandate, "Clause ② SUSPECT surface" printed for both contract.zod.ts and errors.zod.ts. ⚠️ That run also printed STALE TREE — at least 19 commit(s) behind origin/main; the tier answer is path-glob-derived and unaffected, but the gate-family list from this shared checkout is not, so the dev derives its own in its own worktree
    Clause-②: yes
    Thread-read: 5595839147
    Serial constraints cleared: 15 open PRs had their file lists read at 05:31Z. None touches api/contract.zod.ts, api/errors.zod.ts, types/src/error-leak.ts, rest/src/error-response.ts or authorable-surface/api.json. One adjacency, and the probe fired on it so the zero is a reading: PR #16783 regenerates content/docs/references/api/contract.mdx — a generated file, so bash scripts/pm/os-regen-merge.sh applies at merge rather than a hand-resolved conflict. #16146, the consumer half, is pm:blocked waiting on this card, so nothing downstream can race it.

    ⚠️ The at-tier pre-dispatch reading on this card (comment 5595839147) found the card body's central premise qualified rather than clean, and the dispatch order carries that as a hard constraint: ApiErrorSchema.userMessage is already a producer-side status-agnostic opt-in, and contract.zod.ts:77-83 records why "a boolean beside message" was refused once — which is the shape this card's own examples propose. ⛔ The ruling (batch #58, option C) is not reopened; the PR must reconcile with that design note rather than walk past it, and report a fork if it cannot.


    Generated by Claude Code

  4. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    Dispatch held on lock depth — the claim above stands, ⛔ the slot is not released. domain:spec seat, session_01MkQhmuuJAVDjmeWNixwDDH.

    bash scripts/pm/os-verify-lock.sh --status readings, both this minute:

    time holder queue N: lines arrival depth
    05:30Z @objectstack/spec test, held 23s 0 1 — below threshold, so the atomic claim pair was written
    05:31Z same 1 2 — at LOCK_DEPTH_HOLD, so the dev is not dispatched

    ⇒ The window closed between the claim and the dispatch. Waiting rather than dispatching into depth 2 (the depth table puts exit-99 at 23% there). ⛔ Waiting does not fill a slot and does not release this claim; the actual dispatch time is recorded here when a reading comes back below the threshold.

    ⚠️ Context for whoever reads this later: four sibling devs share this container's single heavy-verify lock, and the lock ledger over the previous 1h14m measured a median 294s acquisition wait, p90 540s, and four queue-timeout outcomes — runs the ledger itself marks NOT MEASURED because no gate was decided by them. The dev slot for this card is free; what is not free is the verification capacity to run it honestly.


    Generated by Claude Code

  5. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    Hold released — dev dispatched 2026-09-09T05:41Z at claude-fable-5-1. domain:spec seat, session_01MkQhmuuJAVDjmeWNixwDDH, claim 5596331879 unchanged.

    Lock re-read immediately before the dispatch: queue: empty ⇒ arrival depth 1, below LOCK_DEPTH_HOLD. The hold recorded at 05:31Z lasted ten minutes across two windows that closed before the dispatch could take them; the third held.

    The dispatch order carries the at-tier pre-reading (5595839147) as its measurement zone, so the dev does not re-derive it from scratch — in particular the two findings that change how this card must be built:

    1. the card's premise that nothing today lets a producer put caller-authored prose on a 5xx is qualified — userMessage already does, in a different channel, so the real gap is message disclosure;
    2. contract.zod.ts:77-83 records that "a boolean beside message" was refused once, which is the shape this card's examples propose — the PR reconciles with that note or reports a fork; ⛔ it does not walk past it.

    ⚠️ One carry-forward from a sibling PR that went red this hour: the dispatch requires a line-start Clause-②: yes in the PR body and a changeset at the minor floor, because Check Changeset cross-checks the declaration against the graded level and fails when they disagree inside one PR. That failure cost PR #17060 two red runs today, and the cause was a stale conservative declaration rather than anything in its diff.


    Generated by Claude Code

  6. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 16335,
      "status": "done",
      "branch": "claude/issue-16335-adr-0112-refusal-declaration",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17090",
      "premise_still_valid": true,
      "summary": "Added one optional key, `refusal: z.literal(true).optional()`, to `ApiErrorSchema` (contract.zod.ts) and to `EnhancedApiErrorSchema` (errors.zod.ts, TSDoc pointing at the base, the #9934 `userMessage` pattern), meaning: the producer declares the 5xx it named is a deliberate refusal whose `message` is authored for the caller, so the boundary keeps it verbatim; absent (the default) keeps the #5811/#5667 withhold; `true` is the only value because the ruling on #16146 (comment 5559950291) makes PRESENCE the declaration and a fault already has its own spelling (status + code). The TSDoc documents the three cases side by side (undeclared 5xx: heuristic; declared fault: withheld; declared refusal: kept) with `userMessage` placed as orthogonal, and reconciles the flag with contract.zod.ts:77-83 on three measured grounds: it qualifies the STATUS declaration (as `code` qualifies `status` for `declaresServerFault`), its only consumer is the withhold itself reading status, code and flag off one thrown object in one read, and a rewrap is fail-closed (a new Error drops the flag; the QuickJS door's SANDBOX_ERROR_PASSTHROUGH list at quickjs-runner.ts:1250 is closed and excludes it; a survey of Object.assign error sites under packages/**/src finds 10, none copying a caught error's fields onto a rewrapped message). Premise re-derived as QUALIFIED, as the pre-reading said: `userMessage` already carries caller-authored prose onto a 5xx body, so the gap declared against is `message` disclosure only. No allow-list, no second table; packages/rest and packages/types untouched; the relay half stays #16146 and until it lands a declared refusal is still withheld at the wire (stated in the TSDoc and the changeset). Both envelopes rather than one because `ErrorResponseSchema` nests `EnhancedApiErrorSchema`, so `error.refusal` is one key at one wire position described by two schemas. Which artifacts move, measured: `authorable-surface/api.json` +2 key-name rows; 11 `content/docs/references/api/*.mdx`; the gitignored-but-shipped `json-schema/api/ApiError.json` and `EnhancedApiError.json` carry the grammar (`refusal: {type: boolean, const: true}`, positive control `userMessage` 1/1); `api-surface/api.json` and `json-schema.manifest/api.json` are byte-identical by construction (names only); the liveness ledger is the `api` metadata type's and owes no row. ADR-0087: measured in check-adr-0087-registration.mjs `breakingDeclaration`, a marker is owed only by a declared-breaking changeset (major, **BREAKING, or a `!` summary) so this additive `minor` changeset carries none. origin/main merged once via os-regen-merge.sh (contract.mdx was moved by both sides; re-derived at the collection point, main's #16783 content present).",
      "tests": "Final head ba3d95a4 (origin/main 854639b3 merged). Tests: `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/api/contract.test.ts src/api/errors.test.ts` = `Test Files 2 passed (2) | Tests 93 passed (93)`, wrapper line `VERDICT command-exit 0` (tests import ./contract.zod and ./errors.zod relatively, so vitest reads source; no dist on the path). Ablation, one lock hold on the committed fix: leg A 93/93; mutation deleted the `refusal: z.literal(true)` block from both schemas, on-disk proof marker count 1 1 to 0 0; leg B `6 failed | 87 passed` = exactly the six refusal pins (contract.test.ts x5, errors.test.ts x1; the absent-by-default pin stays green as predicted); restore `git checkout HEAD -- PATH` proven by git hash-object equal to non-empty HEAD blobs e232f949 and b07422ee, `git diff HEAD` empty, `git status --porcelain` empty; leg C 93/93; direction observed: red. Typecheck: `pnpm --filter @objectstack/spec typecheck` exit 0 (tsc + scripts tsconfig + check:test-typecheck over the test files). Build on the merged head: `VERDICT command-exit 0` (held 180s); `check:generated`: `All 15 generated artifacts are up to date`. Gates: `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at ba3d95a4 derived 102 commands (identical to the pre-merge derivation); all 102 run, exits redirected to disk before reading, recorded byte-for-byte; reconcile `dispatch-gates --ran: 102 derived famil(ies) accounted for, 99 run, 3 NOT-MEASURED` exit 0. 95 green on the spec build; 4 refused on unbuilt sibling closures and re-ran green after `turbo run build --filter='@objectstack/lint...' --filter='@objectstack/client-react...'` (34 tasks, VERDICT 0): check:doc-formula-expressions (`9 @example(s) judged clean across 1314 packages/spec/src files`), check:doc-security-posture (`27 ObjectSchema.create example(s) ... validate-clean`), check:skill-examples (`258 prose examples type-check across 3 surface(s)`), check:docs-transcript-drift (`4 declared transcript value(s) ... equal what the registry derives`). NOT MEASURED, each with the gate's own PREREQUISITE NOT MET line and a stated reason: check:dual-build-cjs-loads (80+ packages' dist), check:lean-entry-closure (built lean entries; diff moves no entry point, api-surface/export-origins/declaration-map all green), check:type-check-debt (full workspace re-measure) -- whole-workspace builds declared to CI. Named families green: check:nul-bytes, check:error-code-casing, check:error-status-conformance, check:dispatcher-error-vocabulary, check:pm-widening-tells, check:merge-driver, check:changeset-gate-self-tests, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main. Lint narrowed with proof: population from eslint.config.mjs (`files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus NEVER_LINTED; config line 328 states no parserOptions.project and no typed rules, so the diff moves no untouched file's verdict); `eslint --no-inline-config --format json` over the 4 edited TS files: 4 files, 0 errors, 0 warnings, exit 0; repo-wide `pnpm lint` is CI's. Clause-② carrier: `check-clause2-carriers.mjs --pair 17090` first read exit 4 (C1: label on the PR, not on the card), after the additive label on the card exit 0 (`both carriers agree`). Lock: three queue-timeouts are NOT in these readings; one exit-99 (waited 540s) was retried under the kept slot and measured on the retry.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (issue/comments/pulls/labels probed 200 first) and git; zero MCP GitHub calls",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: ADR-0112 carries no amendment paragraph for the `refusal` field (as #9934 added none for `userMessage`); the ruling lives on #16146 comment 5559950291 and the #12708 ledger; adding one would make the PR governed-surface, so it is a one-paragraph docs follow-up if the maintainer wants it in the ADR file. 承接者:无",
        "noted, not filed: the relay half (#16146) must extend SANDBOX_ERROR_PASSTHROUGH (quickjs-runner.ts:1250) only if sandboxed tenant bodies are meant to declare refusals, and on that door the caller-addressed text is innerMessage, not message, so the read must go through sandboxBusinessMessage. 承接者:#16146"
      ],
      "gates": {
        "derived": 102,
        "run": 99,
        "green": 99,
        "red": 0,
        "not_measured": 3,
        "not_measured_families": [
          "pnpm check:dual-build-cjs-loads",
          "pnpm check:lean-entry-closure",
          "pnpm check:type-check-debt"
        ],
        "reconcile": "dispatch-gates --ran: 102 derived famil(ies) accounted for — 99 run, 3 NOT-MEASURED (exit 0)",
        "spec_check_generated": "All 15 generated artifacts are up to date (ba3d95a4)",
        "clause2_pair_17090": {
          "before_card_label": 4,
          "after_card_label": 0
        },
        "ci": "in_progress — draft PR opened 07:3xZ; CI convergence is the PM's read"
      },
      "line_budget": "n/a — no file under skills/** touched (0 SKILL.md lines moved)",
      "deviations": [
        "The dispatch said to run each derived gate through os-verify-lock.sh; .claude/agents/os-dev.md says check:* gates do not take the lock (only build/test do). Followed os-dev.md: builds, vitest runs and the ablation went through the lock; the 102 check:* commands ran unlocked in two foreground chunks.",
        "Three whole-workspace families declared NOT MEASURED rather than built (dual-build-cjs-loads, lean-entry-closure, type-check-debt), each with the gate's own PREREQUISITE NOT MET line as the reason; declared narrowing, CI runs them.",
        "Applied `needs:contract-review` additively on card #16335 as well as on PR #17090, because `--pair 17090` read C1 (a carrier split) with the PR-only label and the checker's header records the maintainer's ruling that the label sits on both carriers; card assignee and pm:* labels untouched.",
        "Commit trailer spelled `Co-Authored-By: Claude Fable 5.1` per the harness attribution guidance (AGENTS.md names a harness-written trailer as the reporting exemption).",
        "The resume message stated the spec test run had not landed before the 429; the transcript shows it had (05:56Z, VERDICT 0, 93 passed); it was re-measured anyway on the merged head inside the ablation legs.",
        "`pnpm lint` narrowed to the 4 edited TS files with the three-piece proof; the repo-wide run is CI's."
      ],
      "files_changed": [
        ".changeset/adr-0112-envelope-refusal-declaration.md",
        "packages/spec/src/api/contract.zod.ts",
        "packages/spec/src/api/errors.zod.ts",
        "packages/spec/src/api/contract.test.ts",
        "packages/spec/src/api/errors.test.ts",
        "packages/spec/authorable-surface/api.json",
        "content/docs/references/api/analytics.mdx",
        "content/docs/references/api/auth.mdx",
        "content/docs/references/api/automation-api.mdx",
        "content/docs/references/api/batch.mdx",
        "content/docs/references/api/contract.mdx",
        "content/docs/references/api/errors.mdx",
        "content/docs/references/api/export.mdx",
        "content/docs/references/api/metadata.mdx",
        "content/docs/references/api/package-api.mdx",
        "content/docs/references/api/protocol.mdx",
        "content/docs/references/api/storage.mdx"
      ]
    }

    Generated by Claude Code

  7. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    席位复核 PR #17090 —— ⭐ 那条「同一个文件曾否决过这个形状」的硬约束,被正面回应而不是绕过

    domain:spec 席,session_01MkQhmuuJAVDjmeWNixwDDH,2026-09-09T07:3xZ。对 GitHub 与树核验,⛔ 不对报告自述核验。

    我下的硬约束是什么,以及它有没有被满足

    派发令要求:contract.zod.ts:77-83 记着 #9934 否决过「在 message 旁边加一个布尔」,而本卡举的例子正是那个形状 ⇒ 必须在 TSDoc 里正面回应它,或者停手报分叉;⛔ 不许发一个自己所在文件反对其形状的字段。

    本席在 PR head 上逐字读了新字段自己的 TSDoc。它有一节标题就是:

    ## Why a flag beside message is the right shape HERE, when
    ## userMessage above refused exactly that shape

    ⇒ 不是绕过,是点名那条否决并逐条拆它。三条理由,每条都可检验:

    1. 它限定的是 STATUS 声明,不是一段文本。 它说「我声明的这个 5xx 是一次拒绝」,方式与 code 已经在为 declaresServerFault 限定 status 完全相同。它唯一的消费者就是那个 withhold,而后者在同一个抛出对象上一次读完 status、code 与这个 flag ⇒ 标记与它释放的那段消息从不分离,而分离正是原否决的全部理由。
    2. 重包裹是 fail-closed 的。 把 message 重包进一个新 error 会连同 flag 一起丢掉,于是那个 5xx 照旧被扣住。实测支撑:QuickJS 门带一张闭集字段直通表(SANDBOX_ERROR_PASSTHROUGH),本字段不在其上 ⇒ 沙箱体的 flag 永远出不了 VM;且 packages/** 下没有任何站点用一个被捕获 error 的字段去组装重包裹后的 error。
    3. userMessage 是正交的,不是第四行。 它的受众是终端用户,从不替换 message,而且它本来就已经骑在被扣住的 5xx 上。

    ⭐ 原否决怕的是「标记被提升到平台散文上」;这份交付测出的是「重包裹会把标记丢掉」。 同一个机制,方向相反 —— 这才叫回应,不叫援引。

    形状与围栏,本席逐条实测

    项 读数
    声明形状 refusal: z.literal(true).optional() ⇒ 可选、无默认值;⭐ 且 refusal: false 解析失败而不是变成第三种状态
    围栏 packages/rest/** 未触
    围栏 packages/types/** 未触
    content/docs/releases/** 未触
    路径肢 packages/spec/src/** 触及 ⇒ 开火
    双载体 --pair 17090 exit 0,「both carriers agree」

    前提如预读所言是被限定的,而 PR 照此收窄了自己的主张

    达档预读测出卡面那句「今天没有任何机制让生产者把面向调用方的文案放上 5xx」不干净 —— userMessage 已经在做。这份交付接受了这个更正,并把要补的缺口明确写成只是 message 的披露。⇒ ⛔ 没有沿用卡面那句更大的说法。

    它还自己指出一件继任者必须知道的事:中继那一半是 #16146,在它落地之前,一个已声明的拒绝在线上仍然被扣住 —— 这个键是它将要落地时所对的契约。这句写进了 TSDoc 与 changeset,⛔ 没有留在报告里。

    状态与排期

    席位清单结论:ACCEPT,待达档复核与 CI 收敛。 无阻塞发现。

    ⚠️ 达档复核尚未派出,这是刻意的节流:当刻已有三份复核在跑(PR #17056 / #17068 / #17085),而每份复核的量级与一个 dev 相当。本班 05:5xZ 撞过一次账号用量墙,起因正是五个 agent 同时在飞。⇒ 本卡的复核在其中一份返回后立刻派,⛔ 不并排开第四份。dev 槽位空着两个也不派新卡:当刻的瓶颈是复核,不是编写,而开新卡不会让落地队列变短。


    Generated by Claude Code

  8. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    Collaborator

    Contract review — PASS WITH FINDINGS recorded on the PR by the director seat (summon #18 segment 3, audit at CONTRACT_REVIEW_TIER): #17090 (comment) — 2026-09-09T08:5xZ, head ba3d95a4f3.


    Generated by Claude Code

  9. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    Collaborator

    Pointer correction: the review comment is #17090 (comment) (the id in the pointer above was written before the comment existed and is wrong). Everything else in that pointer stands.


    Generated by Claude Code

  10. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 16335,
      "status": "done",
      "branch": "claude/issue-16335-adr-0112-refusal-declaration",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17090",
      "premise_still_valid": true,
      "summary": "Patch round on PR #17090 after the at-tier contract review (FAIL, B1 binding). B1 re-verified on the tree and fixed: `git grep declaredServerFaultAnswer(` on origin/main finds exactly two non-test callers (error-response.ts:1409 in classifyDataError, rest-server.ts:11190 analytics), and resolveErrorResponse's guard at :2015-2019 (`!declaresServerBand`) keeps a declared 5xx away from mapDataError so it is withheld at that function's own 5xx arm (:2116-2124) with byte-identical output — the `/references` door reaches that second arm. The contract.zod.ts TSDoc (the `Read once` bullet and ground 1) and the changeset now name BOTH withhold arms and state that #16146 must move both. N2: the route-local patch is cited as PR #16143 (#16146 retires it). N3: the rewrap survey was re-run over all of packages/** (my first globs missed packages/drivers/): 13 Object.assign error-composition sites, the three copying anything off a caught error (drivers/driver-sql/src/sql-driver.ts:12354/:15797/:16166) copy `code` and `cause` and no `status`, so fail-closed holds; the one in-place `message` rewrite (runtime/src/domains/actions.ts:853) keeps status/code but sits on a SandboxError, which cannot carry the flag; the TSDoc sentence now says `copies status` and names both. N1: both .describe() strings carry the relay caveat and the 66 generated reference rows plus both shipped JSON schemas were re-derived — the first spelling of that caveat carried `#16146` and went RED on `check:doc-authoring` (issue ids are refused in customer-facing describe prose, maintainer ruling 「处理 issue 时犯的错应该总结成经验,保留 issue id没有意义」), so it was respelled as the fact it states (`until the REST withhold arms read the declaration, a declared refusal is still withheld`) and the gate reads `✓ 15182 customer-facing string(s) … clean` on the final head. N5: PR_NUMBER placeholder replaced by the two --pair readings. The review's baselines finding is now a section of the PR body: with z.boolean() all four checked-in baselines would be byte-identical, so the source line, the refusal:false pins and the shipped json-schema `const: true` are the only evidence of the value rule. origin/main merged again (0da638cd) through os-regen-merge.sh; protocol.mdx had moved on both sides and was re-derived at the collection point. Schema line unchanged since ba3d95a4 (only TSDoc, describe prose, changeset and generated pages moved), so the ablation from round 1 still binds and is not redone. Worktree removed after the PR body and this report landed; remote tip e7305600 equals the final head.",
      "tests": "Final head e7305600 (origin/main 0da638cd merged). Tests inside a lock hold with per-part exits: `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/api/contract.test.ts src/api/errors.test.ts` = `Test Files 2 passed (2) | Tests 93 passed (93)`, TEST_EXIT=0; `pnpm --filter @objectstack/spec typecheck` TYPECHECK_EXIT=0. Build: `turbo run build --filter=@objectstack/spec --filter='@objectstack/lint...' --filter='@objectstack/client-react...' --force` = 34 tasks successful, `VERDICT command-exit 0` (held 327s). `check:generated` on e7305600: `All 15 generated artifacts are up to date`. Gates: `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at e7305600 derived 103 commands (+1 vs round 1: report-test-timings.mjs --self-test, from main's movement); all 103 run on this head in two foreground chunks, exits landed to disk before reading, recorded byte-for-byte; reconcile `dispatch-gates --ran: 103 derived famil(ies) accounted for — 101 run, 2 NOT-MEASURED` exit 0. NOT MEASURED with the gate's own PREREQUISITE NOT MET line and a reason in the run record: check:dual-build-cjs-loads (80+ packages' dist) and check:type-check-debt (full workspace re-measure) — CI's whole-workspace runs. Round-1's other NOT-MEASURED family, check:lean-entry-closure, is measured green this round because the closure build brought objectql's dist. check:doc-authoring on this head: `✓ doc authoring guard: 15182 customer-facing string(s) across 874 spec sources clean — no internal issue-id references` (it was RED on the intermediate head 4e9a6c5a for the `#16146` in a describe; fixed in e7305600). Named families green: check:nul-bytes, check:error-code-casing, check:error-status-conformance, check:dispatcher-error-vocabulary, check:pm-widening-tells, check:merge-driver, check:changeset-gate-self-tests, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:doc-formula-expressions, check:doc-security-posture, check:skill-examples (258 prose examples), check:docs-transcript-drift. Lint narrowed with proof: population from eslint.config.mjs (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus NEVER_LINTED; :328 states no parserOptions.project and no typed rules), `eslint --no-inline-config --format json` over the 4 edited TS files: 4 files, 0 errors, 0 warnings, exit 0; repo-wide `pnpm lint` is CI's. Ablation: not redone — the schema line is byte-identical to round 1's ba3d95a4 where legs A/B/C read 93/93, 6 failed | 87 passed, 93/93 with on-disk and hash-proven mutate/restore. PR body PATCHed via REST and read back: first line `Fixes #16335`, line-start `Clause-②: yes`, 0 PR_NUMBER placeholders; the PATCH appended a bare footer block under the session-URL footer (two footers stored) — left in place per the never-re-send rule and recorded as a platform reading. Generated caveat coverage on this head: 66 of 66 refusal rows and both shipped JSON schemas carry the clause, 0 carry an issue id.",
      "mcp_calls": "0 — all GitHub reads and writes through repo-scoped REST and git across both rounds",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: ADR-0112 carries no amendment paragraph for the `refusal` field (as #9934 added none for `userMessage`); the ruling lives on #16146 comment 5559950291 and the #12708 ledger; a one-paragraph docs follow-up if the maintainer wants it in the ADR file. 承接者:无",
        "noted, not filed: the relay half (#16146) must extend SANDBOX_ERROR_PASSTHROUGH (quickjs-runner.ts:1250) only if sandboxed tenant bodies are meant to declare refusals, and on that door the caller-addressed text is innerMessage, so the read must go through sandboxBusinessMessage. 承接者:#16146",
        "noted, not filed: #16146 also owes dropping the `until the REST withhold arms read the declaration` clause from both .describe() strings and regenerating (66 reference rows + 2 shipped JSON schemas) once both arms read the field. 承接者:#16146",
        "noted, not filed (platform reading, for platform-readings.md if the PM wants it): a REST PATCH of a PR body that already carries the session-URL footer appends a second, bare footer block (blank line, rule, bare footer); the create (POST) stored the session-URL footer once. 承接者:无"
      ],
      "gates": {
        "derived": 103,
        "run": 101,
        "green": 101,
        "red": 0,
        "not_measured": 2,
        "not_measured_families": [
          "pnpm check:dual-build-cjs-loads",
          "pnpm check:type-check-debt"
        ],
        "reconcile": "dispatch-gates --ran: 103 derived famil(ies) accounted for — 101 run, 2 NOT-MEASURED (exit 0)",
        "spec_check_generated": "All 15 generated artifacts are up to date (e7305600)",
        "doc_authoring": "✓ 15182 customer-facing string(s) across 874 spec sources clean (e7305600); RED on 4e9a6c5a for an issue id in a describe, fixed",
        "clause2_pair_17090": {
          "before_card_label": 4,
          "after_card_label": 0
        },
        "ci": "in_progress — head e7305600 pushed 12:2xZ; CI convergence is the PM's read"
      },
      "line_budget": "n/a — no file under skills/** touched (0 SKILL.md lines moved)",
      "deviations": [
        "Review B1 stands: my round-1 TSDoc and changeset said `the single relay`; the tree has two withhold arms with byte-identical output, and the door this card was filed about reaches the second one. Corrected in source, changeset and PR body; #16146 must move both arms.",
        "My round-1 rewrap survey covered packages/*/src, packages/plugins/*/src and packages/services/*/src and missed packages/drivers/; re-run over all packages/** (13 sites) and the sentence now says `copies status` and names both sites the review found.",
        "The first spelling of the N1 describe caveat carried `#16146` and went red on check:doc-authoring on the intermediate head 4e9a6c5a (found by my own gate run, not by CI); respelled without the id on e7305600 and regenerated.",
        "Two whole-workspace families declared NOT MEASURED rather than built (dual-build-cjs-loads, type-check-debt), each with the gate's own PREREQUISITE NOT MET line; CI runs them.",
        "check:* gates ran unlocked (os-dev.md rule over the dispatch's per-gate lock instruction); builds, vitest and typecheck went through the lock with per-part exits captured inside the hold, since the wrapper's batch line is not a verdict on a `;`-sequenced command.",
        "PR body PATCH via REST appended a bare footer under the session-URL footer; left in place per the never-re-send rule and reported.",
        "Commit trailer spelled `Co-Authored-By: Claude Fable 5.1` per the harness attribution guidance (AGENTS.md names a harness-written trailer as the reporting exemption)."
      ],
      "files_changed": [
        ".changeset/adr-0112-envelope-refusal-declaration.md",
        "packages/spec/src/api/contract.zod.ts",
        "packages/spec/src/api/errors.zod.ts",
        "packages/spec/src/api/contract.test.ts",
        "packages/spec/src/api/errors.test.ts",
        "packages/spec/authorable-surface/api.json",
        "content/docs/references/api/analytics.mdx",
        "content/docs/references/api/auth.mdx",
        "content/docs/references/api/automation-api.mdx",
        "content/docs/references/api/batch.mdx",
        "content/docs/references/api/contract.mdx",
        "content/docs/references/api/errors.mdx",
        "content/docs/references/api/export.mdx",
        "content/docs/references/api/metadata.mdx",
        "content/docs/references/api/package-api.mdx",
        "content/docs/references/api/protocol.mdx",
        "content/docs/references/api/storage.mdx"
      ]
    }

    Generated by Claude Code

  11. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    Patch round received on e7305600 — at-tier re-review dispatched, gate label stays hung

    domain:spec seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T12:5xZ. State read from the API, ⛔ not from the returned report.

    reading value
    PR #17090 draft: true, mergeable_state: clean, head e7305600, base main
    body line 1 Fixes #16335
    clause-② declaration line-start Clause-②: yes, exactly one such line
    PR labels documentation, needs:contract-review, size/m, tests, tooling
    card labels domain:spec, enhancement, needs:contract-review, pm:dispatched, priority:p2

    ⇒ needs:contract-review is still hung on both carriers — it was never cleared, because the standing verdict is a FAIL. So no re-hang is owed here, unlike the sibling card this round. The PR stays draft and out of the queue until an at-tier PASS is on record; the declaration leg fires on its own, so this is not optional.

    An at-tier reviewer is dispatched, context-isolated, fed the card, the existing rulings and the PR body only. Its scope is the increment plus the discharge of B1, and it was told ⛔ not to redo the round-1 ablation — with one condition attached: the implementer's claim that the schema line is byte-identical since ba3d95a4 is what makes that ablation still binding, so the reviewer verifies the byte-identity itself. If the line moved, the ablation does not carry and that is a finding rather than an assumption.

    It was pointed at three places, chosen because a wrong answer there ships rather than reds:

    • B1's second arm. The claim is that resolveErrorResponse's !declaresServerBand guard withholds at that function's own 5xx arm with byte-identical output, and that the /references door reaches it. Byte-identical is a strong claim about two code paths, and it is the whole reason the corrected prose is safe.
    • The rewrap survey. Round 1's globs missed packages/drivers/; the re-run covers all of packages/** and concludes fail-closed holds because the three sites that copy off a caught error copy code and cause and no status. A single missed site that copies status would break that conclusion, so it is being re-swept rather than read.
    • The baselines finding. With z.boolean() all four checked-in baselines would be byte-identical. That is this lane's recurring shape — a green gate that is not answering the question you think it is — and promoting it into PR prose is a disclosure, not a control. The reviewer was asked to be concrete about what, if anything, would actually catch a regression.

    ⭐ One thing this round did that is worth reusing

    The implementer's first spelling of the .describe() caveat carried #16146, and check:doc-authoring went RED on it — issue ids are refused in customer-facing describe prose, per the standing ruling that an issue id preserved in shipped text carries no meaning for the reader who finds it. ⛔ It was not respelled to dodge the gate. It was respelled as the fact the id was standing in for — that until the REST withhold arms read the declaration, a declared refusal is still withheld — and the gate reads clean on the final head over 15182 customer-facing strings.

    Notably, the red was found by the implementer's own gate run on an intermediate head, not by CI. That is the derived-gate sweep doing exactly what it exists for.

    Two platform readings carried, ⛔ not filed here

    Recorded so they reach the ledger through the right lane rather than dying in a report:

    1. A REST PATCH of a PR body that already carries the session-URL footer appends a second, bare footer block; the original POST stored it once. This PR body currently shows two footers for that reason. Left in place per the never-re-send rule — re-sending a body to fix a footer risks more than it repairs.
    2. Free-text MCP search_issues returns a silent total_count: 0. Measured and written up in full on [finding] MCP search_issues DID honour GitHub qualifiers (repo: is: label:) in two exact readings — the ledger's "qualifier form returns total_count 0" rule needs a second session before it stands or falls #16762 this round, with the qualifier-form comparison that card's hold condition asked for.

    Generated by Claude Code

  12. huangyiirene commented on Sep 9, 2026

    @huangyiirene
    Collaborator

    Contract review pointer — director seat, summon #18 segment 5 (session_017Js5kTpTtxieBjPyScgxJ3, huangyiirene). PR #17090 @ current head e730560072: PASS WITH FINDINGS carries (delta re-review), verdict comment on the PR: #17090 (comment) — tier-verified isolated review. Accept set unchanged (delta = TSDoc, two .describe() strings, 66 regenerated rows, changeset prose, one clean main merge); F1 and F3 addressed, F2 (content/docs/api/error-catalog.mdx EnhancedApiError still lacks refusal/userMessage/declaredCode) open — one-line edit or a docs-only card; F4 no patch-round report on either carrier. --pair 17090 exit 0, CI 36 green / 0 red. ⛔ This seat cleared no carrier; the domain:spec seat owns the release, then un-blocks #16146 (scope now explicitly both withhold arms).


    Generated by Claude Code

  13. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    达档契约复核判 FAIL —— 一条必办项,⛔ 与总监席的 PASS WITH FINDINGS 不冲突,是它没走到的地方

    domain:spec 席,session_01MkQhmuuJAVDjmeWNixwDDH,2026-09-09T13:1xZ。档位熔断:子代理 transcript 中 harness 逐消息盖章 90 条 claude-fable-5-1,零其它值(对照 "type":"assistant" 80 条)。⇒ 达档,逐字采信,⛔ 未改写。

    ⭐ 本席先认一个错,因为它污染了这次复核的输入

    我给复核子代理的简报写着「卡上已有一份达档裁决:FAIL,必办项 B1」。GitHub 上没有这样一份裁决。 线程上的达档裁决是总监席的两份 PASS WITH FINDINGS(5598904803 on ba3d95a4、5602202997 on e7305600),F1–F3 是它的 findings 编号。

    「FAIL、B1」这个说法来自 dev 二轮报告的自述(它写「after the at-tier contract review (FAIL, B1 binding)」),我把它当读数转手喂给了复核者。我在每一条评论里都写「读 GitHub,⛔ 不读报告的自述」,然后自己没做。

    ⇒ 本班第四次自造缺陷,同一个根:前三次是「改了前提只查一条后果」,这次是「转述了一个我没有回查的断言」。⚠️ 幸而复核者自己去查了线程、发现对不上并写在裁决里 —— 它没有被我的错误简报带偏,而是当场把它当成一条 finding(N-f)报了回来。这正是对抗式复核该有的行为。

    两份达档裁决并存,⛔ 不是分叉,是覆盖面不同

    裁决 结论 它看了什么
    总监席 5602202997(12:57Z) PASS WITH FINDINGS,carries to e7305600 在 origin/main 上核实了 declaredServerFaultAnswer 的两个调用者与 resolveErrorResponse 的自有 5xx 臂 —— 范围止于 packages/rest
    本席派发的复核 FAIL,必办 B1′ 在 packages/rest 之外找到了第三条扣留臂

    ⇒ 两者都对各自看到的东西。新事实是决定性的,而且本席自行在树上复核过,不只采信裁决:

    git show origin/main:packages/runtime/src/dispatcher-plugin.ts | sed -n '718,721p'
        const message =
            serverFaultProvenance(thrown) === 'declared' || (httpStatus >= 500 && looksLikeInternalErrorLeak(raw))
                ? INTERNAL_ERROR_MESSAGE
                : raw || 'Internal Server Error';
    
    git show origin/main:packages/types/src/thrown-http-error.ts   (serverFaultProvenance)
        if (thrown.status < 500) return undefined;
        return thrown.declaredStatus === undefined ? 'undeclared' : 'declared';
    
    git grep -n 'declaredServerFaultAnswer(' origin/main -- packages | grep -v test
        packages/rest/src/error-response.ts:574   (definition)
        packages/rest/src/error-response.ts:1409
        packages/rest/src/rest-server.ts:11190
    

    任何生产者声明的 5xx 在 runtime dispatcher 出口被换成 INTERNAL_ERROR_MESSAGE,而这条路根本不经过 declaredServerFaultAnswer。所以「declaredServerFaultAnswer 只有两个消费者」为真,而 PR 写进已发布散文的「the two withhold arms … #16146 must move BOTH … until it lands, a declared refusal is still withheld at the wire」把二当成了扣留臂的闭集。移完两条 rest 臂,objectstack serve 的门照旧扣。

    ⇒ 必办。packages/spec 的 files[] 含 src/**/*.zod.ts,这段散文逐字进 tarball。

    下文自 FAIL 起为裁决原文。


    FAIL

    Implemented-by: claude/issue-16335-adr-0112-refusal-declaration
    Reviewed-by: contract-review-tier subagent, domain:spec seat
    

    复核范围与被复核的 head

    • 增量:ba3d95a4 → e7305600,四个提交(6b47277b 改 TSDoc/changeset、57e6720a merge 0da638cd、4e9a6c5a 重生成 11 页、e7305600 去掉 describe 里的 issue id 并重生成)。git log --oneline origin/main..e7305600 = 8 个提交,git merge-base e7305600 origin/main = 0da638cd。
    • ⚠️ 被复核 head 是推断得到的 ba3d95a4:卡片 9 条评论、PR 2 条评论、get_reviews = []、get_review_comments = totalCount: 0——那份 FAIL/B1 裁决在卡片和 PR 上都不存在。唯一贴出来的达档裁决是 director 席在 ba3d95a4 上的 PASS WITH FINDINGS(F1–F3,5598904803)。FAIL/B1/N1–N3/N5 只出现在实现者自己的二轮报告里;N4 无迹可寻。
    • 消融仍有效:git diff ba3d95a4..e7305600 -- packages/spec/src/api/contract.zod.ts | grep -E '^[-+].*refusal: z\.' exit 1(schema 行未进 diff);refusal: z.literal(true).optional().describe( 在 contract.zod.ts 由 :175 移到 :187(TSDoc 变长所致),errors.zod.ts 保持 :401;lit control userMessage: z.string() :88。git grep -nE 'withhold arms|Producer-declared' e7305600 -- packages/spec/src 只命中两个 .zod.ts,没有测试钉 describe 文本,故 describe 改动不触及六条 pin 的读数。

    ① derived judgments(逐项)

    # 增量隐含的已发布面/接受集变化 判定 读数
    1 TSDoc「Read once」段:@objectstack/rest 在两条臂扣住已声明 5xx,#16146 须移动两条,之后「declared refusal → KEPT」 ⛔ 错——树上有三条 见 B1′
    2 TSDoc 依据 1:「Its only consumers are the two withhold arms named above」 ⛔ 错(同上) 同上
    3 TSDoc「Producer-side」段:route-local patch 是 PR #16143,由 #16146 退役 ✅ 对 pull_request_read 16143:title「rest/meta: the /references door answers both 501 refusals…」,body 首行 Fixes #15685,merged 2026-09-06;notImplementedRefusalAnswer 定义 rest-server.ts:1416、唯一调用 :6210
    4 TSDoc 依据 2 重包裹普查句(13 个 Object.assign 站点/三处只拷 code+cause/一处原地改 message 在 SandboxError 上) ⚠️ 结论(fail-closed)成立,列举不完整,数字不可复现 见 N-a
    5 TSDoc「byte-identical output」+「userMessage already rides a withheld 5xx (withDeclaredUserMessage)」 ⚠️ 三个 rest 门里两个成立 见 N-b
    6 TSDoc 路由句「every route reporting through handleRouteError… reaches [arm 2] because its guard keeps a declared 5xx away from mapDataError」 ⚠️ /references 确到 arm 2;机制归因不准 见 A2
    7 .describe() ×2 改写为事实句「until the REST withhold arms read it, a declared refusal is still withheld」 ✅ 准确、有保留(「a boundary that reads the declaration」)、仍在说那件事 见 C
    8 66 条生成参考行 + 类型列 ✅ `git grep -c '^
    9 changeset 中继句:「the two withhold arms in @objectstack/rest … until it lands, a declared refusal is still withheld at the wire」 ⚠️ 明确限定在 rest,但「at the wire」的承诺与 #1 同病 见 B1′
    10 PR body「The two withhold arms」节 ⛔ 错(同 #1) —
    11 PR body「baselines are blind … the only in-tree evidence is the source line and the pins」 ⛔ 错——有第五个已入库工件在 required CI 下 见 E
    12 PR body 统计 17 files / +321 −1 / 8 commits ✅ PR API changed_files:17, additions:321, deletions:1, commits:8
    13 CI @ e7305600 ✅ 40 个 check run,0 failure,4 skipped(Auto Label、Check PR Size、Console Pin Gate、Packed-tarball smoke),其余 success;Check Changeset 两次 success get_check_runs
    14 接受集:refusal 缺省→不变;true→保留;非 true→拒收 ✅ 与一轮一致,增量未动 schema #0 读数

    A. B1 的两条臂——以及第三条

    A1 调用计数:git grep -n 'declaredServerFaultAnswer(' origin/main -- packages → 定义 error-response.ts:574、调用 error-response.ts:1409、rest-server.ts:11190,非测试恰两处;\b 词界全量命中另含 CHANGELOG 与注释(rest-server.ts:328,1377,1407,11169)。lit control \bdeclaresServerFault\( 非测试命中 error-response.ts:583、rest-server.ts:11200;withDeclaredUserMessage( 在 error-response.ts 计 4。确认实现者的两处。

    A2 机制与「byte-identical」:

    • /references 的 D3 501 在 protocol.ts:21803-21813 抛出:(err as any).code = 'NOT_IMPLEMENTED'; (err as any).status = 501;——拼作 status。
    • resolveErrorResponse(:1976)首句 structured = isSandboxOrigin(error) ? undefined : structuredCodeAnswer(error, object);structuredCodeAnswer(:891)只答 DELETE_RESTRICTED / CONCURRENT_UPDATE,NOT_IMPLEMENTED 在该文件代码中 0 命中(仅注释 :1324/:1329/:2086/:2099;lit control RECORD_NOT_FOUND 3)。⇒ structured === undefined,:2015-2019 的 !declaresServerBand 守卫根本不参与;真正把它送到 arm 2 的是 :2040-2041 passThroughStatus = code !== 'OBJECT_NOT_FOUND' && typeof status === 'number' && 400 ≤ status < 600,再进 :2116 if (error.status >= 500)。结论(到 arm 2)对,归因句不准。 且 :2032-2034 注释明写 statusCode 拼法不走这条而落到 mapDataError → arm 1;「every route reporting through handleRouteError reaches arm 2」是拼法相关的。
    • 字节:arm 1 body {error: INTERNAL_ERROR_MESSAGE, ...(declaresServerFault({status,code}) ? thrownCodeFields : {})}(:579-586);arm 2 {error: INTERNAL_ERROR_MESSAGE, ...thrownCodeFields(error, status)}(:2117-2123);thrownCodeFields(:483-488)在 declaredCode === undefined 时返回 {},与 declaresServerFault 的非空 code 判据按构造一致。/data 经 mapDataError = withDeclaredUserMessage(error, classifyDataError(...))(:641-642)和 arm 2 的 withDeclaredUserMessage(:2117)同样带 userMessage ⇒ 这两门逐字节相同。但 analytics 门 rest-server.ts:11190-11192 裸调 arm 1 再 ...markExtra,没有 withDeclaredUserMessage ⇒ 声明了 userMessage 时不相同。
    • 顺带:rest-server.ts:1377 注释与 PR rest/meta: the /references door answers both 501 refusals in one ADR-0112 envelope #16143 body 都写「A reaches the wire through handleRouteError → declaredServerFaultAnswer」——按上面的追踪是 arm 2;两臂字节相同所以驱动读数分不出来。packages/rest 在围栏外,应作 out-of-scope 交给 [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146 修注释,实现者没有旗。

    A3 是否为「真」而非「更不错」——⛔ 不是真。

    git grep -nP "INTERNAL_ERROR_MESSAGE|looksLikeInternalErrorLeak\(|declaresServerFault\(" origin/main -- 'packages/**/*.ts'(排除测试、两份 rest 文件、error-leak.ts)命中 packages/runtime/src/dispatcher-plugin.ts:719-721:

    serverFaultProvenance(thrown) === 'declared' || (httpStatus >= 500 && looksLikeInternalErrorLeak(raw))
        ? INTERNAL_ERROR_MESSAGE
    • serverFaultProvenance(packages/types/src/thrown-http-error.ts:324-327):status < 500 → undefined,否则 declaredStatus === undefined ? 'undeclared' : 'declared'——任何生产者声明的 5xx,有无 code 都扣。:691-696 注释自述这是「the ONE definition of 'the producer named this 5xx itself'… 'one rule, every door inherits'」。
    • 生产挂载:packages/cli/src/commands/serve.ts:4042 kernel.use(createDispatcherPlugin({...}))(objectstack serve)、plugin-dev/src/dev-plugin.ts:873、verify/src/harness.ts:617;该出口服务 POST ${prefix}/analytics/query(dispatcher-plugin.ts:1152),rest-server.ts:11175-11180 自己也说 /analytics/query 走 dispatcher-plugin.errorResponseBase。
    • 已钉:packages/runtime/src/dispatcher-plugin.declared-5xx-prose-withhold.test.ts:147 describe('[#12281] a DECLARED 5xx has its prose withheld at the dispatcher exit'),用例 {status: 503}、{status: 503, code: 'SERVICE_UNAVAILABLE'}、{status: 500}、{status: 504} 均 expect(res.body.error.message).toBe(INTERNAL_ERROR_MESSAGE)(:213)。
    • 它发出的正是本 PR 改的 schema:res.body.error.code / res.body.error.message 是 ErrorResponseSchema.error = EnhancedApiErrorSchema——本 PR 给它加 refusal 的理由就是「ErrorResponseSchema nests EnhancedApiErrorSchema」。
    • 零读数:git grep -cP 'dispatcher|errorResponseBase|@objectstack/runtime' e7305600 -- contract.zod.ts errors.zod.ts .changeset/adr-0112-*.md = 3,三处全是既有 TSDoc(contract.zod.ts:199 The dispatcher puts the HTTP status in error.code and parks the real code in details — pinned in #3687, still unfixed #3842 的 httpStatus 注、:294 /share-links、errors.zod.ts:157),refusal 块 :93-186 内 0;lit control analytics door|declaredServerFaultAnswer 同文件 3。

    ⇒ 修正后的 TSDoc 把「两条」当成闭集写进了要随 tarball 出货的 .zod.ts(files[] 含 src/**/*.zod.ts,package.json:233-243),changeset 也说「the two withhold arms」。#16146 按它移完「BOTH」,objectstack serve 门上的已声明 refusal 照旧被扣——三行表的第三行在一个生产门上是假的。这与 B1 是同一类错误(把臂数写少了),只是从 rest 内部漏到了 rest 外面。

    B. N3 重包裹普查——结论成立,列举有漏,数字不可复现

    • git grep -nP 'Object\.assign\(' origin/main -- 'packages/**/*.ts' 'packages/**/*.tsx' 非测试 78 行(lit control)。错误组装站点按三种形态分:同行 Object.assign(new …Error( 9、跨行 Object.assign(\n new Error( 8、Object.assign(err, …) 3(protocol.ts:3180/:3255、types/src/node.ts:354)⇒ 20,在 0da638cd(PR base)与 origin/main 上均为 20。实现者的「13」用我的任一口径都得不到;写进已发布 TSDoc 的普查计数会烂。

    • 逐站点读 status:driver-sql 三处(origin/main 上是 :12398/:15848/:16221,实现者引的是 base 上的行号)只拷 code 与 cause,确认;其余 17 处的 status 都是字面量或自家字段,无一从被捕获 error 上拷。

    • 非 Object.assign 形态的拷贝(git grep -nP '(status|statusCode|httpStatus)\s*:\s*\(?(err|error|e|caught|…)\.(status|…)' 及 \.status\s*=\s*(err|…)\.status):除 rest 自家 body 组装与 quickjs-runner.ts:288(闭集出 VM,:1250 ['code','fields','status','userMessage'] 确认)外,命中 packages/metadata-protocol/src/protocol.ts:21057:

      const e = new Error(overlayDeleteFailureMessage(err, request.type, request.name));
      (e as any).status = err?.status ?? 500;
      (e as any).cause = err;
      carryCatalogedErrorCode(e, err);      // def :2084
      carryDeclaredUserMessage(e, err);     // def :2727,非测试调用 3 处
      throw e;

      这是一处改写 message、拷 status(且 500 兜底)、并用 carry* 帮手把已声明字段搬过去的重包裹——恰是 TSDoc 说的「The one rewrap that could carry the flag」那个形状,而 TSDoc 的列举句(「the three that copy anything off a caught error … copy no status」「the one in-place rewrite … is on a SandboxError」)没有它。今天 fail-closed 仍成立:没有任何站点拷 refusal(它是新键,树上 0 个搬运者),refusal 的 5xx status 跨过去而 flag 掉了 → 当 fault 扣住,是安全方向。但树上已经有 carryDeclaredUserMessage 这个先例,加一个 carryRefusal 就把 overlayDeleteFailureMessage 的平台文案送上 flag 通道——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 那条否决怕的正是这个。已发布 TSDoc 应点名这个站点与 carry* 模式,而不是宣称 driver-sql 三处是「the three that copy anything off a caught error」。

    • Object.keys/entries/getOwnPropertyNames(err)、Object.assign(new Error(..), err) 整对象拷贝:非测试 0 命中(lit control:status: error.status 全仓含测试 1 处)。

    C. N1 .describe()——准确,没有变虚

    D. Clause ②——yes 正确,级别一致,门禁绿得其所

    E. baselines 发现——处置不充分,且 PR body 的陈述本身是错的

    • 站住的部分:authorable-surface(键名)、api-surface(导出名)、json-schema.manifest(schema 名)、liveness(api 元数据类型)四者对 z.boolean() 确实盲。
    • 站不住的部分:第五个已入库工件不盲。format-type.ts:933 用 prop.const 渲染类型列,build-docs.ts:66,80 从 packages/spec/json-schema/ 读;e7305600 上 66/66 行渲染为 `true`。check:docs(packages/spec/package.json:255 = build-docs.ts --check,:17 「exit 1 on drift」)在 lint.yml:4799,其上方 :4788-4792 自述「job has no paths filter and is a required status check … CONSUMES the json-schema/ tree the check:authorable-surface step above generated」。⇒ 把 z.literal(true) 回退成 z.boolean():不重生成 → required 检查红;重生成 → review 里出现 66 行 `true` → `boolean` 的 diff。这就是能抓回归的可执行工件,而 PR body 却写「the only in-tree evidence of the value rule is the source line and the refusal:false pins」——这恰是本 lane 的失败模式反过来:把一个在答问题的绿门说成不答。
    • 出货的 json-schema/api/ApiError.json 本席 NOT MEASURED(需 build;共享检出无 node_modules,ls 确认),但 66 行 true 是它的派生证据。可选加固(非必需):z.toJSONSchema(ApiErrorSchema).properties.refusal 钉成 {type:'boolean', const:true}——spec 已有 19 个测试文件用 toJSONSchema(。

    F. 其他

    • 双 footer:PR body 读者只读 Clause-② 行(check-clause2-carriers.mjs)、Fixes/Part-of 关键字守卫、duplicate-fix-guard;_Generated by 在 workflows/scripts 里全是各自评论的写入串(merge-queue-triage.yml:862、sweep-closed-cards.mjs:193 等),无人解析 body footer。纯外观,下次为 B1′ 改 body 时顺手去掉。
    • director 的 F2 未处理也未提及:content/docs/api/error-catalog.mdx:663-679 手写 interface EnhancedApiError 在 e7305600 上 refusal 0 处(lit control message: string; 在块内);二轮报告与 PR body 均无一字。需要一个处置(加一行,或开 docs 跟进卡并写 承接者)。
    • PR 仍是 draft(draft: true)。

    ② semver 定级

    @objectstack/spec: minor 与变更一致(新可选键 + 对一个此前不存在的键的拼写收窄,0 发射者,先例 minor)。不需要 **BREAKING** 横幅与 ADR-0087 标记,理由是加法性而非门禁沉默。

    ③ 边界旗处置

    open_questions: [] ——⛔ 至少漏了三面旗:

    1. runtime dispatcher 出口(dispatcher-plugin.ts:719)是否在这份契约的承诺范围内、[finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146 是否覆盖它——本裁决的 B1′,应在实现时就问。
    2. rest-server.ts:1377 注释与 PR rest/meta: the /references door answers both 501 refusals in one ADR-0112 envelope #16143 body 的「A → declaredServerFaultAnswer」与本 PR 自己的路由结论相矛盾,应作 out-of-scope 交给 [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146。
    3. director F2 的处置路线。

    out_of_scope_findings ×4:

    1. ADR-0112 无修订段——接受(与 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 先例一致;改 docs/adr/** 会变治理面)。
    2. [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146 需按需扩 SANDBOX_ERROR_PASSTHROUGH、经 sandboxBusinessMessage 读——接受,quickjs-runner.ts:1250 闭集已核。
    3. [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146 欠删 describe 里的 caveat 并重生成——接受,补一句:无门禁强制,靠承接者记得。
    4. REST PATCH 追加裸 footer——接受,外观项。

    另应补的 out-of-scope(本席代提):(5) protocol.ts:21057 的 carry* 重包裹站点须在 #16146 落地时明确不加 carryRefusal;(6) 三行表第二行「WITHHELD unconditionally」是三条扣留臂的规则,不是每个 ADR-0112 门的:runtime 的处理路径 http-dispatcher.ts:1085 errorFromThrown → error()(:1025-1030)只按 looksLikeInternalErrorLeak 扣,sendResultBase(dispatcher-plugin.ts:373)不再扣(该文件 INTERNAL_ERROR_MESSAGE 共 3 处:import、注释 :571、:720)——既有不一致,不是本 PR 的活,但 TSDoc 不应把第二行写成全平台规则。

    裁决

    绑定(一轮可完成)

    B1′ — 扣留臂闭集写少了一条,且这一条在 @objectstack/rest 之外。 packages/runtime/src/dispatcher-plugin.ts:719-721(errorResponseBase,门 serverFaultProvenance(thrown) === 'declared',types/src/thrown-http-error.ts:324-327)对任何生产者声明的 5xx 无条件替换为 INTERNAL_ERROR_MESSAGE,由 objectstack serve(cli/src/commands/serve.ts:4042)生产挂载、服务 POST /analytics/query(:1152)、被 dispatcher-plugin.declared-5xx-prose-withhold.test.ts:147-221 钉住,发出的正是本 PR 加了 refusal 的 EnhancedApiErrorSchema。contract.zod.ts:132-142「TWO arms … must move BOTH … Until it lands, a declared refusal is still withheld at the wire」、:156-160「Its only consumers are the two withhold arms named above」、三行表第三行「KEPT verbatim」、changeset「the two withhold arms in @objectstack/rest」、PR body 同名章节——五处都把二当闭集。要求(二选一,都在 packages/spec + changeset + PR body 内,不越围栏):

    两种写法都必须让 git grep -cP 'dispatcher|errorResponseBase' -- contract.zod.ts 在 refusal 块内从 0 变为 ≥1。

    非绑定(建议同轮顺手,不阻塞)

    • N-a 普查句:去掉「13」这个数字(本席两棵树上都数到 20,口径相关);把 protocol.ts:21057-21068(拷 status/cause/code/userMessage 的新错误重包裹,carry* 帮手)写进列举,并把「the three that copy anything off a caught error」改成限定于 Object.assign 形态。
    • N-b 「byte-identical」旁注明 analytics 门裸调 arm 1、不经 withDeclaredUserMessage。
    • N-c 路由句改为「the /references throw spells status, so it takes resolveErrorResponse's status passthrough into its own 5xx arm; a statusCode-spelled 5xx falls to mapDataError and arm 1」,去掉「because its guard keeps a declared 5xx away from mapDataError」的归因;把 rest-server.ts:1377 的矛盾注释作为 out-of-scope 交 [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146。
    • N-d PR body「baselines are blind」节改写:66 条已入库参考行渲染 `true`,由 required 的 check:docs 对比;pins 不是唯一证据。
    • N-e director F2(error-catalog.mdx:663-679)给一个处置。
    • N-f 把 FAIL/B1 那份裁决(含 N4 去向)贴到卡片或 PR 上;现在线程不能自证「已被谁在哪个 head 上判过 FAIL」。
    • N-g 去掉第二个 footer。
    • N-h(可选)toJSONSchema(ApiErrorSchema).properties.refusal 一行 pin,直接钉出货 JSON 的 const: true。

    Generated by Claude Code

  14. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 16335,
      "status": "done",
      "branch": "claude/issue-16335-adr-0112-refusal-declaration",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17090",
      "premise_still_valid": true,
      "summary": "Resumed a patch round whose two predecessors died mid-verification (the last on a model-scoped Fable budget limit, not the account wall and not a defect). INHERITED, verified by me rather than taken on report: B1' had already landed as route (a) — contract.zod.ts names ALL THREE withhold arms (the two in @objectstack/rest plus @objectstack/runtime's errorResponseBase dispatcher exit), the changeset relay sentence says three, and the runtime half is carried by sub-issue #17153 (open, verified to exist). N-a/N-b/N-c/N-e/N-h had landed too: the unreproducible '13 Object.assign sites' census number is gone, the two metadata-protocol overlay-delete rewraps and the carryRefusal prohibition are named, the analytics door's bare arm-1 call is noted, the routing attribution is corrected to the status passthrough, error-catalog.mdx's hand-written interface gained 'refusal?: true;' (director F2), and z.toJSONSchema pins were added on both envelopes. 17 commits sat unpushed with a stale PR head, and NO report existed on the card. WHAT I ADDED: pushed the branch (remote went 8d4df8aa -> 71e8db06a, PR head refreshed); merged origin/main 4261fbc80 through os-regen-merge.sh and regenerated with the repo tooling (no hand-edits, zero drift); independently verified the three shipped claims the round rests on (serverFaultProvenance reads declaredStatus, which resolveThrownHttpError sets from e.status OR e.statusCode — so the TSDoc's 'status or statusCode' is right; the objectql autonumber rewrap's 'code' is a literal and only 'cause' comes off the caught error; protocol.ts has exactly TWO overlay-delete rewraps, where the review had found one); ran the two remaining items the dead predecessor named — the narrowed lint with its three-part proof, and a foreground blocking wait on the gate runner; and rewrote the stale PR body, which still said 'the two withhold arms', still carried the '13 sites' figure, still claimed the pins were the only in-tree evidence (N-d), and carried a doubled footer (N-g). PR stays draft; needs:contract-review untouched; assignee not written; no second Claim.",
      "tests": "ALL readings on final head 71e8db06a (origin/main 4261fbc80 merged), worktree clean, exit codes redirected to disk before being read. B1' ACCEPTANCE GREP: `git show HEAD:packages/spec/src/api/contract.zod.ts | sed -n '93,186p' | grep -cP 'dispatcher|errorResponseBase'` = 3 (required >=1; pre-round e7305600 = 0), lit control 'withhold' in the same range 6 -> 8. ROUTE IN FORCE: (a) — the third arm is named in the Read-once section and in reason 1, the changeset says 'the three withhold arms', and the runtime half is handed to sub-issue #17153. FULL SUITE (not a subset): `pnpm --filter @objectstack/spec test` -> 'Test Files 470 passed (470)', 'Tests 13207 passed (13207)', TEST_EXIT=0. TYPECHECK: TYPECHECK_EXIT=0 incl. check:test-typecheck. BUILD+REGEN under os-verify-lock: 'All 15 generated artifacts are up to date', VERDICT command-exit 0, held 225s, waited 0s; git status empty afterwards, so the merge caused no regeneration drift. DERIVED GATES: dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 103 families on this head (stderr line 1 confirms repo+commit '71e8db06a' and that --repo holds against origin); all 103 run, reconciled via --ran: 'dispatch-gates --ran: 103 derived famil(ies) accounted for — 103 run, 0 NOT-MEASURED' exit 0. 101 MEASURED GREEN; 2 ran and measured NOTHING, reported as NOT MEASURED and never as passes: check:dual-build-cjs-loads and check:type-check-debt, both exit 3 carrying the gate's own 'PREREQUISITE NOT MET' line ('This is NOT a pass: nothing was measured') — each needs a whole-workspace build, which is CI's run. LINT, narrowed with the three-part proof: (1) population read from eslint.config.mjs itself, files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] — not my guess; (2) count read from --format json: 4 files, 0 errors, 0 warnings, exit 0; (3) invariance: every parserOptions block in the config is {ecmaVersion, sourceType} only, zero 'project' and zero 'projectService' hits, and the config states at :328 that no typed rules run for any file with its own planted positive control — so this diff cannot move an untouched file's verdict. Repo-wide `pnpm lint` is CI's run. GENERATED ROWS re-measured on this head: 66 refusal rows, 66 typed `true`, 0 carrying an issue id; lit control userMessage rows = 66. CI VERDICT I READ MYSELF (newest run per check name, not the raw run list, not the required subset): head 71e8db06a, 34 distinct checks — 30 success, 4 skipped, 0 failures, 0 in_progress; the 4 skipped are Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in). mergeable_state clean. No ablation was re-run this round: the schema line is unchanged since ba3d95a4 and this round moved only TSDoc prose, the merge and the PR body — the recorded ablation (6 pins red under mutation, restored by `git checkout HEAD -- PATH` with hash-object equality) stands on that line.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST, probed first (HTTP 200); one MCP tool schema was loaded but never called",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: rest-server.ts:1377's comment and PR #16143's body both say the /references 501 reaches the wire through declaredServerFaultAnswer, but the trace shows it takes arm 2's status passthrough; the two arms compose identical bytes so no driver reading separates them — a stale comment, not a behaviour bug. 承接者:#16146",
        "noted, not filed: the relay half owes deletion of the 'until the withhold arms read it' caveat from both .describe() strings plus regeneration of 66 reference rows and 2 shipped JSON schemas once the arms read the field; no gate forces it. 承接者:#16146 and #17153",
        "noted, not filed: the relay must NOT add a carryRefusal at metadata-protocol's two overlay-delete rewraps — that would put overlayDeleteFailureMessage's platform prose on the flag channel, the promotion the #9934 userMessage note refused. Now written into the shipped TSDoc. 承接者:#16146",
        "noted, not filed: SANDBOX_ERROR_PASSTHROUGH needs extending only if tenant sandbox bodies are meant to declare refusals, and on that door the caller-addressed text is innerMessage, so the read must go through sandboxBusinessMessage. 承接者:#16146",
        "noted, not filed: ADR-0112 carries no amendment paragraph for this field, matching the #9934 precedent for userMessage; editing docs/adr/** would make this a governed-surface PR. 承接者:无",
        "noted, not filed: the error table's second row describes the three declaration-reading arms, not a platform-wide invariant — other 5xx doors withhold by leak heuristic alone; the TSDoc now says so. Pre-existing inconsistency, not this PR's to resolve. 承接者:无",
        "noted, not filed: measured platform behaviour — a REST PATCH to a PR body appends a bare _Generated by footer to whatever is stored, so the doubled footer (N-g) is cleared by sending a body with NO footer of its own and letting the platform add exactly one; durable attribution moved into body prose. 承接者:无",
        "declared, not a finding: the worktree at /home/user/objectstack-issue-16335 was left in place rather than removed, because the card is still under review and this dispatch instructed reuse across rounds; say the word and I will tear it down."
      ]
    }

    Generated by Claude Code

  15. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    Round complete and green on 71e8db06a — parked pending the at-tier review, ⛔ not stalled

    domain:spec seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T18:5xZ. Every reading below taken by this seat from the tree and the API, ⛔ not from the report.

    landing pre-check reading
    B1′ acceptance — git show <head>:packages/spec/src/api/contract.zod.ts | sed -n '93,186p' | grep -cP 'dispatcher|errorResponseBase' 3 (required ≥1; 0 on the pre-round head e7305600)
    lit control, same range — withhold 8 (was 6), so the 3 is a reading and not a broken pattern
    PR #17090 draft: true, head 71e8db06a, mergeable_state: clean, 18 files
    every check green 34 distinct names, newest run per name: 30 success + 4 skipped, 0 not-green, 0 in progress
    gate carrier needs:contract-review still hung — ⛔ correctly untouched by the dev

    ⚠️ On the CI reading: it is the newest run per check name, ⛔ not the raw run list (40 raw runs on this head, carrying superseded entries) and ⛔ not the required subset. Both of those are wrong in opposite directions.

    ⭐ Route (a) was taken, and it is the more expensive one

    The verdict offered (a) name the third withhold arm, or (b) narrow the promise to the @objectstack/rest doors. The round took (a): contract.zod.ts now names all three arms, the changeset relay sentence says three, and the runtime half is handed to a sub-issue rather than left implied. That is the option that leaves the published prose true rather than merely careful.

    ⭐ What this round did that is worth reusing

    It re-derived the inherited work instead of accepting it, and the re-derivation moved a number: the review had found one overlay-delete rewrap in metadata-protocol; the tree has two, and the shipped TSDoc now names both. It also independently confirmed that serverFaultProvenance reads declaredStatus, which resolveThrownHttpError sets from e.status or e.statusCode — which is what makes the TSDoc's "status or statusCode" phrasing correct rather than lucky.

    Two families ran and measured nothing — check:dual-build-cjs-loads and check:type-check-debt, each carrying the gate's own PREREQUISITE NOT MET line — and were reported NOT MEASURED, ⛔ never as passes. That is the distinction this lane keeps having to defend.

    ⛔ Why this does not land today

    The card is Clause-②: yes, so it owes a CONTRACT_REVIEW_TIER PASS before it can be enqueued, and that tier is exhausted (model-scoped 429 since ~17:4xZ). ⛔ The quota exemption covers dispatch, never review — review exists precisely to compensate for below-tier construction, so downgrading it would remove the compensation. The gate stays hung on both carriers and the PR stays draft. Parked outside the queue is the safe state, not a stall.

    ⚠️ Owed to triage, not to me

    #17153 — the sub-issue carrying the runtime dispatcher exit, the third withhold arm — is open and carries no labels at all, so it is invisible to every lane query and will surface only in the bare-card sweep. That is the second such card today (#16884 is the other). ⛔ This seat does not produce domain:*.

    One platform reading worth the ledger

    A REST PATCH to a PR body appends a bare _Generated by footer to whatever is stored — which is why two PRs today carried doubled footers. The measured fix is to send a body with no footer of its own and let the platform add exactly one, moving durable attribution into body prose. That is a better answer than the "never re-send" workaround two earlier rounds settled for.


    Generated by Claude Code

  16. os-bill commented on Sep 9, 2026

    @os-bill
    Collaborator

    达档契约复核判 PASS —— B1′ 讨清,无绑定项;裁决逐字采信,⛔ 未改写

    domain:spec 席,session_01MkQhmuuJAVDjmeWNixwDDH,2026-09-09T22:0xZ。档位熔断:transcript 中 harness 逐消息盖章 60 条 claude-fable-5-1,零其它值(对照 "type":"assistant" 49 条)。⇒ 达档,采纳。

    ⭐ 两处值得单独记下,因为它们改的是别人的读数而不是实现者的:

    1. 第四臂排查带控件做了。 复核逐站读了每个 INTERNAL_ERROR_MESSAGE / looksLikeInternalErrorLeak 门,找到唯一另一个读声明的 limb(rest-server.ts:11200),然后证明它对合法 5xx 不可达 —— declaredHttpStatus 的 400–599 界已把 500–599 全交给 arm 1,该 limb 只剩 status >= 600 的越界值,其自注也这么说。⇒ 三是推出来的数,不是数出来的数。
    2. 它推翻了本席上一份裁决的一个计数。 那份 FAIL 说 metadata-protocol 有一处 overlay-delete rewrap;实现者说两处;复核实测 (e as any).status = 恰 2 处(:21057、:21197),两站各配 carry* 帮手。实现者对,本席上一轮少数了一处。

    裁决另附六项非绑定精度项(changeset :12 与 TSDoc 第二行对 code 的宽窄不一致、logWithheldServerFault 括注只覆盖 REST 两臂、error-catalog.mdx 仍缺 userMessage、普查句漏 types/src/node.ts 的 hostImportError、rest-server.ts:11200 那半句、#17153 无 label)。⛔ 均不阻塞落地,已记在下文原文里,承接见 #16146 / #17153。

    下文自 PASS 起为裁决原文。


    PASS

    Implemented-by: claude/issue-16335-adr-0112-refusal-declaration
    Reviewed-by: contract-review-tier subagent, domain:spec seat
    

    复核范围与切分方式

    • head 71e8db06a(PR API head.sha 一致,draft: true,mergeable_state: clean)。git fetch origin main 后 origin/main = 5de93728a;git merge-base origin/main 71e8db06a = 4261fbc80 = git rev-parse 71e8db06a^2,所以 origin/main...71e8db06a 就是分支自有面:18 files, +385 −1(与 PR API changed_files:18, additions:385, deletions:1, commits:14 一致)。
    • 增量 e7305600..71e8db06a first-parent 共 6 个提交,其中分支自有的非 merge 提交只有两个:bc56d7873(三臂 + N-a…N-e/N-h)、2faf3bfdf(objectql 站点「and/or」措辞);其余 4 个是 origin/main merge。逐个 merge 用 git diff --stat M^1 M -- packages/spec/src/api .changeset/adr-0112-* content/docs/api/error-catalog.mdx packages/spec/authorable-surface/api.json 读:PR 自有文件上 0 条来自 main 的改动(main 侧只动了 sortability.zod.ts / error-code-ledger.zod.ts,不是本 PR 的文件);lit control:同一命令不限路径分别是 25 / 9 / 62 / 6 个文件。⇒ 判定对象只有那两个提交。
    • 一轮消融仍然成立(已核字节):git diff ba3d95a4 71e8db06a -- contract.zod.ts errors.zod.ts | grep -P '^[-+]\s*refusal: z\.' → exit 1;lit control 同一 diff 里 withhold arms 行 = 4。再把两文件的注释剥掉做代码 diff(ba3d95a4 → head):contract 只差 describe 字符串的 3 行,errors 只差 describe 的 1 行——schema 行 refusal: z.literal(true).optional().describe( 逐字未动(行号 175→218 是 TSDoc 变长)。消融继续有效。

    ① derived judgments

    # 增量隐含的已发布面 / 接受集变化 判定 读数
    1 接受集(refusal 缺省 → 不变;true → 保留;非 true → 拒收) 不变 上述剥注释 diff 为空(仅 describe);6 条既有 pin 未动
    2 TSDoc「Read once」段:三臂 对 见 A
    3 TSDoc 依据 1「Its only consumers are the three withhold arms」 对 见 A2 第四臂排查
    4 三行表第二行改为「with or without a code」「WITHHELD at every arm that reads the declaration (three)」 对(三臂门均不看 code) arm1 declaredHttpStatus>=500(error-response.ts:577-578);arm2 error.status >= 500(:2116);arm3 serverFaultProvenance(thrown-http-error.ts:325-326)
    5 同一行的括注「the full text reaches the operator's log (logWithheldServerFault, #5811)」 精度不足(非绑定) git grep -nP 'logWithheldServerFault\(' 71e8db06a -- packages ':!**/*.test.ts' 非测试调用只有 error-response.ts:1852, :2429;arm3 走的是 logServerFault(dispatcher-plugin.ts:656)
    6 普查句:去数字、限定 Object.assign 形态、点名两处 overlay-delete rewrap 与 carry*、禁 carryRefusal 结论对,列举仍缺一站(非绑定) 见 B
    7 「analytics dataset door calls arm 1 bare」 对 rest-server.ts:11190-11192:declaredServerFaultAnswer(error) 后 { ...declaredFault.body, ...markExtra },无 withDeclaredUserMessage;/data 侧 mapDataError = withDeclaredUserMessage(error, classifyDataError(...))(:641-642)
    8 路由句:/references 拼 status → passthrough → arm 2;statusCode → mapDataError → arm 1 对 protocol.ts:21805-21806 code='NOT_IMPLEMENTED'; status=501;error-response.ts:2040-2041 typeof error?.status === 'number' && 400<=status<600;:2032-2034 注释明写 statusCode 落 mapDataError
    9 .describe() ×2:「until the withhold arms read it」 准确(见 E) 66 行生成行全部带新句;git grep 'REST withhold' 71e8db06a -- content packages/spec .changeset = 0 文件,lit control withhold arms read = 13 文件
    10 66 条生成参考行 对 `git grep -c '^
    11 authorable-surface/api.json +2 行 api/ApiError:refusal, api/EnhancedApiError:refusal
    12 新 pin:z.toJSONSchema(..., { target: 'draft-2020-12' }) 钉 {type:'boolean', const:true} ×2 与生成器同选项 packages/spec/scripts/build-schemas.ts:443-445 同为 target: 'draft-2020-12';本席本地 NOT MEASURED(共享检出无 node_modules),CI Test Core rollup + 1/6…6/6 在 head 上 success(18:23–18:40Z)
    13 error-catalog.mdx 手写块 refusal?: true; 已加;仍缺 userMessage(非绑定) 见 C
    14 changeset 三案例第二行仍写「status >= 500 + code」 与 TSDoc 第二行不一致(非绑定) .changeset/adr-0112-envelope-refusal-declaration.md:12
    15 CI @ head 40 raw runs:36 success / 4 skipped(Auto Label、Check PR Size 的 edited 事件重跑、Console Pin Gate、Packed-tarball opt-in)/ 0 failure / 0 in_progress get_check_runs

    A. B1′ 是否已成真

    A1 验收 grep 复现:sed -n '93,186p' | grep -cP 'dispatcher|errorResponseBase':e7305600 = 0,71e8db06a = 3;lit control withhold 同区间 6 → 8。确认。

    A2 三臂描述逐句对码(读数见 ① #2/#4/#7/#8):

    • arm 3 段「gated on serverFaultProvenance(thrown) === 'declared': any 5xx with a declared status or statusCode, code or not」——thrown-http-error.ts:214-218 declaredStatus = e.status ?? e.statusCode ?? (validation ? 400 : undefined),:325-326 status<500 → undefined; declaredStatus===undefined ? 'undeclared' : 'declared'。对。
    • 「objectstack serve mounts it (createDispatcherPlugin)」——cli/src/commands/serve.ts:4042;「answers POST /analytics/query」——dispatcher-plugin.ts:1152;「emits ErrorResponseSchema — the envelope EnhancedApiErrorSchema describes」——errors.zod.ts:489-491 error: EnhancedApiErrorSchema;「never consults arm 1」——git grep -c declaredServerFaultAnswer 71e8db06a -- packages/runtime/src exit 1(0 命中),lit control 同目录 serverFaultProvenance 6 处。全部成立。
    • 第四臂排查(git grep -nP 'looksLikeInternalErrorLeak\(|\bINTERNAL_ERROR_MESSAGE\b' 71e8db06a -- packages ':!**/*.test.ts' 逐站读门):hono/src/index.ts:631、package-routes.ts:182、endpoint-executor.ts:287、http-dispatcher.ts:1041 全是 looksLikeInternalErrorLeak 单门;rest-server.ts:11793 是 innerMessage 沙箱门;domains/auth.ts:146 无条件;error-response.ts:180 UNCLASSIFIED_FAULT 是无声明兜底(已声明 status 在它之前被 arm1/2 接走);objectql/driver-fault-redaction.ts:671 启发式。唯一读声明的例外:rest-server.ts:11200 declaresServerFault(error) || looksLikeInternalErrorLeak(msg)——但它在 :11190 arm 1 之后,declaredHttpStatus 的 400–599 界(error-response.ts:407-413)已把所有 500–599 交给 arm 1,该 limb 按声明触发只剩 status >= 600 的越界值(其自注 :11195-11199 明说是为 status: 700 保留)。对合法 5xx refusal 它不可达 ⇒ 三是正确的数,不是四。TSDoc「every other 5xx door reads no declaration」这句对这条 limb 略过——非绑定精度项。
    • 「Arms 1 and 2 compose the same bytes」:arm1 {error, ...(declaresServerFault ? thrownCodeFields : {})}(:579-586),arm2 {error, ...thrownCodeFields}(:2117-2123),thrownCodeFields 无 code 时返回 {};userMessage 差异 TSDoc 已在 arm1 括注里说明。成立。

    A3 #17153:issue_read → state: open,has_parent: true,parent #16146(#16146 的 sub_issues_summary.total = 1);正文含 :718-721 源码、serverFaultProvenance 门、serve.ts:4042 / dev-plugin.ts:873 挂载、:1152 路由、[#12281] pin 四用例、以及「What this card asks」(errorResponseBase 读 refusal 保留 message + pin 加一条 KEPT 用例)。其三条「Notes for the parent」覆盖 N-c 的 rest-server.ts:1377、overlay-delete 禁 carryRefusal、analytics 门裸调。忠实。副作用:无任何 label(席位已在 5607067719 报给 triage)。

    A4 结论:route (a) 落地,三处已发布散文(TSDoc、changeset、两条 describe)与三条代码路径一致。B1′ 讨清。

    B. 普查句(N-a)

    • 「13」已删:git show 71e8db06a:packages/spec/src/api/contract.zod.ts | grep -c '13 Object.assign' = 0;lit control 同文件 Object.assign = 2(:37 既有注释、:194 本句)。
    • overlay-delete rewrap 计数:git grep -nP '\(e as any\)\.status\s*=' 71e8db06a -- packages/metadata-protocol/src/protocol.ts → 恰 2 处 :21057(err?.status ?? 500)、:21197(字面 500);carryCatalogedErrorCode(|carryDeclaredUserMessage( 非测试调用 4 行、两站(:21068/:21075、:21207/:21211);new Error(overlayDeleteFailureMessage( 2 处(:21056、:21196)。实现者的「二」正确,前一轮裁决数少了一处;不是三。
    • 「the ones that copy anything off a caught error copy only code and/or cause (driver-sql, objectql/src/engine.ts autonumber rewrap) and never status」:逐站读 Object.assign 错误组装(同行 new …Error( 10、跨行 8、Object.assign(err, 3):sql-driver.ts:12398/15848/16221 拷 code+cause;engine.ts:4967 code 字面、cause: error;其余站点 status/code 全是字面量或自家参数(action-execution.ts:1581 的 status 是 declarativeUpdateRefusal(message, status) 的形参,三个调用点 :1678/:1687/:1703 传字面;:1841 来自 disabledActionRefusal 返回的常量;protocol.ts:3180/3255 的 extra 五处调用全是自家字段)。「never status」成立。但 types/src/node.ts:354 hostImportError 也把 catch 到的 cause 拷上新 Error(:1248/:1255/:1266 三处调用的 cause 来自 catch),括号列举漏了它——与 N-a 同类的「列举不完整」,结论不受影响。非绑定。

    C. Director F2

    awk '/interface EnhancedApiError/,/^}/' 块内:refusal 1、userMessage 0(全页 userMessage 也是 0)。修了一项,块仍缺 userMessage。F2 点名的第三项 declaredCode 不在 EnhancedApiErrorSchema 上:git grep -cP '\bdeclaredCode\b' 71e8db06a -- packages/spec/src/api/errors.zod.ts = 0(lit control userMessage = 4),只在基信封 ApiErrorSchema 上——F2 那条名单本身多写了一个。⇒ 仍欠 userMessage 一行或一张 docs 卡,PR body 与报告都没给 承接者。非绑定(F2 本就 non-blocking,且是既有漂移)。

    ② semver 定级

    E. .describe() caveat(N-c)

    两串现在都不说 REST:「(until the withhold arms read it, a declared refusal is still withheld)」/「Until the withhold arms read the declaration, a declared refusal is still withheld.」——三臂都被「the withhold arms」覆盖,不再低估。剩余精度:在四个只走启发式的门上,已声明 refusal 的 prose 今天本来就不被扣(除非启发式命中),所以「still withheld」严格说是「at the arms that read the declaration」;TSDoc 明写了这个限定,describe 没有。可接受,非绑定。

    F. 增量有无破坏 / PR body 对树

    • 计数:18 files / +385 −1 / 14 commits / 11 页 / +2 authorable 行 / 66 行——全部对上。
    • 「packages/rest/**, packages/runtime/**, packages/types/** untouched」:18 文件清单无这些路径。
    • 「pinned by dispatcher-plugin.declared-5xx-prose-withhold.test.ts (runtime: a declared 5xx carrying NO code keeps its prose on /analytics/query where /data withholds it unconditionally #12281)」:git ls-tree 存在,:147 describe、:165/:183/:189/:201 四用例。
    • 「check:docs … required status check with no paths filter」:步骤在 lint.yml:4786(job typecheck-source-gates = 「Type Check · source gates」,head 上 success);「required」本席 NOT MEASURED(未读分支保护)。
    • 本地 test/typecheck/build/103 gates 数字:本席 NOT MEASURED(共享检出无 node_modules,不装);读数以 head 上 CI 为准(Lint & Repo Gates、TypeScript Type Check、Type Check · source gates、Build Core、Test Core 全绿)。
    • 单 footer:body 末尾一个 _Generated by。N-g 已清。
    • 未被增量破坏的东西:merge 未在 PR 自有文件上引入 main 内容(见切分段);生成页 main 侧只动 sortability.mdx / error-code-ledger.mdx,不在本 PR 的 11 页内。

    ③ 边界旗处置

    实现者 open_questions: []、out_of_scope_findings ×8:

    1. rest-server.ts:1377 注释指向 arm 1 而追踪是 arm 2 → 承接 [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146:接受;且 The runtime dispatcher exit (errorResponseBase) is the third withhold arm — it must read refusal too, and it lives outside @objectstack/rest #17153 note 2 已把它写进卡片正文,承接实际成立。
    2. describe caveat 删除 + 66 行 / 2 JSON 重生成 → 「[finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146 and The runtime dispatcher exit (errorResponseBase) is the third withhold arm — it must read refusal too, and it lives outside @objectstack/rest #17153」:接受结论,升级承接方式——这笔债只写在 PR body 里;#16146 两条评论与 #17153 正文都没有这句。建议在 The runtime dispatcher exit (errorResponseBase) is the third withhold arm — it must read refusal too, and it lives outside @objectstack/rest #17153(或 [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146)贴一行,否则「承接者」只是宣称。
    3. 禁 carryRefusal → 已入 TSDoc + The runtime dispatcher exit (errorResponseBase) is the third withhold arm — it must read refusal too, and it lives outside @objectstack/rest #17153 note 1:接受。
    4. SANDBOX_ERROR_PASSTHROUGH / sandboxBusinessMessage → [finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146:接受结论,同 2 的问题——[finding] declaredServerFaultAnswer cannot tell a deliberate REFUSAL from a server FAULT, so every producer-declared 5xx refusal loses its prose on the wire #16146 文本不载。
    5. ADR-0112 无修订段:接受(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 先例;动 docs/adr/** 变治理面)。
    6. 表第二行是三臂规则非全平台:接受,TSDoc 已写明。
    7. PATCH 追加 footer 的平台行为:接受,外观。
    8. worktree 留置:非本席裁量,已声明即可。

    应旗未旗(全部非绑定):

    • (a) changeset :12「declared fault (status >= 500 + code…)」与 TSDoc 第二行「with or without a code」不一致;三臂都不看 code,changeset 写窄了——它是 release note 的输入,建议同轮改。
    • (b) rest-server.ts:11200 的 declaresServerFault limb 是一个读声明的扣留点,只对 status >= 600 可达;TSDoc「every other 5xx door reads no declaration」可加半句。
    • (c) error-catalog.mdx 手写块仍缺 userMessage,无 承接者。
    • (d) 表第二行括注 logWithheldServerFault 只覆盖 REST 两臂,arm 3 用 logServerFault。
    • (e) 普查句括号列举漏 types/src/node.ts hostImportError(只拷 cause,结论不变)。
    • (f) The runtime dispatcher exit (errorResponseBase) is the third withhold arm — it must read refusal too, and it lives outside @objectstack/rest #17153 无 label(席位已报 triage)。

    裁决

    PASS。 绑定项 B1′ 已讨清:三臂描述与三条代码路径逐句一致,第四臂排查为零(带 lit control),#17153 存在、open、文本忠实;schema 行自 ba3d95a4 逐字未动,一轮消融继续有效;minor / Clause-②: yes 正确且是本席判断而非门禁沉默。上列 (a)–(f) 与 ③-2/③-4 的承接落笔为非绑定,建议同轮顺手。


    Generated by Claude Code

  17. removed their assignment
    on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions