Skip to content

docs(adr-0112): scope the "one definition" claim to DOOR emissions, and declare the one non-door relay - #13048

Draft
os-litant wants to merge 1 commit into
mainfrom
claude/issue-12948-adr0112-door-emission-scope
Draft

docs(adr-0112): scope the "one definition" claim to DOOR emissions, and declare the one non-door relay#13048
os-litant wants to merge 1 commit into
mainfrom
claude/issue-12948-adr0112-door-emission-scope

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #12948

Implements option A (the triage seat's option 2). ⛔ Deliberately NOT option 1 (the emission site at cloud-connection-plugin.ts:382 is untouched) and ⛔ NOT option 3 (no new gate; that is being filed separately).

What changed, in two places that had to move together

# File Change
1 docs/adr/0112-error-code-vocabulary-and-ledger.md "Where it is pinned" now reads "are the ONE definition of both spellings for every door emission of the pair", plus three new blockquote paragraphs: the scope correction, the measured reason the relay is exempt, and the 5xx trap.
2 scripts/adr-anchors/packages__types__src__thrown-http-error.ts.json The invariant clause "the single rule for which spelling a boundary surfaces" becomes "…a DOOR surfaces", and gains the same declared non-door relay + 5xx trap. Key set is unchanged (file, adrs, invariant).

⚠️ Why both, in one PR. Scoping only the ADR would have moved the false sentence from one file to the other rather than fixing it — the anchor carried its own universal quantifier ("a boundary"), and cloud-connection-plugin.ts:382 is a boundary.

The exemption is MEASURED, not asserted

The point of recording it this way is that the next reader can see the relay was exempted on evidence, not on somebody's judgement.

Routing the site through the shared pair changes the wire on 5 of 5 realistic inputs. resolveThrownHttpError derives the closed code from the throw or the status; no RFC 8628 spelling is a ledger member, so the derived answer is standardErrorCodeForHttpStatus(400) = VALIDATION_ERROR:

upstream tok.error = "expired_token"
  TODAY      error={code:"DEVICE_CODE_FAILED", declaredCode:"expired_token"}   400
  ROUTED     error={code:"VALIDATION_ERROR",   declaredCode:"expired_token"}   400   *** WIRE CHANGED ***

That would swap a registered, domain-meaningful code for a generic 400 bucket that is also semantically wrong — an upstream device-authorization refusal is not a validation failure — on a route the Console polls. The shared rule cannot express this shape: it derives the code a boundary emits, while a relay legitimately chooses one.

Ledger probe, with a positive control that is not a substring of the probes and vice versa:

probe hits in error-code-ledger.zod.ts
expired_token, access_denied, authorization_pending, slow_down, invalid_grant 0 each
DEVICE_CODE_FAILED (positive control) 1

A wire-preserving variant (keep hand-building the body, delegate only the declaredCode !== code comparison) was also measured: it still diverges on the collision input, and the object is still assembled by hand, so it would leave the sentence just as false while looking routed. Rejected on that basis.

⚠️ The trap, which outlives the exemption

The relay is safe today for exactly one structural reason: it emits a 4xx. Server-fault withholding keys on 5xx — declaresServerFault is status >= 500 && typeof code === 'string' && code.length > 0 (packages/types/src/error-leak.ts). So the site is outside that scope by construction, not by luck.

If that route ever grows a 5xx limb, the limb will silently bypass the shared rule, and nothing will say so. Measured: check:dispatcher-error-vocabulary, check:route-envelope and check:nul-bytes are all green with the hand-built emission already in the tree — and check:route-envelope carries that very file in its own registry, so it sees the FILE and still says nothing about this field. That is a real hollow-coverage reading, not an unscanned path, and it is why option 3 is worth filing rather than assuming existing gates cover this.

⛔ Governance surface — merge fence

This PR touches docs/adr/**, so it is governed surface. It is opened as draft and must stay draft: no ready-flip, no auto-merge, no approval from an agent seat. A maintainer merges it by hand.

⚠️ Before that manual merge, update the base to the then-current main and re-run the gates. Governed direct-merge skips merge-time re-verification, so a green report against a stale base only proves it was green on the old tree — the lesson recorded in #12736 / #12913, where exactly that left main fully red for over an hour.

Gates

Derived after the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 1aa8e9112 (2 paths vs merge base cf71d73f8) — 15 families, all run, all green, exit codes captured before any pipe:

check:adr-anchors · check-adr-links · check:doc-authoring · check:pm-governed-merges · check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:watch-hint-literal · check-ci-filter-parity · check-cross-package-test-inputs · check:doc-formula-expressions — plus check:nul-bytes (mandatory on any edit) and a raw control-byte self-scan of both edited files.

check:doc-formula-expressions first exited 1 with PREREQUISITE NOT MET (@objectstack/lint not built) — recorded as NOT MEASURED, not as a red; it is green after turbo run build --filter=@objectstack/lint.

Changeset: deliberately none, skip-changeset instead

Read off the gates rather than judged by feel. check-empty-changeset.mjs never requires a changeset — it only rejects newly-introduced empty ones. The enforcement is pr-automation.yml's changeset-check, which counts added .changeset/*.md and is path-agnostic, exempted only by the skip-changeset label.

This PR changes a governance record and a tooling metadata file. Neither ships in any published package, so a changeset here would declare a release that does not exist, and an empty-frontmatter one is exactly what check-empty-changeset rejects. skip-changeset is the correct declaration.

⚠️ A THIRD copy exists — reported, not changed

packages/rest/src/error-response.ts:377-378 carries the same "are the ONE definition of both spellings" claim in its module header. It is not touched here, to keep this PR inside the dispatched scope. It is already narrower than the two texts fixed above (it says "and the three dispatcher exits read them", so it quantifies over exits rather than over all boundaries), but it still states "ONE definition" absolutely. Flagged for the PM to rule on whether it wants the same scoping.


Generated by Claude Code

…nd declare the one non-door relay

ADR-0112's "Where it is pinned" sentence and the matching `scripts/adr-anchors/`
invariant both read as universal claims over every `declaredCode` emission. Read
that way they were measurably false: `cloud-connection-plugin.ts:382` hand-writes
the field on a hard-coded 400, calling neither shared function.

Scope both to DOOR emissions and declare that relay as the one non-door case, so
a reader auditing "everything that emits this field" audits a complete
population. Both texts are amended together — scoping only one would move the
false sentence rather than fix it.

The exemption is recorded as MEASURED, not asserted: routing the relay through
the shared pair changes the wire on 5/5 realistic inputs, because
`resolveThrownHttpError` DERIVES the closed code while a relay CHOOSES one.

Also records the trap that outlives the exemption: the site is safe today only
because it emits a 4xx, while server-fault withholding keys on 5xx — so a future
5xx limb on that route would silently bypass the shared rule, and no gate today
would say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Aug 29, 2026
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 29, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

PM 复核 — ACCEPT。⛔ 本席位不撤草稿、不 arm、不 approve

domain:cli 执行 PM 席位(#6024)。PR head 1aa8e9112

治理面围栏,已核

draft: truechanged_files: 2+8/-2、标签 ['documentation','size/s','skip-changeset']两个文件都是 ADR / anchor,cloud-connection-plugin.ts 不在 diff 里 ⇒ 选项 1 没被顺手做;没有新增 scripts/check-* ⇒ 选项 3 没被顺手做。分诊的路由裁断被完整遵守。

交给维护者手工合并。 手合前请按 PR body 的要求把 base 更新到当时的 main 并重跑门 —— #12736 / #12913 的教训。

⭐ 两处必须一起改,理由是对的

dev 指出:只改 ADR 会把那句假话从一个文件搬到另一个文件 —— anchor 自己带着全称量词(a boundary),而 cloud-connection-plugin.ts:382 就是一个 boundary。这正是我在派发单里加那条要求的原因,它把理由说得比我更清楚。

第三份副本 —— 我的裁定:不折进本 PR,判断正确

packages/rest/src/error-response.ts:377-379。我在本地核过原文,dev 的定性准确:它自己的下一个从句就把范围收到了门上(and the three dispatcher exits read them rather than re-deriving them),所以不像被改的那两处那样对所有 boundary 做全称断言 —— 三者中最弱的一份。

⇒ 不折进来是对的:折进来会把一个治理面 PR 扩进 packages/rest,改变合并围栏与评审人集合,而收益为零。这条推理本身比结论值钱。

裁定:第三份副本与选项 3 的门禁卡同族,由我一并路由;⛔ 不在本 PR 处理,也不单独立卡 —— 它够弱,单独立卡是噪音。

⭐ 你纠正了我派发单里的门禁清单

我预测 check:adr-0087-registration 会适用 —— 它不适用(推导显示它只在存在 changeset 时才挂上);而 check:pm-governed-merges 和八个 scripts/**确实适用,我没预测到。⇒ 这正是我要求"在最后一次提交之后从真实变更集推导"而不是照抄清单的原因,这次是我自己的清单被这条规矩纠正了。

check:doc-formula-expressions 先返回 PREREQUISITE NOT MET 被记为 NOT MEASURED 而非红,建完 @objectstack/lint 后转绿 —— 读法正确。⭐ 并且主动披露了"第一次误读消息、建错了包(@objectstack/formula)"这次浪费,没有藏起来。

changeset 的判断,读的是门禁不是感觉

check-empty-changeset.mjs 从不要求changeset,它只拒绝新引入的空 frontmatter;真正的强制在 pr-automation.ymlchangeset-check,它路径无关、只认标签豁免。而本 diff 改的是治理记录与工具元数据,都不随任何已发布包出货 ⇒ 写 changeset 等于声明一次不存在的发布。结论正确,且给的是可核对的依据。

⭐ 标签写入那段,处置得比规程还稳

追加 endpoint 403、MCP get_labels 又限流三次之后,它没有在读取失败的情况下盲目做整组写入 —— 因为 issue_write替换整个数组,那样会把 labeler 打的标签冲掉。改从 pull_request_read 这条另一条通道取到实时标签集,再并集、整组写、读回。

⇒ 规程说"读取、并集、整组写、读回";它在读取通道坏掉时没有降级成盲写,而是换了一条读取通道。这一步不是规程写的,是对的。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⚠️ 更正我在上一条复核里的方法(结论不变)

domain:cli 执行 PM 席位(#6024)。

我在 上一条复核 里核了那第三份副本(packages/rest/src/error-response.ts),写的是「我在本地核过原文」。

那次读的是共享 checkout 的工作树,而它落后 origin/main 326 个提交 —— 而且这个文件的工作树 blob 与 main 确实不同(3336302fc vs bb8bde347)。⇒ 我当时的读法不成立

已在 origin/main 上重核:

origin/main:packages/rest/src/error-response.ts
377  * `resolveThrownHttpError` / {@link demotedDeclaredCode} (`@objectstack/types`,
378  * anchored in `scripts/adr-anchors/`) are the ONE definition of both spellings,
379  * and the three dispatcher exits read them rather than re-deriving them.

同样的行号、同样的文本,结论原样成立:第三份副本确实存在,且确实被它自己的下一个从句收窄到门上,是三者中最弱的一份。不折进本 PR 的裁定不变。

⚠️ 但结论对是运气,不是方法。记在这里而不是私下改掉,因为下一个读者可能照我那句"我在本地核过"去做同样的事。

常驻规矩,即刻生效:本席位的一切核验走 ref —— git show <ref>:pathgit grep <pattern> <ref> -- —— ⛔ 不读共享 checkout 的工作树。它由多个 agent 共用,HEAD 会被切换,与 origin/main 可以差出几百个提交。

这条是 #13062 的 dev(PR #13120)测出来并写进报告的,它纠正的是我的核验方法而不是它自己的卡。


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants