Commit ecf91cb
fix(pm): bind H31's carrier pair through the closing keyword (#18243)
Fixes #18229
## What changed
`scripts/pm/check-half-states.mjs` only. H31 compares the two carriers
of the
`needs:contract-review` gate, and it took `prDeliversCard` — the wide
body-first/branch-fallback delivery relation — as its population. That
relation
counts a `Part of #N` line, which declares MEMBERSHIP in what #N tracks
rather
than a delivery of it, so an epic tracker whose sub-PRs are gated
correctly drew
a row saying the gate was missing from the card half of a dual carrier.
The
row's remedy text is action-shaped, so acting on it hangs the gate on a
card
nothing will ever close — where the stroke that clears gates never
arrives.
Three edits:
1. **`bindingClosesCard(pr, n)`** — a new exported predicate, one read
of
`deliveryEvidence`'s existing grading, never a second keyword parser. It
answers "does this PR CLOSE #n", which is the question the gate's
clearing
stroke rides. `part-of`, `part-of-inline` and `branch-name` all answer
false.
2. **H31's own population filter** — the carrier comparison runs over
the PRs
`bindingClosesCard` accepts. Those rows are byte-identical to before.
3. **A DECLINED row for every other binding** — a weak-bound delivering
PR whose
carrier differs from the card's still produces a row, one that names
both
carriers and the binding it read, states that it declined to judge, and
prescribes no write. A weak-bound PR whose carrier agrees produces
nothing,
exactly as before.
`TRACKING_ANCHOR_LABEL` is extracted so the ruling-anchor state has one
spelling
across H13's exemption list and H31's new clause.
## Why this seam, and how the shared invariant survives
The shared relation is NOT narrowed. `prDeliversCard`'s docblock forbids
it
(「⛔ Do not narrow it here to serve H8: that would make the live half
invisible
to the rows that exist to see it」), and H8's open side, H35's sibling
resolver,
`claimDelivery`, H53 and the `check-clause2-carriers` pairing all still
read it
wide. The narrowing is H31's own filter over the population that
relation hands
it, so the rows still agree about which PR delivers which card and
differ only
about which binding makes a CARRIER PAIR — a question only H31 asks.
Both
docblocks that state the invariant were updated to say so rather than
left to
imply the old thing.
Silently dropping the weak bindings would have been #4690 in this row's
own
uniform: a split that was never judged renders identically to a board
whose two
carriers agree, and this is the one row that can tell 「被剥」 from 「从未挂过」.
Hence the declined row, and hence its scope: it speaks only where the
carriers
actually differ, because a standing row per tracker per sweep is the
disease,
not the cure. The declined row is deliberately NOT marked
`UNJUDGED_MARKER` —
that marker buys trim priority ahead of judged rows, and a decline must
never
sort ahead of a real carrier split in the same `gate` band.
## Measured on the live board
Anchor #9857's sweep at 2026-09-15T01:57Z, commit `b3b43b6`, run
34919049964,
carries exactly one H31 row: #14122 (`tracking`), naming open PR #18212
(draft,
via a `Part of` declaration). Replayed offline against the 9 open PRs
and the 9
open cards those PRs name (the exact H31 candidate set — a card no open
PR names
can never reach the comparison):
| card | `tracking` | delivering evidence | row before | row after |
|---|---|---|---|---|
| #18122 | no | #18238 closing-keyword | none | none |
| #17502 | no | #18231 closing-keyword | none | none |
| #17598 | no | #18230 closing-keyword | none | none |
| #15410 | no | #18227 closing-keyword | none | none |
| #18202 | no | #18212 closing-keyword | none | none |
| #14122 | **yes** | #18212 **part-of** | action-shaped finding |
**DECLINED row** |
| #17396 | no | #18198 closing-keyword | none | none |
| #17356 | no | #18131 closing-keyword | none | none |
| #17097 | no | #18096 closing-keyword | none | none |
One row changes, and it is the expected one. Every other delivering
binding on
the live board is a closing keyword, so nothing else moves.
## Tests
`pnpm check:pm-half-states` (= `node scripts/pm/check-half-states.mjs
--self-test`): **4042 cases pass** before, **4075 cases pass** after,
exit 0 —
33 new cases.
The pin and the control both land, per the card's own remedies:
- pin: the `Part of`-only tracker still produces a row (⛔ not a silent
skip),
the row says `DECLINES to judge`, names the binding it read, explains
why a
ruling anchor can never clear a gate, and prescribes NOTHING; it is
asserted
to be neither the old action-shaped sentence nor a LOUD row nor an
UNJUDGED-ranked one.
- control: a closing-keyword binding whose card lacks the gate — the row
STANDS,
with the unchanged action-shaped sentence.
- plus: agreement on a weak binding stays clean both ways; a weak-bound
card
without `tracking` still reports the split but without the ruling-anchor
clause; an adjudicable split outranks a decline and names only the
closing-bound PR.
**Ablation** (one-off, restored; run from the committed fix). Mutating
`bindingClosesCard` back to the pre-fix wide population
(`deliveryEvidence(pr, n) !== null`) turns the self-test RED:
```
== anchor counts BEFORE == old-text occurrences: 1 / new-text occurrences: 0
mutation written
== anchor counts AFTER == old-text occurrences: 0 / new-text occurrences: 1
HEAD blob=dca4e1dbff5d6a46d10c5ac53a527b888e5f0811
MUT blob=906ac10ab7837ef5dc746f1bb3ccda8740e88c75
VERDICT ablation-exit=1
✗ check-half-states self-test: 11 of 4075 case(s) failed.
== restore leg ==
REST blob=dca4e1dbff5d6a46d10c5ac53a527b888e5f0811
old-text occurrences after restore: 1 / new-text occurrences after restore: 0
git diff HEAD bytes: 0
```
The mutation is proved on disk by the blob hash moving off the HEAD blob
and by
the two anchor occurrence counts flipping; the restore leg is proved by
the blob
hash returning to the HEAD blob and by an empty `git diff HEAD`, not by
an exit
code. There is no build step and no `dist/` for a repo-root `.mjs`, so
the
rebuild half of the ablation preflight does not apply here.
**Gates** — derived with no paths, at head `e0a4efa`:
```
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
-> 41 command(s)
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RECORD_FILE
-> Run reconciliation — 41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN.
EXIT CODES — all 41 accounted famil(ies) carry one, so the NOT-MEASURED
count above is DERIVED from them.
```
All 41 exited 0, each captured by redirect before any pipe. The gate
script's
own wiring is inside that set: `pnpm check:pm-half-states` is the
package.json
step lint.yml runs, and it is the script's `--self-test`.
**Lint** — a declared narrowing, not the farm. `eslint
scripts/pm/check-half-states.mjs
--no-inline-config --format json` at head `e0a4efa`: **1 file linted, 0
errors, 0
warnings**, exit 0. The population is read from eslint's own config, not
guessed
— an `ESLint` instance over `git ls-files` reports `tracked=8674
in-scope=6755
ignored=1919 no-rules=0`. Invariance: this repo runs one
`eslint.config.mjs`
which enables type-aware linting for no file at all (every
`parserOptions` in it
is exactly `{ ecmaVersion: 'latest', sourceType: 'module' }`; zero
`project`/`projectService` hits; the config says so itself at its
`QUERY_OPTIONS_TEST_GLOBS` note), and that config is not in this diff —
so no
rule's verdict on a file this PR did not touch can move because of it.
The
repo-wide `pnpm lint` run is CI's.
`skip-changeset`: `scripts/pm/**` is PM tooling and ships in no
package's
`files[]`, so nothing published moves.
## Acceptance notes
- `scripts/pm/check-half-states.mjs` is non-governed PM tooling, so this
is
in-seat review. `.github/workflows/half-state-patrol.yml` and anchor
#9857 are
untouched: the workflow runs the script from `main`, so the fix reaches
the
anchor at the first scheduled sweep after landing.
- #18214 is the sibling shape one file over — the clause-② dual carrier
that
`scripts/pm/check-clause2-carriers.mjs --pair` demands on an epic
tracker
reached through a `Part of` line. It is out of scope here and remains
open.
`bindingClosesCard` is exported for it: that file already imports
`deliveryEvidence` and `prDeliversCard` from this one, so the fix there
is an
import rather than a second copy of the predicate.
- noted, not filed: the sibling install of this script in `objectui`
(`scripts/pm/check-half-states.mjs`) has already drifted from this copy
(different md5 at `b3b43b6`), while this file's header describes the
pair as
copied VERBATIM. No gate can hold two repos equal, so the wording is an
aspiration rather than a claim — carrier: the next dev dispatched on a
cross-repo patrol card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 75237a9 commit ecf91cb
1 file changed
Lines changed: 214 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2155 | 2155 | | |
2156 | 2156 | | |
2157 | 2157 | | |
| 2158 | + | |
| 2159 | + | |
| 2160 | + | |
| 2161 | + | |
| 2162 | + | |
| 2163 | + | |
| 2164 | + | |
2158 | 2165 | | |
2159 | 2166 | | |
2160 | 2167 | | |
| |||
3014 | 3021 | | |
3015 | 3022 | | |
3016 | 3023 | | |
| 3024 | + | |
| 3025 | + | |
| 3026 | + | |
| 3027 | + | |
| 3028 | + | |
| 3029 | + | |
| 3030 | + | |
| 3031 | + | |
| 3032 | + | |
| 3033 | + | |
| 3034 | + | |
| 3035 | + | |
3017 | 3036 | | |
3018 | 3037 | | |
3019 | 3038 | | |
3020 | 3039 | | |
3021 | 3040 | | |
3022 | | - | |
| 3041 | + | |
3023 | 3042 | | |
3024 | 3043 | | |
3025 | 3044 | | |
| |||
6045 | 6064 | | |
6046 | 6065 | | |
6047 | 6066 | | |
| 6067 | + | |
| 6068 | + | |
| 6069 | + | |
| 6070 | + | |
| 6071 | + | |
| 6072 | + | |
| 6073 | + | |
| 6074 | + | |
| 6075 | + | |
| 6076 | + | |
| 6077 | + | |
| 6078 | + | |
| 6079 | + | |
| 6080 | + | |
| 6081 | + | |
| 6082 | + | |
| 6083 | + | |
| 6084 | + | |
| 6085 | + | |
| 6086 | + | |
| 6087 | + | |
| 6088 | + | |
| 6089 | + | |
| 6090 | + | |
| 6091 | + | |
| 6092 | + | |
| 6093 | + | |
| 6094 | + | |
| 6095 | + | |
| 6096 | + | |
| 6097 | + | |
| 6098 | + | |
| 6099 | + | |
| 6100 | + | |
| 6101 | + | |
| 6102 | + | |
| 6103 | + | |
| 6104 | + | |
6048 | 6105 | | |
6049 | 6106 | | |
6050 | 6107 | | |
| |||
6054 | 6111 | | |
6055 | 6112 | | |
6056 | 6113 | | |
| 6114 | + | |
| 6115 | + | |
| 6116 | + | |
| 6117 | + | |
| 6118 | + | |
| 6119 | + | |
| 6120 | + | |
| 6121 | + | |
| 6122 | + | |
| 6123 | + | |
| 6124 | + | |
| 6125 | + | |
| 6126 | + | |
| 6127 | + | |
| 6128 | + | |
| 6129 | + | |
| 6130 | + | |
| 6131 | + | |
| 6132 | + | |
| 6133 | + | |
| 6134 | + | |
| 6135 | + | |
| 6136 | + | |
| 6137 | + | |
| 6138 | + | |
6057 | 6139 | | |
6058 | 6140 | | |
6059 | | - | |
| 6141 | + | |
| 6142 | + | |
| 6143 | + | |
| 6144 | + | |
| 6145 | + | |
| 6146 | + | |
| 6147 | + | |
| 6148 | + | |
| 6149 | + | |
| 6150 | + | |
6060 | 6151 | | |
6061 | 6152 | | |
6062 | 6153 | | |
| |||
6076 | 6167 | | |
6077 | 6168 | | |
6078 | 6169 | | |
6079 | | - | |
6080 | | - | |
| 6170 | + | |
| 6171 | + | |
| 6172 | + | |
| 6173 | + | |
| 6174 | + | |
| 6175 | + | |
| 6176 | + | |
| 6177 | + | |
| 6178 | + | |
6081 | 6179 | | |
6082 | 6180 | | |
6083 | 6181 | | |
| |||
6107 | 6205 | | |
6108 | 6206 | | |
6109 | 6207 | | |
6110 | | - | |
| 6208 | + | |
| 6209 | + | |
| 6210 | + | |
| 6211 | + | |
| 6212 | + | |
| 6213 | + | |
| 6214 | + | |
| 6215 | + | |
| 6216 | + | |
| 6217 | + | |
| 6218 | + | |
| 6219 | + | |
| 6220 | + | |
| 6221 | + | |
| 6222 | + | |
| 6223 | + | |
| 6224 | + | |
| 6225 | + | |
| 6226 | + | |
| 6227 | + | |
| 6228 | + | |
| 6229 | + | |
| 6230 | + | |
| 6231 | + | |
| 6232 | + | |
| 6233 | + | |
| 6234 | + | |
6111 | 6235 | | |
6112 | 6236 | | |
6113 | 6237 | | |
| |||
25803 | 25927 | | |
25804 | 25928 | | |
25805 | 25929 | | |
25806 | | - | |
| 25930 | + | |
| 25931 | + | |
25807 | 25932 | | |
25808 | 25933 | | |
25809 | 25934 | | |
| |||
25825 | 25950 | | |
25826 | 25951 | | |
25827 | 25952 | | |
| 25953 | + | |
| 25954 | + | |
| 25955 | + | |
| 25956 | + | |
| 25957 | + | |
| 25958 | + | |
| 25959 | + | |
| 25960 | + | |
| 25961 | + | |
| 25962 | + | |
| 25963 | + | |
| 25964 | + | |
| 25965 | + | |
| 25966 | + | |
| 25967 | + | |
| 25968 | + | |
| 25969 | + | |
| 25970 | + | |
| 25971 | + | |
| 25972 | + | |
| 25973 | + | |
| 25974 | + | |
| 25975 | + | |
| 25976 | + | |
| 25977 | + | |
| 25978 | + | |
| 25979 | + | |
| 25980 | + | |
| 25981 | + | |
| 25982 | + | |
| 25983 | + | |
| 25984 | + | |
| 25985 | + | |
| 25986 | + | |
| 25987 | + | |
| 25988 | + | |
| 25989 | + | |
| 25990 | + | |
| 25991 | + | |
| 25992 | + | |
| 25993 | + | |
| 25994 | + | |
| 25995 | + | |
| 25996 | + | |
| 25997 | + | |
| 25998 | + | |
| 25999 | + | |
| 26000 | + | |
| 26001 | + | |
| 26002 | + | |
| 26003 | + | |
| 26004 | + | |
| 26005 | + | |
| 26006 | + | |
| 26007 | + | |
| 26008 | + | |
| 26009 | + | |
| 26010 | + | |
| 26011 | + | |
| 26012 | + | |
| 26013 | + | |
| 26014 | + | |
| 26015 | + | |
| 26016 | + | |
| 26017 | + | |
| 26018 | + | |
| 26019 | + | |
| 26020 | + | |
| 26021 | + | |
| 26022 | + | |
| 26023 | + | |
| 26024 | + | |
| 26025 | + | |
| 26026 | + | |
| 26027 | + | |
| 26028 | + | |
| 26029 | + | |
| 26030 | + | |
| 26031 | + | |
| 26032 | + | |
| 26033 | + | |
| 26034 | + | |
| 26035 | + | |
25828 | 26036 | | |
25829 | 26037 | | |
25830 | 26038 | | |
| |||
0 commit comments