Repository navigation
Commit 14d2544
pm(half-states): H19 founds every verdict on the set it judges (#18654)
Fixes #18379
H19 gathered `Blocked-by:` targets over the **body ∪ comment** union and
judged the expiry against the **body's** set (#17564), but the
`UNJUDGED` branch never moved with that split: it stayed founded on the
**resolution** set, so a target a seat had **superseded in the body**
could still veto the verdict from the comment archive whenever its
number was **unreadable**. Two conditions had to coincide, which is why
it outlived #17564 — the target is superseded (judging correctly ignores
it) **and** the superseded number 404s (resolution cannot dismiss it
either) — and the resulting row was **unclearable by construction**: the
card's owner had already done the prescribed thing, and the only
remaining lever, editing the archived comments, is the write the
protocol forbids because those comments record what an earlier seat
measured and why the line was migrated.
_Authored by `session_01Gqi43smmqjJ5sUrhfoPeKu` (Claude Code). Every
reading below was taken on this branch at **`ab30706ed`**, the head this
body describes._
## The two sets, before and after
`blockerTargetsFor` is untouched: DISCOVERY stays a union, because H26
and H28 share those resolutions and a comment-borne target is a
perfectly good answer to what they ask. What changed is inside
`h19BlockOutlivedBlocker`.
| reading | may FOUND a row — before | may FOUND a row — after |
|:--|:--|:--|
| CLOSED, body-borne (or comment-borne on a body-less card) | yes | yes
(unchanged) |
| CLOSED, comment-only beside a body set | no — hygiene clause | no —
hygiene clause (unchanged) |
| UNRESOLVED, body-borne (or on a body-less card) | yes — `UNJUDGED` |
yes — `UNJUDGED` (unchanged) |
| **UNRESOLVED, comment-only beside a body set** | **yes — the
unclearable row** | **no — named in its own clause** |
| OPEN, either channel | no — withholds only | no — withholds only
(unchanged) |
The denominator moved with it: an archive-only unreadable target no
longer enters the row's `N of M` count, which was the other half of the
sentence the card quotes (「1 of 2 … the set judged against the BODY's
set」 on a card whose body names one target).
**What the archive is still consulted for, and must be:** evidence that
the wait may still be **RUNNING**. An OPEN target it names still
withholds a discharge, and an UNREADABLE one now withholds a **FULL**
discharge in a new branch (「No target this row JUDGED is still open, but
N target(s) the comment archive names … could not be read at all」).
Narrowing that half to the body too would re-create **#11747** — the
measured mirror defect, where a re-park parks the live blocker in a
comment while the body still names the spent one, and a card was
released to `pm:queue` while its real blocker was open and dispatched.
Only the power to FOUND a verdict is carrier-scoped; evidence of a live
wait never is.
⛔ `UNJUDGED` is **not** silenced. Three controls say so live, below.
## Pins
Battery **`H19 judged-set founding`** — the file's second registered
battery (`SELF_TEST_BATTERIES`), **37 cases registered, pinned at 34**;
`SELF_TEST_BATTERY_FLOOR` raised `1` → `2`, so deleting either battery
now fails the roster rather than shrinking a total nobody reads.
It floors a NEGATIVE — a row that must not fire — plus the controls that
keep the silence scoped:
- the instance from the card, in its recorded shape (#11975: body
refreshed to `Blocked-by: #18336`, the archive naming `#13515`, resolved
404) ⇒ discovery still yields both targets, and **no row of any kind**;
- the same shape generically, cross-repo and local ⇒ no row;
- CONTROL ①: a body-borne unreadable target ⇒ `UNJUDGED` fires, counted
`1 of 1`, still refusing to be read as a quiet row;
- CONTROL ②: a body-less card ⇒ still judged from comments, `UNJUDGED`
fires;
- CONTROL ③: a closed body target ⇒ H19's ordinary row, `1 of 1`, full
discharge;
- CONTROL ④: #11747's mirror ⇒ an OPEN archive target still withholds,
still `1 of 2`, still `PARTIAL`;
- the withholding path: an archive-only unreadable target beside a
closed body target ⇒ the row fires on the body target, is ⛔ not
published as a full discharge, and names the archive target in a clause
saying it founds nothing;
- ⛔ H26/H28 unaffected: the union still discovers both channels.
**Two existing pins were re-judged rather than deleted**, and both are
called out where they sit:
1. `H19 precedence: an UNRESOLVED comment-borne target is NOT silenced
by the body` asserted exactly the row this card measures as unclearable.
Same fixture, re-judged to the direction the file's own asymmetry table
always declared: it now founds **no** row beside a body set.
2. `H19 superseded: the clause rides the UNJUDGED branch as well` used a
fixture whose body named a target that appeared in no resolution. Its
unreadable target is now the BODY's own, which is what the pin was
always about: a hygiene clause can only ride a row something in the
judged set founded.
One prose correction rides along: the summary line's H19 shortfall
clause promised that every unresolved target is named on its own card's
row. That clause was measured false once already (#11218) by promising
more than the rows deliver, and a repair that narrows what a row founds
owes it a second correction — it now names the archive-only class it no
longer founds a row for.
## Ablation — from the committed fix, restored by blob hash under a trap
Mutation: the founding filter `state === 'unresolved' && carries(r)`
reverted to `state === 'unresolved'`, on disk, at `HEAD` = `ab30706ed`.
```text
HEAD blob: fe92847
BEFORE: fixed-line count = 1 AFTER: fixed-line count = 0
BEFORE: mutant-line count = 1 AFTER: mutant-line count = 2 (one pre-existing identical line in H4's prose clause)
AFTER: on-disk blob = f764989fbeebeedd2d9920f7dd7678da0abdff1c (differs from HEAD blob)
ABLATED self-test exit: 1 — 8 of 4583 cases failed
RESTORED: blob fe92847 == HEAD blob; git diff HEAD empty
```
Direction: RED, as predicted, and the reds are the right ones — five
negatives plus three counts:
```text
✗ H19 precedence: an UNRESOLVED comment-only target founds NO row beside a body set
✗ H19 #18379: …and the unclearable row is GONE
✗ H19 #18379: …⛔ with no quieter row invented in its place
✗ H19 #18379: a superseded target the archive alone names founds nothing
✗ H19 #18379: …and that holds for a LOCAL superseded number too
✗ H19 #18379 withhold: …and the count stays the judged set's
✗ H19 #18379 clauses: …and neither enters the count
✗ H19 #18379: the UNJUDGED denominator never counts an archive-only target
```
The `got` value on the card's own fixture is the measured sentence,
reproduced verbatim by the ablated code:
> `pm:blocked` and 1 of 2 `Blocked-by:` target(s) could NOT be resolved
this sweep (`#13515` (HTTP 404)) — the set judged against the BODY's
`Blocked-by:` set …
## Self-test
```text
node scripts/pm/check-half-states.mjs --self-test :: exit 0
✓ check-half-states self-test: 4583 cases pass. Batteries: H66 released queue card 183/172, H19 judged-set founding 37/34.
```
Run under `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`), as
were the ablated run and the live sweep.
## Live sweep — report-only, from this worktree, after the fix
`node scripts/pm/check-half-states.mjs` at 2026-09-17T12:25Z, head
`ab30706ed`, under the verify lock (`VERDICT command-exit 0`, held
366s): swept 409 open pm-/p0-labeled issues, 462 half-states, 12 H19
rows.
**Before** — anchor #9857 as the scheduled patrol rewrote it at
`2026-09-17T07:50:06Z` (four H19 rows, all `UNJUDGED`, which sort above
the body's size trim): `11973`, **`11975`**, `15140`, `15214`.
> **H19** #11975 — `pm:blocked` and 1 of 2 `Blocked-by:` target(s) could
NOT be resolved this sweep (`#13515` (HTTP 404)) — the set judged
against the BODY's `Blocked-by:` set …
**After** — this run:
- **#11975 carries no H19 row at all.** Its rows are H11
(important-parked) and H26 (transitive wait on `#18336`) — the two that
were always true of it.
- **#11973 still fires `UNJUDGED` on `#13689` (HTTP 404)** — the same
unreadable epic, reached through that card's **body**. This is the scope
note on the card, and it is the control that proves nothing was
silenced.
- **#15140 and #15214 still fire `UNJUDGED`** on `objectstack-ai/cloud`
targets (HTTP 403, repo not readable to this credential) — the standing
accepted cross-repo class, untouched.
- Ordinary expiry rows still fire (#15642, #15768, #16354): `is CLOSED …
the block has outlived its blocker`.
- The summary clause now publishes the new limit on every run: 「a target
reaching a card ONLY through the comment archive beside a body that
states its own `Blocked-by:` set is outside what H19 judges, so it rides
a row rather than founding one」.
## Gates
Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack
scripts/pm/check-half-states.mjs` (list re-derived after the commit:
identical), every command run, each exit code captured by
redirect-then-`$?`, reconciled with `--ran`:
```text
✓ dispatch-gates --ran: 38 derived familie(s) accounted for — 38 run, 0 NOT-MEASURED
(a DERIVED zero — all 38 recorded an exit code and none of them is 3)
```
All 38 exited **0**. In full:
```text
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --self-test :: exit 0
node scripts/check-ci-filter-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-scripts-symbol-anchors.mjs :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
node scripts/pm/board-snapshot.mjs --self-test :: exit 0
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/sweep-closed-cards.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closing-target-claim :: exit 0
pnpm check:commit-card-trailers :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:partof-closing-keyword :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pm-half-states :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm lint (repo-wide, as PR #18514 did) :: exit 0
```
`skip-changeset`: `scripts/pm/**` is the fast lane — nothing in any
package's `files[]` moves, and the patrol script is not published.
## Acceptance notes
- Diff is one file and confined to H19, its clauses, its summary
sentence and its pins — #18372 (a new reading in the same file) queues
behind this PR untouched.
- objectui's byte-pinned copy is **not** touched; it re-syncs through
objectui#9395.
- Noted, not filed: H26 (`h26BlockOnIndefiniteTarget`) reads the same
union with no carrier split at all, so a target superseded in the body
can still found an indefinite-wait row there. It is a different row with
a different founding question and no measured instance was taken in this
act, so it is an observation, not a card. Carrier: whoever next opens
H26.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 84ad2e1 commit 14d2544
1 file changed
Lines changed: 257 additions & 31 deletions
0 commit comments