feat(pm): check-clause2-carriers emits the contract-review record as a copyable template - #18138
Conversation
…a copyable template The record is the one machine-read artefact the skill described only in prose: `references/contract-review.md` states the shape and the authorship pair, and nothing in the tree could be copied. Four in-seat records in one session wrote a leading word before the `Implemented-by:` value, C4 refused all four as HALF WRITTEN, and two reached main where C6 reads no review of record on that head. The checker that enforces the shape now emits it: `--template` prints the record with the value directly after every colon, fenced, with the calibration around it. Its 23 self-test cases drive the printed bytes back through `readVerdictAuthorship`, `readServedTier`, `contractReviewHeadMatch` and the heading marker, and hold the measured miss — and the same record with the one word removed — apart in both directions. C4's own refusal now names the flag. The rule line keeps its budget: `可前置 N/N` moves into the printed legend, where the author copies from, and the doc line gains the pointer instead. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Contract reviewHead: ① derived judgments (seat-measured on the fetched head
② semver: unchanged — ③ boundary flags: the dev's out-of-scope finding (a) is real and this seat files it as a bare Implemented-by: Verdict: PASS — references tier ⇒ this seat lands: ready through 维护者速读(终稿)契约复审记录以前只有散文描述、没有可照抄的模板,一轮里四份记录把 Generated by Claude Code |
|
CI red on Generated by Claude Code |
…ot read as a dead path population The dispatch derivation reads any quoted path-shaped literal in a gate's module body as the population that gate watches. The template's branch placeholder is path-shaped and names no tracked file, so it became this family's only declared literal and `check:declared-population-live` refused it: a gate telling the derivation it reads a population while the derivation reads none. Assembled from two parts, neither carrying a separator, so neither is admitted and this file goes on declaring no path population — the state it had before the template landed, and the truthful one, since it reads no file in the tree. The printed template is byte-identical (same md5 before and after), and a new self-test case spells the printed placeholder out where the scan does not reach, so a later tidy that re-joins the literal is caught rather than silently shipped. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Contract reviewHead: ① derived judgments (seat-measured on the fetched head
② semver: unchanged. ③ boundary flags: the dev notes the family could honestly carry a Implemented-by: Verdict: PASS — references tier ⇒ this seat lands: the PR is already ready; auto-merge re-armed through 维护者速读(终稿)上一版 CI 红是模板里的分支占位符被派发推导当成了一个"声明了却不存在"的路径群;返工后占位符改为运行时拼接,打印出来的模板一个字节没变,原来红的检查在新 head 上绿了,自测 629 例。references + 脚本层,本席自审自落地,不需要你批。 Generated by Claude Code |
…rt binds (objectstack-ai#18157) Fixes objectstack-ai#17991 `inherited-population` is keyed on the MODULE, while whether a contribution is fabricated is a property of the CALLER. `scripts/pm/check-clause2-carriers.mjs` imports one exported string constant (`CONTRACT_REVIEW_TIER`) from `scripts/pm/dispatch-gates.mjs` and thereby inherited that module's whole declared watch surface, so every card touching `.github/workflows/**` was told to run `pnpm check:pm-clause2-carriers` — a gate that never opens that directory. No per-module declaration can express the refusal, because the same module's globs ARE a real population for `check:pm-widening-tells`, which binds and reads them. ## What changed One file: `scripts/pm/dispatch-gates.mjs`. - `firstPartyImportBindings` is the resolver `firstPartyImportTargets` already was, carrying the one thing that function dropped: WHAT the importer binds from each module it reaches. The resolution, its refusals and its sort order are unchanged to the byte — `firstPartyImportTargets` is now a `keys()` of it, and the whole-tree derivation was proved identical across that refactor before any rule was added. - `exportedValueConstant` reads the exporter half: is NAME declared in that module as a `const` initialised to one primitive literal? It is read in ONE direction only (a positive answer removes leads), so every shape it cannot read answers negative — an array, an object, a function, a class, a computed or multi-line initialiser, a re-exported binding and an undeclared name are all "not a value". - `importBindsNoPopulation` is the per-caller rule: an importer binding only value constants reaches none of the module's reads and inherits nothing; a table, a function, a class, a namespace, a default, a re-export and every clause this reading cannot parse inherit exactly what they did. One exception, and it is a live specimen rather than a hypothetical: a string constant whose own text is one of the module's paths stays population-bearing — `scripts/adr-anchors.mjs` exports its anchor directory that way, and binding a one-path population is reading it. - `discoverFamilies` keeps `entry.imports` whole (the edge is real) and records the contributing subset as `entry.populationImports`, unioned across the family's files. The two docblocks the card quotes are extended in place rather than a new marker being added: `declaredInheritedPopulation` now states what it is keyed on and what reaches the caller instead, and the follow's docblock states that reaching a module and inheriting from it are two questions. objectstack-ai#11556's semantics for table importers are untouched, and `.github/workflows/**` is untouched. ## Acceptance All readings on this branch at `1b75a853a0`, base `295eae57d`. **The card's ablation, reproduced on the committed base** (mutate, measure, restore, restore proved by blob hash `e59aaff8e0698af16cc20bd2128b81b6bc211c5a` with `git status --porcelain` empty): ``` base, import present -> check:pm-clause2-carriers hints: [".github/workflows"] base, import removed -> check:pm-clause2-carriers hints: [] ``` **And reversed on the head** — the same ablation, same restore proof, now with no effect at all: ``` head, import present -> check:pm-clause2-carriers hints: [] (imports: ["scripts/pm/dispatch-gates.mjs"]) head, import removed -> check:pm-clause2-carriers hints: [] (imports: []) ``` **The derivation a dispatch prompt pastes.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack .github/workflows/lint.yml`, base against head, is exactly one line shorter: ``` 26d25 < pnpm check:pm-clause2-carriers ``` and for `scripts/pm/check-clause2-carriers.mjs` the two command lists are byte-identical — the gate's own identity key is untouched, and a card editing the carrier checker still runs it. **The control population (`check:pm-widening-tells`) still inherits.** It binds `SUSPECT_TIER_GLOBS` (a table) and `hintCovers` (a function) from the same module, keeps `.github/workflows` with `hintOrigin` `scripts/pm/dispatch-gates.mjs`, and keeps all 50 of its inherited hints (49 of them from `scripts/regen-artifacts.mjs`). The derivation's own line, from the self-test: ``` ✓ the answer is a property of the CALLER and not of the module — the live tree has 1 module(s) answering BOTH ways (scripts/pm/dispatch-gates.mjs), which is the shape no per-module declaration can express ✓ and the caller that BINDS the population still inherits every path of it ``` **The family-population diff for the whole tree.** Every family's `hints`, `imports` and `hintOrigin` dumped before and after: ``` families with a changed derived population: 1 of 307 CHANGED check:pm-clause2-carriers imports base: ["scripts/pm/dispatch-gates.mjs"] head: ["scripts/pm/dispatch-gates.mjs"] inherit base: [".github/workflows <= scripts/pm/dispatch-gates.mjs (import)"] head: [] hints base: [".github/workflows"] head: [] ``` Measured the same way over every live import edge the derivation follows: 1 of 52 edges binds only value constants, and it is the card's. The other four importers of this module are unmoved — `check:pm-widening-tells` binds a table, and `check:declared-population-live`, `check:watch-hint-literal` and the two-hop cases bind functions. Note the card's suggested enumeration (`git grep "from './dispatch-gates.mjs'"`) is incomplete: it misses the `./pm/dispatch-gates.mjs` spelling two of those importers use, so the enumeration here is taken from the derivation rather than from the grep. **Gates.** Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths, three-dot against merge base `295eae57d`, change set = 1 path), every one run in the foreground with `$?` captured before any pipe, then reconciled: ``` ✓ 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, plus two run beyond the union because this change moves them: `pnpm check:pm-clause2-carriers` and `pnpm check:pm-widening-tells`, both exit 0. `pnpm check:declared-population-live` (its self-test and then the live sweep) exits 0 — the objectstack-ai#18138 lesson was measured rather than assumed: the wording added here adds no path-shaped literal any family declares, and the whole-tree dump above is the check. **The self-test.** `pnpm check:pm-dispatch-gates`: **1723 cases pass, 0 failures, 0 NOT MEASURED subjects.** Eleven `t(` call sites added and zero removed (`git diff` counts), so the count strictly increased; 1712 before is arithmetic off that diff, 1723 after is the measured run. The new cases pin both directions on fixtures (the clause reader, the exporter recogniser, the rule) and then the live shape: ``` ✓ the narrowing is NOT vacuous on this tree — 1 of 52 followed import edge(s) bind only value constants (check:pm-clause2-carriers -> scripts/pm/dispatch-gates.mjs {CONTRACT_REVIEW_TIER}) ✓ and not one of those edges contributes a hint to the family that binds it ``` The self-test's own reconstruction of a family's hints now models the binding rule too, so a future widening of the recogniser reds there rather than silently agreeing with itself. ## Restart-touch Hold objectstack-ai#14290 (`pm:on-hold`, "dispatch-gates: STAGE-THEN-RUN reaches a program by an edge neither follow traverses") carries a `Restart-touch:` on `scripts/pm/dispatch-gates.mjs`, so this landing fires its re-verification. **This change does not touch STAGE-THEN-RUN edges**: it changes only what an IMPORT edge contributes, and leaves `spawnedProgramTargets`, `readProgramTargetsInSource` and `packageManifestTargets` — the follows objectstack-ai#14290 is about — untouched in both traversal and population. ## Changeset None. `scripts/pm/**` publishes nothing — the `skip-changeset` label is applied. ## Acceptance notes - Observed, not filed: a card editing `scripts/pm/dispatch-gates.mjs` does not derive `check:pm-clause2-carriers` today either, measured on the base — the import edge's only contribution to that family was the fabricated workflow lead. An IDENTITY key over import edges is deliberately refused (objectstack-ai#13126, priced in the derivation's own self-test), so this is a recorded disposition rather than a gap this PR opens. Successor: none. - Observed, not filed: `entry.selfTest` is still read off the WORKFLOW argv while a `--self-test` in a `package.json` script body is invisible to it, so the cheaper guard beside the fix stays silent for every `pnpm check:*` family whose self-test-only-ness is spelled in the manifest. The card offers it as a separable shape and the machinery for it already exists next door (`selfTestOnlyInvocation` resolves a row one hop through the manifest that defines it). It is left out here because it would move other families' derivation, which is the one thing this PR's blast radius is pinned against. Successor: the card, which stays open on this half if the seat wants it. --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18042
The contract-review record is the one machine-read artefact this skill described only in
prose.
references/contract-review.mdnames its shape (:28), itsServed-tier:first line(:29) and the machine-read authorship pair (:35, :37) — and nothing in the tree could be
copied. Four in-seat records in one session composed the pair from memory and put a word in
front of the value; C4 refused all four as HALF WRITTEN, and two reached
main, where C6then reads「no review of record on that head」. The standing remedy this family prescribes
everywhere else — COPY the template's line rather than composing one — was unfollowable for
exactly this artefact, because nothing existed to copy.
The measured instance (⛔ read only; not edited, not re-posted)
Comment 5652813288 on PR #17986 carries the pair as:
The leading word is the entire defect.
readImplementerTokenstrips DECORATION (spaces,**, backticks) and never a word, so the token has to be the first thing after the colon —「值紧跟冒号,前置词即不可读」. Both directions are now pinned: the specimen verbatim reads
malformed, and the same record with that one word removed readspair.What landed
The checker that ENFORCES the shape now EMITS it:
node scripts/pm/check-clause2-carriers.mjs --template # exit 0, no board is readIt prints the record fenced between two markers, with every value directly after its colon,
followed by a per-key legend. The record block (its last line, the verdict line in caps, is
omitted HERE on purpose — a body carrying it would be readable as a verdict comment by any
reader handed this body, and this PR is not a review of record):
Three properties the placeholders are chosen for, each pinned by a self-test case:
changes is WHOSE record it is, never whether it reads at all.
template is provably round-trippable) while prefixing NO real head — a record pasted
unedited is refused by C6, never silently adopted. Fail-closed in the one direction that
matters.
ones included; a placeholder spelled that way would be eaten out of the very comment a seat
pastes it into. This is the same fact
CLAUSE2_CORRECTION_KEY_LINEalready states for thecorrection carrier, and it is why the card's suggested
issue-(n)-(slug)spelling is NOTwhat shipped.
Two shape corrections the template carries by construction, both measured on the specimen:
H51_SHA_SPANmatches a span that is hexand nothing else, so the corpus spelling — the whole
Head-sha: …pair inside ONE span,which is what comment 5652813288 writes — matches nothing, and that record names no head
at all. A self-test CONTROL pins the difference.
VERDICT_MARKERis case-sensitive, so the capitalisedspelling is recognised by the marker alone, without depending on the head match.
c4VerdictSelfReview's own HALF WRITTEN refusal now names the flag, which closes the loop:the row a seat lands on when it gets the pair wrong tells it what to copy.
The route taken, and why
The seat's suggested route, and measurement agreed with it: the checker emits the template
rather than the doc carrying a literal block.
contract-review.mdis 60/60 with zeroheadroom, so a literal block would have had to be funded by deleting five or six rule lines.
⛔ Re-wrap is not currency and ⛔ no ceiling was raised.
One detail of the suggested route was falsified by measurement and is reported rather than
followed: the pointer could not land on :35, which is already at exactly 120 bytes — the
cap — and :29 held only 10 bytes of headroom, less than any pointer spelling costs.
Line budget — net 0, paid by density INSIDE the file
Paid by dropping 「,可前置 N/N」 (16 B) to fund 「,模板见
--template」 (25 B): 110 − 16 + 25 = 119 B,under the 120-byte cap, and the file stays at 60 lines with no line added or removed.
Where the dropped clause survives — named, as the ruling requires. The stamp control moves
into the printed legend, where the author actually copies from:
· Served-tier … an at-tier/total stamp control may precede it — 75/75, then the constant,pinned by a self-test case so it cannot silently vanish. Its ENFORCEMENT never lived in the
doc at all:
STAMP_CONTROLandservedStampsHoldcarry it, unchanged by this PR. C7's rulestring, which mirrors :29 verbatim, is updated in the same edit so the quote cannot go stale.
Acceptance greps, both directions
Reverse verification (one-off, nothing left in the tree)
Committed first, then the template's
Implemented-by:value was mutated to carry themeasured leading word, on disk and proven there (clean anchor 1 → 0, mutated anchor 0 → 1;
blob
cb3c41f79→50c4ed175):Restored with
git checkout HEAD --(⛔ not the bare form, which restores from the pollutedindex):
git diff HEAD --statempty, blob back tocb3c41f79, byte-identical to HEAD.Gates — 42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths), every command run in the foreground with
$?captured before any pipe, thenreconciled with
--rancarrying each exit code:All 42 exit 0, including
check:pm-clause2-carriers(628 self-test cases, 23 of them thiscard's),
check:pm-skill-ratchet,check:pm-skill-id-lint,check:pm-governed-merges,check:nul-bytesandcheck:pm-dispatch-gates. Two ran outside the derivation and also pass:check:pm-governed-proseandnode scripts/check-skills-token-ratchet.mjs.check:doc-formula-expressionsfirst exited 3 — PREREQUISITE NOT MET (a workspace packagewas unbuilt). That is not a verdict: the two packages were built under
scripts/pm/os-verify-lock.shand it was re-run to exit 0.Changeset
None.
.claude/**andscripts/pm/**publish nothing — no packagefiles[]ships eitherpath — so this carries the
skip-changesetlabel instead.Out of scope — reported to the PM rather than filed
The write budget for this dispatch is
git push+ onePOST /pulls+ the label + one reportcomment, so these are handed to the PM to file rather than filed here:
5652813288 writes
Head-sha: …and the sha INSIDE one code span;H51_SHA_SPANneeds aspan that is hex and nothing else, so
contractReviewHeadMatchanswersnulland C6 readsno review of record on that head even where a correct record exists. Probe: the CONTROL case
in this PR's own battery.
contract-review.md:28 says 「所审 head sha 码段」 without sayingthe span holds the sha ALONE, which is the authoring trap behind it. The template closes it
for every record written from here on; it does not touch the ones already on the board.
Served-tier:line at all, sounder C7 it would not stand as a verdict today. Successor: whoever next touches the record
shape — this PR does not, and ⛔ a merged record is not edited.
维护者速读(草稿)
改了什么 — 契约复核记录第一次有了可照抄的模板:执行形状的那个检查器现在自己打印它
(
--template),值一律紧跟冒号;contract-review.md:29 加一句指针,行数不变。为什么改 — 一个会话里四份席内记录都把
Implemented-by:写成「前置一个词」,四份全被 C4判 HALF WRITTEN,其中两份已经进了
main,那两条 head 上等于「没有复核记录」。规则一直写在散文里,四个连续的读者都没读出来;本仓其他机读字段的标准解法是「照抄模板」,唯独这一份没有模板可抄。
风险与代价(含回滚) — 只动两个文件,都不发布,不影响任何运行时;新增的是一个只打印、不读板子的
flag,和 23 条自测。代价是规则行里 「可前置 N/N」 换成了模板指针 —— 该条的强制逻辑在检查器里没动,
说明移进了模板的图例并被自测钉住。回滚 = revert 本 PR,没有迁移、没有数据、没有下游依赖。
席位意见 —
你要做的 — 确认这条取舍:规则层用 16 字节的「可前置 N/N」换 25 字节的模板指针,是否接受。
若接受,本 PR 无其他待决项;另有一条超范围发现(记录里 head sha 的码段拼法)已写在上一节,请代立卡。
Clause-②: no — the diff adds no key, arm, export or registration on any declared surface;
check-widening-tells --declaration nojudged 2 changed file(s), 0 against a declared surface.Generated by Claude Code
Generated by Claude Code