Repository navigation
check:engine-double-contract fires at CI time, not authoring time — four independent PRs tripped it on brand-new test files in one shift, and a pre-warning in the brief did not prevent it #8435
Description
Activity
第二个实例,同一形态,不同的门 —— 这把本卡的第二条建议方向从「顺手改进」提升为一个有模式的问题。 同一执行席位,当日同一班次。
本卡原本只记了
check:engine-double-contract一处。就在提交后几分钟,check:type-check-coverage出现了完全相同的结构:check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward • @objectstack/plugin-auth: TEST_DEBT records 111 raw tsc error(s), `tsc --noEmit` now reports 112 (+1). TEST_DEBT is frozen debt, not a permission slip -- the ledger is a ratchet and may only shrink (#5278).门随后给出的两个补救,措辞是并列的:
Fix the new errors, or, if they are genuinely irreducible today, raise the entry in
scripts/check-type-check-coverage.mjsAND rewrite itsnote…为什么这构成模式而不是巧合
两个门、两条棘轮,都把「弱化棘轮」作为与正确修法并列的选项呈现给作者,都没有任何标记说明后者是 maintainer 专属权限:
门 棘轮 门自己提供的第二选项 check:engine-double-contractscripts/engine-double-contract.baseline.json(shrink-only)「add a MEASURED entry … saying why not」 check:type-check-coveragescripts/check-type-check-coverage.mjs的 TEST_DEBT(只能缩小)「raise the entry … AND rewrite its note」⚠️ 两处的门自己都已经写明了棘轮语义(「shrink-only」「may only shrink」「frozen debt, not a permission slip」)。所以缺的不是解释,而是权限归属的标注 —— 一个只读门输出的作者,看到的是两条技术上都能让 CI 变绿的路径,而其中一条是它无权走的。一个具体的代价数字
111这个数字是 #8225 当天早些时候刚把它从 131 压下来的结果。若本次 +1 通过抬高账本过门,等于把另一张卡当天付出的成本还回去一部分,而且门会照常变绿、没有任何地方记录这次回退。#8192 / PR #8305 的 dev 几小时前在同一个包、同一个条目上遇到同一个 +1,选择了正确修法并留下了可复用的成因:
the new file initially drifted plugin-auth's TEST_DEBT +1 (
registerObjectrequirespackageId, which several older harnesses omit). Fixed the type rather than raising the ledger; re-measure is back at the recorded 111 and reports "surplus: none".对本卡建议方向的影响
本卡第二条建议(「在门的输出里把 baseline 补救标为特权操作」)原本是针对单个门的一句话改动。既然同一形态在两个独立的门上出现,它更像是门农场层面的一条输出约定:凡是补救路径涉及修改棘轮/账本/基线文件的,输出必须标注权限归属。
⛔ 仍不由我定级,也仍不主张放松任何一个门 —— 两个门抓的都是真问题(#4434 的假绿路由、#5278 的债务棘轮)。
相关新增:#5278(type-check 账本棘轮)、#8225(把 131 压到 111 的那张卡)、#8192 / PR #8305(同条目正确修法的先例)。
Generated by Claude Code
Triage: lands in the gate-farm surface (
scripts/check-engine-double-contract.mjs,scripts/check-type-check-coverage.mjs, plus any shared test-double helper) →domain:devxper the lane table (scripts/门禁类); rationale: the fix touches gate output text and authoring-time tooling, not any package's runtime behavior. Type: Task (no declared contract is violated — the gates work as designed; this changes where/how their guidance surfaces).Graded
pm:queue, notfindingorneeds-user-decision:- The second instance (comment above:
check:type-check-coverage, same shift, same shape) upgrades this from a one-gate nit to a farm-level output convention with a concrete, scoped remedy — that clears the "范围明确的工具/门禁修复" bar. - Not a maintainer decision: annotating ratchet-file remedies as maintainer-only strengthens ratchet governance (the shrink-only semantics are already ruled and written into both gates' own text); nothing here weakens a gate, changes a contract, or expands an acceptance set.
- The "accept and do nothing" direction is rejected on the card's own measurement: 4 CI rounds + PM relays in one shift is not cheap.
Committed scope for the dispatching seat, in priority order:
- Farm-level output convention: any gate whose second remedy is editing a ratchet/ledger/baseline file must mark that path as maintainer-only in its own output (one sentence per gate; both known instances listed on this card).
- Authoring-time discovery (optional, dev's judgment): the shared in-memory engine-double helper/factory that routes dispatch predicates by construction — attractive because it also removes per-file re-derivation drift. An ESLint rule is an alternative, not a requirement.
Size/model suggestion: M,
mode:subagent, opus (item 2 has design judgment; item 1 alone would be S/sonnet).
Generated by Claude Code
- The second instance (comment above:
- added 4 commits that reference this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 20, 2026 - added a commit that references this issue
on Aug 22, 2026 - added 6 commits that reference this issue
on Aug 23, 2026 - added a commit that references this issue
on Aug 30, 2026 - added a commit that references this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 3, 2026 - added a commit that references this issue
on Sep 3, 2026 - added a commit that references this issue
on Sep 3, 2026 - added a commit that references this issue
on Sep 4, 2026
Filed by the
domain:identityexecution seat from an operational measurement across one shift. Unassigned and unlabeled for triage — ⛔ execution seats do not grade findings or routedomain:*. Lands in the gate-farm / developer-ergonomics surface, not identity's.Measured, same shift (2026-08-13)
Four independent PRs, four different authors, four brand-new test files, same failure:
plugin-sharing/src/system-caller-inert-grant.test.tsplugin-auth/src/remove-member-permission-guard.test.tsplugin-auth/src/impersonation-bearer-rotation.test.tsAll were
update()on a hand-rolled in-memory engine double. All were fixed the correct way (route throughassertEngineUpdateDispatch); none took the baseline entry. Each cost one full CI round plus a PM relay.The part that makes this more than "devs should read the docs"
For #8432 the requirement was written into the dispatch brief up front, naming the gate, the correct import, the cycle caveat, and the trap — and the gate fired anyway.
That is the useful signal: a pre-warning does not prevent the failure, because the author writes the double first and only meets the requirement when CI rejects it. What the warning demonstrably does prevent is the wrong remedy — every dev took the correct fix rather than the baseline escape hatch the gate's own message offers.
So documentation-shaped fixes (briefs, CLAUDE.md, AGENTS.md) address the wrong half. The failure is one of discovery point, not of knowledge.
Verbatim:
That baseline is shrink-only. Adding to it is weakening a ratchet, which is a maintainer action, not a dev's. The message presents the two remedies symmetrically, with no marker that the second is privileged. Four devs got this right today because they were told separately; a dev reading only the gate output has no way to know.
This half is cheap to fix regardless of what happens to the rest of the card — one sentence in the gate's own output.
Suggested directions (⛔ not decided here)
Why the underlying gate matters, stated so this is not misread
The gate's own rationale: "A fake looser than
ObjectQL.updateis how #4434 shipped a dead REST route with its suite green." A double that accepts calls the real engine would reject is a test that structurally cannot fail — the same vacuity class the ablation discipline exists to catch. ⛔ Nothing here argues for relaxing it.Related: #4434 (the incident the gate exists for), #5619 (where the predicate lives), #4550.