feat(pm): gate the clause-② carrier strip on the served tier the verdict declares - #17990
Conversation
`CONTRACT_REVIEW_TIER`'s own docblock declares the comparison against the SERVED tier EXACT, and nothing performed it: the dispatching seat passes a model as a parameter, a parameter is configuration rather than a reading, and the reviewer's "opening self-check" was prose a round could skip while producing a verdict indistinguishable from one that did not. A contract-review verdict now carries a `Served-tier:` line whose value is the harness-stamped served-model field of the reviewer's own transcript, and `check-clause2-carriers.mjs --pair` refuses to treat a `needs:contract-review` pair as cleared unless the newest verdict on the current head declares exactly the constant — naming the PR, the verdict comment and the served value, at exit 4. The row (C7) rides C6's population and C6's chosen comment, so a clearance judgment of a hung carrier is the whole of its scope: a `Clause-②: no` pair that never carried the label is never refused for lacking the line, and an absent or unsigned record stays C6's row alone. The constant is imported from `dispatch-gates.mjs`, which keeps its single value site. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
C7 compares against `CONTRACT_REVIEW_TIER`, which is declared in `dispatch-gates.mjs` and imported here, so a card editing that constant now predicts this gate — and the marker claiming no card's file surface can predict it stopped being true the moment the import landed. A false declaration is the shape the new row itself exists against, so it is retired rather than kept. The note left in its place states the input half that is still true, the half that is not, and the imprecision the import channel adds on the way: a followed module contributes its own literals, so this family inherits a `.github/workflows` lead it never opens, and the designed narrowing is per-module and cannot express one caller's case while the sibling really does read those globs. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…rved-tier-gates-the-strip
Measured against the live corpus rather than the fixture: every record the remediation rounds post spells the value as the stamp control first, then the tier — `Served-tier: 75/75 \`<tier>\`` — and the ruling's own specimen is written the same way. A reader that demanded the tier token immediately after the colon would have refused every verdict produced under the rule it enforces, on its first day. So the grammar is `[N/M ]<tier>`, and the count is judged rather than skipped: it is the zero-hit control the discipline already requires, and a count that is not total is the fallback evidence whose own rule text voids the verdict entire. Absent, it is vacuous — the tier alone decides, which is the ruling's minimum, so nothing the ruling permits is refused. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: Files: ① derived judgments — four changes to what the gate accepts or reports, each read against ruling 5651924370 items 1–3:
Seat measurements on the head tree ( ② semver: no package touched; ③ boundary flags: Checks on Implemented-by: Verdict: PASS — items 1–3 of ruling 5651924370 implemented as ruled; nothing widens. Governed ( Generated by Claude Code |
维护者速读(终稿)改了什么 —— 一行规则、一道门。规则:契约复审裁决首行必带 为什么 —— 裁决 5651924370 第 1–3 项:常量 docblock 写着「与服役档精确比较」,树上没人在比;卡上实测多轮在档下清了标。 风险 —— 收紧,不放宽。现网今天已有两条裁决没带这行(#17983、#17979 的记录),各补发一条记录即可,落地前不会被卡。一处顺手:该文件的 一红 —— 批准即由本席按裁决 C 落地;顺手点一次 re-run 即可,或等下一次 push。要不要批? Generated by Claude Code |
|
The red, once more and with a card — skills seat, 2026-09-13T11:29Z. Generated by Claude Code |
|
Green — skills seat (session Generated by Claude Code |
|
Ruling C landing step — skills seat (session Generated by Claude Code |
Fixes #17915
CONTRACT_REVIEW_TIER's own docblock declares the comparison against the served tier EXACT — "never a family or prefix floor" — and nothing in this tree performed it. The dispatching seat passes a model as a dispatch parameter, and a passed parameter is configuration rather than a reading; the docblock's other half said the re-review sub-round's "opening self-check reads this", but a self-check is prose to the reviewer, so a round that simply did not run it produced a verdict indistinguishable from one that did. The census on the card measured 11 rounds served below the declared tier across four days and eleven PRs, five of them the only clearance a mergedClause-②: yespair ever had.This lands items 1–3 of the director ruling: the verdict carries the reading, and the strip is gated on it.
What changed
.claude/skills/pm-dispatch/references/contract-review.md(2 lines of new rule, net 0 at the ratchet):scripts/pm/check-clause2-carriers.mjs— a new finding row, C7:readServedTierreads aServed-tier:key line withReviewed-by:'s own discipline (case-sensitive key, the markdown decoration a seat writes without meaning it). Three-valued:read/unreadable/missing— a carrier never started and one started and left unreadable are different facts.[N/M ]tier, corrected from a fixture against the live board before shipping. Every record the ruling's own remediation rounds are posting right now spells it control-first —Served-tier: 75/75 \…`on PR #17877,138/138on #17498 — and the ruling's specimen is written the same way. A reader that demanded the tier token immediately after the colon would have refused every verdict produced under the rule it enforces, on day one. TheN/M` is judged rather than skipped: it is the zero-hit control the discipline already requires, and a count that is not total is the 「回退证据」 whose own rule text voids a verdict entire. Absent, it is vacuous — the tier alone decides, which is the ruling's minimum, so nothing the ruling permits is refused.reviewOfRecordcarries the reading on the record it already chose, so C6 and C7 can never disagree about which verdict a clear stands on.c7ServedTierBelowfires on C6's population and no other —needsRecordRead's completed state, i.e. a clearance judgment of a hung carrier: declaredyes, the gate bound and cleared on both carriers, head unmoved. The refusal names the PR, the verdict comment and the served value, at exit 4 (a limb not standing), never 3.CONTRACT_REVIEW_TIER, imported fromdispatch-gates.mjsso the model id keeps exactly one value site acrossscripts/pm/**and.claude/skills/pm-dispatch/**. No model identifier appears anywhere in this diff outside that import.0/0void,12/133refused as fallback evidence, absent vacuous, a perfect control never rescuing a below-tier value), the exactness pins (a family prefix and an extended value both refused), and the four populations the row must never reach.Deliberately NOT in scope
A
Clause-②: nopair that never carried the label is never refused for lacking the line — it is not in the candidate shape, so the row cannot reach it. A pair still carrying the gate owes nothing yet. An absent or unsigned record stays C6's row alone. No PASS/FAIL token is read to reach any of it: what produced a verdict is measurable, what it concluded stays human.One deviation from the dispatched file surface — and why it is inside it
The dispatch scoped the diff to those two files. It is those two files — but one edit inside the checker was not foreseen and is worth reading before approving.
scripts/pm/check-clause2-carriers.mjscarried adispatch-gates: no-path-populationmarker: "this gate reads no file in the tree at all … so no card's file surface can predict it". The input half is still exactly true. The other half stopped being true the moment C7's import landed: a card editingCONTRACT_REVIEW_TIERmoves the value every clearance is judged against, so it does predict this gate.pnpm check:pm-dispatch-gatescatches this directly — its live-half caseno family both DECLARES no path population and names paths anywaywent red oncheck:pm-clause2-carriers, measured by ablation (base tree: hints[]; with the import: hints[".github/workflows"]).Keeping a declaration that stopped being true is the exact shape C7 itself exists against, so the marker is retired, and the comment left in its place states the trade rather than hiding it: the import channel contributes a followed module's own literals, so this family now also inherits a
.github/workflowslead it never opens, on a gate whose CI step runs the self-test only. The designed narrowing (inherited-population, declared by the followed module) cannot express this case — it is per-module, and the same module's globs are a real population forcheck:pm-widening-tells, which reads them. Filed separately as #17991 rather than worked around here.⛔ The alternative — restating the tier in this file — is the thing that let the declared tier and the served one drift apart in the first place, and is refused.
Acceptance measurements
All taken at
fed29ced, against base9ccc4179.Served-tierinreferences/contract-review.mdServed-tierincheck-clause2-carriers.mjsReviewed-byinreferences/contract-review.md(lit control)references/contract-review.mdlinescheck-clause2-carriers.mjslongest line (bytes)references/contract-review.mdis ≤ 120 bytes (LC_ALL=C awk 'length($0)>120'prints nothing); the four edited lines measure 105 / 113 / 111 / 108 B. The 120-byte register is the reference file's; the checker keeps its own line style unchanged (471 → 502 lines over 120 B, max unmoved at 390).git diff --stat origin/main...HEAD→ exactly.claude/skills/pm-dispatch/references/contract-review.mdandscripts/pm/check-clause2-carriers.mjs.+1the 同形 line,+1the mechanism line replacing the 转录档位核验 prose,-1by folding 「⛔ 自述档位不是读数」 and 「传参只是配置 ⛔ 不作达档读数」 into one clause (they are one rule: a tier claim that is not a harness stamp is not a reading),-1by dropping the 转录核验 grep recipe, whose method survives at :49 (「每场前必读服役档,读法见platform-readings.md」) and in the new 同形 line.node scripts/pm/check-skill-line-ratchet.mjsgreen at 60/60, headroom 0.Checks
pnpm check:pm-clause2-carriers— 588 cases pass (546 before this PR; the C7 battery is 42 and is registered with its own floor,SELF_TEST_BATTERY_FLOOR19 → 20, preserving the roster's existing slack).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) on the merged head: 41 families, 41 run, 41 exit 0. Reconciled:--ranreports41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3).pnpm --filter @objectstack/lint run check:doc-formula-expressionsfirst answeredPREREQUISITE NOT MET(exit 3, nothing measured). Built its two declared prerequisites under the shared verify lock (VERDICT command-exit 0 · held the lock 141s) and re-ran it: exit 0.Served-tier:requirement turned the reference record fixture red —⭐ the #14155 specimen WITH its record still reads CLEAN overallfailed with["C7"]— before the fixture gained the line. The row can fail.node scripts/pm/check-clause2-carriers.mjs --pair 17956: exit 2 on this head and exit 2 on the base script for the same PR, same sentence (PR #17956 is not open, or names no card this file can derive). The pair could not be formed, so nothing about it was judged — the dispatch expected 0 for aClause-②: nopair, and that PR has since left the open set. ⛔ Not a C7 refusal, and unmoved by this diff.eslint .over oneeslint.config.mjs;npx eslint --no-inline-config --format json scripts/pm/check-clause2-carriers.mjs→ 1 file, 0 errors, 0 warnings; the narrowing excludes nothing because that config "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" (its own line 328), so this diff cannot move any untouched file's verdict. The.mdis not an eslint input.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both edited files: no hits;pnpm check:nul-bytesgreen.Changeset
skip-changeset—scripts/pm/**and.claude/**publish nothing: neither path is in any package'sfiles[], and both are on the fast track (.claude/**and PM tooling). Label applied and read back.Acceptance notes
inherited-populationis per-module, so no caller can decline a fabricated lead #17991 — the import channel's over-reach, with both readings:inherited-populationis keyed on the followed MODULE while fabrication is a property of the CALLER (the same module's globs are a real population forcheck:pm-widening-tells, which reads them), and theif (entry.selfTest) continue;guard built to stop exactly this inheritance never fires for apnpm check:*family, becauseselfTestis read off the workflow argv while the--self-testlives in thepackage.jsonscript body. Searched first: [finding] dispatch-gates.mjs is a followable non-gate module, so its join bases and tier globs are inheritable — 2553 fabricated pairs for the next gate that imports it #11556 (already resolved, and left alone here) is the same class by a route its remedy cannot express; no open card covers it.维护者速读(草稿)
改了什么 — 契约复核裁决从此必须带一行
Served-tier:,值取复核者转录里 harness 逐消息盖的model字段;check-clause2-carriers.mjs --pair在判定「双载体已清」时读这一行,不等于CONTRACT_REVIEW_TIER就拒(exit 4),并点名 PR、裁决评论和读到的档位。规则文本同步落在references/contract-review.md,行数 60 → 60。为什么改 — 常量自己的 docblock 写着「与服役档的比较是 EXACT」,而树上没有任何东西在比。派进去的 model 是配置不是读数,「开场自检」是写给复核者的散文:一轮不跑它,产出的裁决与跑了的长得一模一样。卡上实测 11 轮在档下产出裁决,其中 5 轮是已合并
Clause-②: yesPR 唯一的清标依据。这是本仓在别处一律拒绝的 declared ≠ enforced,落在「一次公共契约加宽到底有没有被复核过」那道门本身。风险与代价(含回滚) — 失败方向是响亮的:清标被拒,不是被静默放行。代价一:规则落地前写的历史裁决没有这一行,再被判定时会红,补救是复核席把自己转录里已经盖好的读数补写成一条新记录(最新的记录优先,不动载体)。代价二:本 PR 让这个门禁第一次有了树内依赖(
CONTRACT_REVIEW_TIER所在文件),因此退掉了它「无路径面」的旧声明;派生因此多送一条.github/workflows的线索,是噪音、已在文件里写明,并已记入验收备注。回滚 = revert 本 PR,一次 revert 即可,门禁回到今天的状态。席位意见 — (留空,待席位定稿)
你要做的 — 受管面(
.claude/**),本 PR 恒为 draft,⛔ 不由任何 AI 席位合并、入队或挂 auto-merge。请人工确认两件事:① 规则文本那两行的措辞;② 退掉no-path-population声明这一步是否接受(替代方案是把常量在本文件再写一遍,那正是让档位漂移的那个形状,已拒)。Provenance
Authored by the
domain:skillsseat's dispatched executor, sessionsession_01DAcomhvR9kKizeYgg89Vo8(https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8), on branchclaude/issue-17915-served-tier-gates-the-strip. Attribution is stated here in prose because a REST edit of a PR body appends its own footer block: the first edit of this body left two, and this revision sends none so the appended one is the only one.Generated by Claude Code