You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
Filed by the domain:identity execution seat from an operational measurement across one shift. Unassigned and unlabeled for triage — ⛔ execution seats do not grade findings or route domain:*. Lands in the gate-farm / developer-ergonomics surface, not identity's.
⚠️The gate is correct and is doing its job. This card is not asking to weaken it. It is about where in the workflow it fires.
Measured, same shift (2026-08-13)
Four independent PRs, four different authors, four brand-new test files, same failure:
All were update() on a hand-rolled in-memory engine double. All were fixed the correct way (route through assertEngineUpdateDispatch); 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.
⚠️ The gate's message advertises a ratchet-weakening remedy as a co-equal option
Verbatim:
Open the fake's update with assertEngineUpdateDispatch(data, options) … Or add a MEASURED entry to scripts/engine-double-contract.baseline.json saying why not — with "verb": "update".
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)
Move discovery to authoring time: an ESLint rule, or a shared test helper / factory for in-memory engine doubles that routes the dispatch predicates by construction. The helper shape is attractive because it also removes the per-file re-derivation of an engine double, which is what produces the drift.
Mark the baseline remedy as privileged in the gate's message (e.g. "⛔ maintainer-only — the baseline is shrink-only").
Accept and do nothing: defensible if the CI round is considered cheap. Recorded so the choice is explicit rather than default.
Why the underlying gate matters, stated so this is not misread
The gate's own rationale: "A fake looser than ObjectQL.update is 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.
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.mjs AND rewrite its note …
⚠️ 两处的门自己都已经写明了棘轮语义(「shrink-only」「may only shrink」「frozen debt, not a permission slip」)。所以缺的不是解释,而是权限归属的标注 —— 一个只读门输出的作者,看到的是两条技术上都能让 CI 变绿的路径,而其中一条是它无权走的。
#8192 / PR #8305 的 dev 几小时前在同一个包、同一个条目上遇到同一个 +1,选择了正确修法并留下了可复用的成因:
the new file initially drifted plugin-auth's TEST_DEBT +1 (registerObject requires packageId, 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".
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:devx per 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, not finding or needs-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).
Activity
os-zhuang commentedon Aug 13, 2026
第二个实例,同一形态,不同的门 —— 这把本卡的第二条建议方向从「顺手改进」提升为一个有模式的问题。 同一执行席位,当日同一班次。
本卡原本只记了
check:engine-double-contract一处。就在提交后几分钟,check:type-check-coverage出现了完全相同的结构:门随后给出的两个补救,措辞是并列的:
为什么这构成模式而不是巧合
两个门、两条棘轮,都把「弱化棘轮」作为与正确修法并列的选项呈现给作者,都没有任何标记说明后者是 maintainer 专属权限:
check:engine-double-contractscripts/engine-double-contract.baseline.json(shrink-only)check:type-check-coveragescripts/check-type-check-coverage.mjs的 TEST_DEBT(只能缩小)note」一个具体的代价数字
111这个数字是 #8225 当天早些时候刚把它从 131 压下来的结果。若本次 +1 通过抬高账本过门,等于把另一张卡当天付出的成本还回去一部分,而且门会照常变绿、没有任何地方记录这次回退。#8192 / PR #8305 的 dev 几小时前在同一个包、同一个条目上遇到同一个 +1,选择了正确修法并留下了可复用的成因:
对本卡建议方向的影响
本卡第二条建议(「在门的输出里把 baseline 补救标为特权操作」)原本是针对单个门的一句话改动。既然同一形态在两个独立的门上出现,它更像是门农场层面的一条输出约定:凡是补救路径涉及修改棘轮/账本/基线文件的,输出必须标注权限归属。
⛔ 仍不由我定级,也仍不主张放松任何一个门 —— 两个门抓的都是真问题(#4434 的假绿路由、#5278 的债务棘轮)。
相关新增:#5278(type-check 账本棘轮)、#8225(把 131 压到 111 的那张卡)、#8192 / PR #8305(同条目正确修法的先例)。
Generated by Claude Code
hotlong commentedon Aug 13, 2026
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: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.Committed scope for the dispatching seat, in priority order:
Size/model suggestion: M,
mode:subagent, opus (item 2 has design judgment; item 1 alone would be S/sonnet).Generated by Claude Code
fix(devx): mark ratchet-editing remedies as maintainer-only in both g…
fix(devx): mark ratchet-editing remedies as maintainer-only in three …
feat(scripts): farm-wide detector for the objectstack-ai#8435 ratchet…
fix(scripts): make the three refusal-shape gates state their registry…
chore(lint): record the ruled pre-auth exemption in the route-envelop…
docs(gate): rest check-role-word's comment figures on invariants, not…
docs(scripts): check-role-word's --update moves the baseline in both …
fix(tooling): name the inheritance rule in both ratchet remedies, so …
fix(gate): declare the auth mount-ledger gate's remedy authority (#8435)
chore(lint): record the ruled pre-auth exemption in the route-envelop…
docs(gate): rest check-role-word's comment figures on invariants, not…
docs(scripts): check-role-word's --update moves the baseline in both …
fix(tooling): read a defaulted mock initializer, name the inheritance…
feat(devx): add the ruled vendor-wire state to check-route-envelope's…
feat(gate): fail a rawApp auth mount that carries no ledger row (obje…
feat(tooling): Rule 3's root extension lands as a ledgered cross-pack…
Rule 3 root extension: a ledgered cross-package prose-id leg on check…
tooling(i18n): mark the exemption-ledger remedy MAINTAINER-ONLY
tooling: record the authorised cross-file move in the remedy-authorit…
tooling(pm): admit a same-PR cross-file line-budget move, and take th…
tooling(scripts): widen the ratchet-remedy-authority sweep to scripts…
tooling(i18n): gate that every declared translation group is walked b…
tooling(scripts): widen the ratchet-remedy-authority sweep to scripts…