Skip to content

[Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landed no, #15963 lands yes, and they are the same class #16404

Description

@huangyiirene

Raised by the domain:spec execution PM seat (session session_01T6HeZvT9wdSJD1ZxJb5Eno, seat post #6017), surfaced by the at-tier contract review of PR #16342 (card #15963, verdict adopted verbatim at 5560140069, its advisory A4).

This is escalated, not decided. A repo-wide reading of clause ② is not an execution seat's rule to write. Two landed dispositions now disagree for the same class of change, and the next card of this shape will re-litigate it unless one sentence settles it.

The two dispositions

#14552 → PR #15962 (landed 2026-09-05) #15963 → PR #16342 (at-tier PASS, awaiting landing)
change defineStack's cross-reference refusal gains code: 'STACK_CROSS_REFERENCE_INVALID' / status: 422 the six sibling defineStack refusals gain one STACK_* code each
clause ② no yes
tier default CONTRACT_REVIEW_TIER
registered in ERROR_CODE_LEDGER / StandardErrorCode no no
classification row in dispatcher-error-vocabulary.ts yes yes

Same file, same function, same mechanism, seven raise sites — one of them declared no, six declared yes.

The argument for no#14552's explicit correction, 5553235312

That seat corrected its own dispatch brief with a measurement rather than an opinion:

code hits in StandardErrorCode + ERROR_CODE_LEDGER
OBJECT_OWNERSHIP_CONFLICT (#14367) 0
NAMESPACE_CONFLICT (#14474) 0
STACK_CROSS_REFERENCE_INVALID (#14552) 0
VALIDATION_ERRORpositive control 2
PERMISSION_DENIEDpositive control 2

and named the mechanism at dispatcher-error-vocabulary.ts:642:

the body carries declaredCode: NAMESPACE_CONFLICT beside code: VALIDATION_ERROR. The wire code stays a catalog member; the specific refusal rides alongside it. So the published face is not widened.

Grounded in the maintainer's 2026-08-28 ruling: 「运行时权限/安全行为变更不是条款② …… 条款②只指已发布契约面」.

The argument for yes — the at-tier reviewer of #16342

Measured on a fresh dist: the six STACK_* spellings ship in packages/spec/dist/index.js and index.mjs; nothing reaches .d.ts (0, with defineStack at 17 files as the lit control); the classes are not exported; check:api-surface unchanged. Then:

once shipped the six spellings cannot be renamed without breaking that consumer. Identifiers a downstream pins are contract whether or not a catalog lists them … My probe confirms that mechanism still holds here (resolveThrownHttpErrorcode: VALIDATION_ERROR, declaredCode: STACK_*). But that argument is about the wire vocabulary at a door, and these refusals have no door (door: 'none'; zero call sites under runtime/src + rest/src, positive control 23 example files) — the thrown value is the boundary, so #14552's reasoning does not transfer.

⭐ The question, stated once

Is "the published contract face" the wire vocabulary, or the thrown value's own fields?

Both readings are honest and each has a landed precedent.

Options

A — the published face is the WIRE vocabulary. An unregistered code on a thrown value is Clause-②: no regardless of door. #15963 is re-declared no (its at-tier review is then belt-and-braces, not owed). Cheap, matches two of three precedents, and keeps CONTRACT_REVIEW_TIER capacity for schema/accept-set changes.
⚠️ Cost: a downstream that branches on e.code from a boot refusal has no protection at all — the codes can be renamed by any later card and nothing would flag it.

B — the published face includes a THROWN value's code/status when they ship in dist. Clause-②: yes for this class. #15963 stands as landed-to-be; #14552's no is recorded as an under-tiering, not re-opened (it merged; the codes it added are now de facto contract either way).
⚠️ Cost: more cards routed to a tier that was quota-exhausted for part of today, for changes whose accept set does not move.

C — split on the door, which is what the two dispositions accidentally encode. door: 'none' (nothing catches it; the thrown value is the boundary) ⇒ yes. A code reachable at an HTTP door, where the demotion to declaredCode runs ⇒ no.
⚠️ Cost: the rule now needs a per-code door reading before the tier can be chosen — but dispatcher-error-vocabulary.ts already requires exactly that reading for its classification row, so the cost may be near zero.
⚠️ And it would make both landed dispositions correct, which is suspicious in a way worth saying out loud: a rule that retroactively blesses everything already done is the one most likely to have been fitted to the data.

四棱分析

实际业务需求 — 服务的是真实场景吗?是,而且可测:六个 STACK_* 码实测进了 dist/index.jsindex.mjs,而卡自陈的消费方是 os validate / os build / host boot 与 AI 作者⚠️ 但同样实测:今天树内 .code 消费者为 0(packages/cli/src 实测),所以拉动是预期的而非既有的 —— 这是本卡最诚实的一条读数,也是选项 A 最有力的支点。

项目长远合理性(权重 ≥50%) — 长远看,有一条规则比规则本身选哪一边更要紧:今天同一个函数的七个出口分成 1 + 6 两种申报,下一张同型卡会再吵一遍,而每次重吵都要花一次 CONTRACT_REVIEW_TIER 或一次误判。contract-first 的取向偏 B/C —— 「声明即强制」意味着一个会随包发布、消费方能钉住的标识符就是契约面;⛔ 但把「面」定义得过宽,会让条款②失去分辨力,那也是长远代价。

防 AI 写代码犯错 — 这一轴指向 B 或 C。一个 AI 作者写 catch (e) { switch (e.code) },拿到六个邻居的 undefined 会读成「这不是校验拒绝」而不是「这条还没有码」—— 那正是 #15963 卡自己记录的缺陷形状。选项 A 让这些码可以被任何后续卡静默改名,而没有任何门禁会响;宽容正是 AI 批量犯错被掩盖的温床。

创业阶段不扩散需求 — 这一轴指向 ACONTRACT_REVIEW_TIER 是稀缺的(今日一度配额耗尽,#16342/#16347 都为此排过队),把不移动接受集的改动送进达档复核,是把稀缺档位花在低风险面上。⚠️ 反向读法同样成立:选项 C 恰好不扩散 —— 它只把 door: 'none' 这一小类送进达档,而那正是没有任何其他保护的一类。

推荐:C,以长远读数领起。 它是唯一让规则跟着机制走而不是跟着先例走的选项:declaredCode 的降级确实保护了有门的码(#14552 的测量是对的),而无门的码没有任何东西保护(#15963 复核的测量也是对的)。两条测量都真,选项 C 是唯一同时容纳它们的读法。
⚠️ 并且我把它最大的弱点写在上面而不是藏起来:C 让两个已落地的处置都变成对的,而「回溯性地祝福一切既成事实」正是最可能被数据拟合出来的规则形状。若维护者认为那个巧合太可疑,A 是更保守的落点,代价是那六个码此后无人守护。

⛔ 推荐是输入,永不是放行。

Governing text

.claude/skills/pm-dispatch/references/contract-review.md(载体纪律与降档保险丝)· scripts/pm/dispatch-gates.mjsSUSPECT_TIER_GLOBS 档头(clause ② 由卡的内容判,路径只是暗示)· 维护者 2026-08-28 「条款②只指已发布契约面」· ADR-0112(code / status 是每个拒绝的机读半边)。

Freshness

Both cited comments re-read in this stroke: #14552 5553235312 (2026-09-05T16:37:01Z, last comment on that card is the 2026-09-06T01:58Z sweep) and #15963 5560140069 (2026-09-06T15:1xZ) plus my own correction to it 5560154218. ⛔ Nothing here is quoted from memory.


Generated by Claude Code

Activity

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