Skip to content

fix(pm): CONTRACT_REVIEW_TIER's docblock states a REVIEW tier, not a dispatch mandate - #17509

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-16914-dispatch-gates-clause2-docblock
Sep 10, 2026
Merged

os-litant merged 1 commit into
mainfrom
claude/issue-16914-dispatch-gates-clause2-docblock

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #16914

The residue of #16914 after PR #16915 (#16905) landed the 强制条款② narrowing: one docblock sentence in scripts/pm/dispatch-gates.mjs. Prose only — deriveTier encodes clause ① and nothing else, so no code path and no printed line moves.

The sentence

Before (scripts/pm/dispatch-gates.mjs:10489-10492):

clause ②'s CONTRACT-REVIEW tier: the tier a card that changes contract accept/reject behaviour or widens the public surface must be dispatched at, and the tier the needs:contract-review re-review sub-round must itself be running at (its opening self-check reads this).

After:

clause ②'s CONTRACT-REVIEW tier: the tier the clause-② REVIEW runs at, both halves of it — the spec seat's review of a card that changes contract accept/reject behaviour or widens the public surface, and the needs:contract-review re-review sub-round (its opening self-check reads this). The BUILD of such a card is at the default judgment tier, so this constant is a review tier and never a dispatch mandate.

The same file already carries the post-narrowing shape at its two other sites — the MANDATORY_TIER_GLOBS clause-② bullet (「built at the default tier and REVIEWED at CONTRACT_REVIEW_TIER — in the spec seat only」, :10553-10555) and the clause2 string tierLines prints on every --tier run (「owes a contract-review-tier REVIEW too (spec seat; default-tier build)」, :10759-10760). This docblock was the one site left contradicting both. Everything else in the docblock is byte-identical.

Why this wording, on the four axes

The one judgement call was how far to rewrite: (A) swap "dispatched at" for "reviewed at" and stop, (B) the wording above, which names both halves of the review and states the build tier explicitly, or (C) B plus a new self-test case pinning the docblock's prose. 实际业务需求 is measured, not hypothetical: this docblock is what the next editor of the tier constant reads before touching it, and the drift it carried is exactly what a --tier reader would have to reconcile against the instrument's own printout — A leaves that reconciliation unstated ("reviewed at" alone never says where the build happens), so B is the option that answers the real question the reader arrives with. 项目长远合理性 favours B over C: the file's own #15310 precedent pins a docblock only against something it can COMPUTE (COMPOUND_ANCHOR_LEDGER's counts), and clause ② is deliberately not encoded anywhere here, so any pin would have to hard-code the new sentence — a second hand-typed constant, the very shape that precedent argues against, and a workaround rather than an architecture. 防 AI 写代码犯错 is the axis that decides between A and B: an agent reading A can still conclude "so I dispatch clause-② cards at the ceiling" because nothing contradicts it in the same breath, whereas B states the build tier where the wrong reading would be made — declaration and enforcement said in one place, which is the same reason tierLines prints its clause-② note unconditionally rather than only on a hit. 创业阶段不扩散需求 rules out C outright and keeps B to six comment lines: no new battery, no new declared surface, no staged wording, nothing for a later PR to retire. Recommendation: B, as landed.

Verification

Gate families derived from the FINAL diff by the script itself (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no hand-built path list — the tool takes the change set from the merge base with three-dot semantics, 1 path(s) vs merge base 0ee32edef). Every derived family was run, each exit code captured BEFORE any pipe, and the record reconciled with --ran:

Run reconciliation — 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED
  (a DERIVED zero — all 31 recorded an exit code and none of them is 3).

All 31 exited 0. The heavy one — this file's own self-test — ran under the container's shared verify lock:

pnpm check:pm-dispatch-gates
os-verify-lock: VERDICT command-exit 0 · held the lock 848s (14m08s) · waited 0s

Beside the derived set:

Check Exit Verdict line
node scripts/pm/check-governed-merges.mjs --test scripts/pm/dispatch-gates.mjs 0 ✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.
npx eslint --no-inline-config scripts/pm/dispatch-gates.mjs 0 clean, no output
control-byte self-scan (grep -naP over the edited file) 1 (no match) no control bytes

The seven artifact-roster families the derivation flagged with ⛔ (their roster sits in a directory this diff's path is in, so their silence is evidence in neither direction) were read rather than assumed: check-published-list-mirrors.mjs and its --self-test, check:console-injection, check:engine-double-contract, check:i18n-stale-fill, check:pm-label-desc-cap all exit 0. check:published-readme-exports exits 3NOT MEASURED, prerequisite: it reads 45 packages' built dist/*.d.ts and this worktree has no build; a comment in scripts/pm/ cannot move any package's built type surface, and the repo-wide build belongs to CI.

The eslint run is a narrowing, and here is the proof it excludes nothing. ① Population read from eslint's own config, not guessed: isPathIgnored over git ls-files puts 6,570 of 8,303 tracked files in the lint population. ② Files this run actually linted, counted from --format json: 1, 0 errors, 0 warnings. ③ Invariance for the other 6,569: calculateConfigForFile on the edited file returns parserOptions {"ecmaVersion":"latest","sourceType":"module"} — no project, no projectService, so type-aware linting is off and every rule judging an untouched file reads only that file's own text. Six comment lines in one file cannot move another file's verdict. The repo-wide pnpm lint stays CI's run.

Acceptance notes

  • skip-changeset: scripts/pm/** publishes nothing from any released package — it is not in any package's files[], and the diff is a comment. Label applied.
  • noted, not filed: two other dispatched at occurrences survive in this file (:10521, :21352). Both are the clause-① misclassification incident narrative ("a card ... was claimed as 'not under the fable-mandatory roots' and dispatched at opus"), which is a correct account of what happened and not a clause-② mandate statement — a different sentence class, off this card's declared surface, and correct as written. Successor: whoever next edits this file's tier section.
  • noted, not filed: this file has no docblock-prose battery to extend. Its one docblock-consistency pin (COMPOUND_ANCHOR_LEDGER, [finding] COMPOUND_ANCHOR_LEDGER's docblock counts its own table wrong — says "all 22 / fifteen genuine / seven accidental" over a table that already held 25 rows (17/8) #15310) checks the docblock's counts against a computation over the table, never against a hand-typed second copy; clause ② is deliberately not encoded here, so there is nothing to compute a pin against. Not inventing a battery for one sentence — reported, per the dispatch. Successor: whoever next edits this file's tier section.

Clause-②: no — this is prose in an internal PM tool; no schema, accept/reject behaviour or exported surface moves, and the derivation's own --tier run prints no path mandate for scripts/pm/dispatch-gates.mjs.


Generated by Claude Code

…dispatch mandate

The constant's docblock still described clause ② as the tier a card that
changes contract accept/reject behaviour or widens the public surface "must
be dispatched at". Since the 强制条款② narrowing landed, that is false: such
a card is BUILT at the default judgment tier and REVIEWED at
CONTRACT_REVIEW_TIER — which is what this same file already states in the
MANDATORY_TIER_GLOBS clause-② bullet and prints from `tierLines` on every
`--tier` run. The docblock was the one site left contradicting both.

Prose only. `deriveTier` encodes clause ① and nothing else, so no code path
and no printed line moves.

Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants