Uh oh!
There was an error while loading. Please reload this page.
check-half-states self-test: route every nullable predicate row through a wrapper, and delete the 14 literal stray spaces - #12390
Merged
Conversation
…ssertion through a row-text wrapper The self-test's message assertions read `predicate(...).includes(needle)` on predicates that are three-valued by design (null when clean, a string when the row fires). The moment a change makes such a predicate go clean, that line throws `TypeError: Cannot read properties of null (reading 'includes')` while evaluating t()'s arguments — before t() runs — so the suite ABORTS there, every later case never runs, and the output names a TypeError instead of a row. Generalizes the per-predicate wrapper PR #12272 established for H34 (`const h34row = (...args) => String(fn(...args) ?? '')`) to the remaining 18 predicates, plus the two local nullable helpers (`halves`, `dead27`) that carry the same shape. 145 call sites switched; predicates themselves are unchanged and their `typeof`/`=== null` assertions deliberately stay un-wrapped. Self-test output is byte-identical to before: 1380 cases pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
…nullable-.includes population The card's table counted only DIRECT call expressions (`pred(...).includes(`). A second population reaches the same throw through a local const — `const h9NoLine = h9OnHoldNoRestartWhen(...)` then `h9NoLine.includes(...)` — which a call-expression scan cannot see. Measured on the pristine base: 108 such sites across 29 variables, spanning four predicates the card never named (h20, h21, h23, h28). This half is what actually kept the suite aborting: with population A alone fixed, the H9 ablation still died at the first `h9NoLine.includes`. Two shapes, chosen per variable: - no nullability assertion on the const -> wrap the DECLARATION in place (19 variables, zero call-site churn) - `typeof X` / `X === null` asserted -> keep the raw const and add a `XRow` sibling (10 variables, 56 sites switched), because stringifying in place would make those assertions trivially true Audit now reports 0 unsafe receivers in both populations. Self-test output remains byte-identical: 1380 cases pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
…jacent ASCII mark #12081 measured a LINE-END event (a soft break after an ASCII , ; : between Han characters). A faithful re-wrap preserves that space as a literal ", " in source, where the line-end predicate can no longer see it — so the defect migrates rather than disappears. Re-derived on this tree with the card's predicate (literal Han + ASCII ,;: + space + Han, one line, excluding fenced / front-matter / multi-line ruling regions via the ratchet's own state machine): 7 + 6 + 1 = 14, reproducing the card's table exactly. All 14 deleted. Each site has a same-line sibling using the identical mark with NO space (实测存在,churn · 判据的,PM · 哈希:空 · 它不是,它), which is what makes the judgment mechanical rather than stylistic. The enumeration-marker family (semicolon + space + a circled mark such as U+2462 / U+2463) is left untouched — measured, it falls OUTSIDE the predicate by construction (the successor is not Han), so all 6 such sites survive unchanged. Byte delta is exactly -14; line counts are unchanged, so no CEILINGS row moves and no table row widens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
os-zhuang
marked this pull request as ready for review
August 26, 2026 00:06
os-zhuang
enabled auto-merge
August 26, 2026 00:06
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#12273, closes#12265
Two disjoint cards, folded into one PR by the skills-lane PM: a self-test
robustness fix in
scripts/pm/check-half-states.mjs, and a typography pass overthree governed corpus files. The file sets do not overlap.
⛔ Draft, governed surface. The diff touches
.claude/**, so under PrimeDirective 14 this is for the maintainer to land by hand: never flipped ready,
never queued, no auto-merge armed.
Card 1 — self-test nullable-row robustness
What the defect is
The self-test's message assertions read
predicate(...).includes(needle)onpredicates that are three-valued by design —
nullwhen the card is clean,a string when the row fires. The moment a change makes such a predicate go
clean, that line throws:
The throw happens while evaluating
t()'s arguments, beforet()runs, sono harness-level catch can turn it into a case. And because
t()only pushesonto a
casesarray that is printed in one loop at the very end, a single throwmeans the suite prints nothing at all — not a partial run.
The card counted direct call expressions (
pred(...).includes(). Re-derivedmechanically on the base commit, that population reproduces exactly: 111
bare sites across the 18 tabled predicates. But it is not the whole shape.
h9OnHoldNoRestartWhen(hold()).includes('x')const h9NoLine = h9OnHoldNoRestartWhen(...)… thenh9NoLine.includes('x')Population A's 145 = the card's 111 bare, plus 4 already written
?.includes(safe from the throw, but reporting
undefinedrather than a cleanfalse),plus 30 reached through two local helpers the table did not name (
halveswrapping H8,
dead27wrapping H27).Population B is invisible to any call-expression scan, and it spans four
predicates the card never mentions — h20, h21, h23, h28. It is also what
actually mattered: with population A alone fixed, the ablation below still
aborted, at
h9NoLine.includes('cannot parse').The fix
Per the triage direction: the per-predicate wrapper pattern PR #12272
established for H34,
const h34row = (...args) => String(fn(...args) ?? ''),generalized. Not a
t()overload — the throw precedest(), so the harnesscannot catch it.
h4row…h33row) declared in one block at the top ofselfTest, plushalvesRowanddead27Rowbeside their own local helpers.145 call sites switched.
nullability assertion had their declaration wrapped in place (zero
call-site churn); the 10 that are asserted with
typeof X/X === nullkeptthe raw const and gained an
XRowsibling (56 sites switched) — stringifyingthose in place would have made their own assertions trivially true.
directly. Every
typeof pred(...) === 'string'andpred(...) === nullcasedeliberately does not go through a wrapper.
An audit deriving the nullable set from the source (functions containing
return null) rather than from a hand-kept list now reportsTOTAL A = 0,TOTAL B = 0.Acceptance (a) — full self-test green, case count unchanged
node scripts/pm/check-half-states.mjs --self-test→ exit 0:Stronger than "green": the run's entire stdout is byte-identical to the
pre-change baseline — same 1380 case names, same verdicts, same order. No case
changed its expected verdict, because no case changed its observable value.
Acceptance (b) — the ablation
The card quotes a TypeError abort from widening
CLAIM_COMMENT_MARKERto acceptU+2014. Re-run here on both the pristine base and the fix, that mutation now
behaves identically on both sides:
All 12 failures sit in the H33/H34 blocks — the rows PR #12272 had already
wrapped. The abort the card recorded was observed before that wrapper
landed; once it did, this leg stopped aborting. So the card's before/after shape
could not be demonstrated with this mutation, and forcing it would have produced
a fabricated contrast.
Substitute leg, exercising the population the card is actually about
Mutation: invert H9's fireable-
Restart-whenguard — one token, in a block with14 direct sites and 6 more behind
h9NoLine. Every H9 fixture with no fireableexit now returns
null: "the row goes clean", the exact regression class thecard describes. Expected direction fixed before running: before → TypeError
abort; after → named failing cases, suite completes.
BEFORE (pristine base
7986d973f), verbatim:→ cases printed: 0 · named failing cases: 0 · summary line: absent.
The suite reports nothing about the other 1379 cases.
AFTER (this branch):
→ cases printed: 1381 · named failing cases: 45, every one an
H9row ·TypeError lines: 0. The suite runs to completion and names what broke.
Both legs proved the mutation on disk before running (anchor present → 1,
mutant present → 1, original absent → 0) and restored under
trap … EXIT INT TERM; restore was verified afterwards (git status --porcelainempty, anchor back to 1, mutant 0).Acceptance (c) — companion sweep of sibling
scripts/pm/*.mjsSame two-population audit run over all 13 sibling scripts.
check-half-states.mjsis the only file that carries the shape.
check-half-states.mjs(this card)dispatch-gates.mjscheck-governed-merges.mjsgit-history.mjsci-failure.mjscheck-skill-line-ratchet.mjsrelease-rehearsal-clone.mjsbare-root-worklist.mjs·check-governed-prose.mjs·check-label-desc-cap.mjs·check-skill-id-lint.mjs·check-governed-queue-guard.mjscheck-dispatch-gates.mjsselfTestfunction (it drives the tool's own--self-test)The instrument was validated against a known-positive control before the
zeros were believed: run against the pristine base of
check-half-states.mjsitreturns
popA=111, reproducing the card's headline number exactly. Nothing tofold, and no finding to file — the sweep's answer is genuinely "only here".
Card 2 — literal stray spaces
Re-derived on this tree with the card's predicate (literal
Han+ ASCII,;:+ space +Han, within one line), using the exclusions #12263 used —fenced regions, YAML front matter, and multi-line 「…」 rulings — taken from the
ratchet's own
initialState/advanceStaterather than reimplemented.Result: 7 + 6 + 1 = 14, reproducing the card's table exactly. All 14
deleted; byte delta exactly −14; line counts unchanged, so no line merges,
no CEILINGS row moves, and no table row is touched.
The direction anticipated that ~3 of the 14 would be enumeration-marker sites to
leave. Measured, none of them is: the predicate requires a Han
successor, and a circled mark (U+2462
③/ U+2463④) is not Han, so everyenumeration site falls outside the population by construction — as the card
itself noted about its line-end sibling. The judged-deliberate family is
therefore preserved without any of it needing to be excluded by hand.
What makes each of the 14 mechanical rather than stylistic: every one has a
same-line sibling using the identical mark with no space —
实测存在,churn,判据的,PM,哈希:空,它不是,它. The house style is settled on the same lineas the defect.
Per-site judgment — fixed
platform-readings.md:4;platform-readings.md:55;存在,churn— no spaceplatform-readings.md:198,platform-readings.md:274,platform-readings.md:276,判据的,PM— no spaceplatform-readings.md:306,platform-readings.md:313;盲区:窗口— no spaceos-dev.md:210,os-dev.md:232,推进它:— no spaceos-dev.md:253,哈希:空,失败;不匹配— no spaceos-dev.md:263,三种处置,不是— no spaceos-dev.md:337:os-dev.md:376:它不是,它— no spaceSKILL.md:127:Per-site judgment — kept
platform-readings.md:56;+ U+2463platform-readings.md:275;+ U+2462SKILL.md:883;+ U+2462platform-readings.md:26;+ inline codeos-dev.md:230:+ inline codeos-dev.md:236,+ inline code⛔ No new gate, per the direction:
hanAsciiPunctTail(landed in #12263)already prevents new line-end sources, literal authorship is rare, and the
residual risk is accepted and recorded on the card rather than priced into a
permanent scanner.
Gates
Derived from the real change set —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which read the 4 changed paths from the merge baseitself (three-dot) rather than from a hand-written list — and run at the final
commit
c5e68a4d5. Exit codes captured by redirecting to a file first, neverthrough a pipe. 24 families matched; all 24 green, each quoted from the
gate's own verdict line:
check:pm-half-states✓ check-half-states self-test: 1380 cases pass.check:pm-skill-ratchet✓ …os-dev.md is 461 lines (ceiling 470; headroom 9).check:pm-skill-ratchet✓ …platform-readings.md is 314 lines (ceiling 314; headroom 0).check:pm-skill-ratchet✓ …pm-dispatch/SKILL.md is 1005 lines (ceiling 1005; headroom 0).check:pm-skill-ratchet✓ …pm-dispatch/SKILL.md: widest table row is 765 bytes (pin 765; headroom 0).check:pm-skill-ratchet✓ …os-dev.md: widest table row is 0 bytes (pin 0; headroom 0).check:pm-skill-ratchet✓ …platform-readings.md: widest table row is 0 bytes (pin 0; headroom 0).check:doc-formula-expressions✓ …22 record-scoped formula example(s) across 422 files / 1449 TS blocks judged cleancheck:agent-model-declared·check:agent-test-spelling·check:bash32-floor·check:cli-command-ids·check:cross-package-test-inputs·check:doc-authoring·check:entry-guard·check:nul-bytes·check:parse-guard·check:partof-closing-keyword·check:pm-governed-merges·check:pm-governed-prose·check:pm-skill-id-lint·check:pnpm-filter-targets·check:skill-frame-sync·check-ci-filter-parity·check-closing-keyword-parity·check-cross-package-test-inputs·check-governed-queue-guardThe MAX_TABLE_ROW_BYTES pins merged in #12263 are respected: the two files with
a
0pin have no table rows, andSKILL.mdsits at 765/765 — unchanged, sincethis diff deletes one space from a prose line and touches no table row.
Two gates initially reported red and both said so themselves: the
doc-formula-expressionsgate exited on an unbuilt-workspace prerequisite("Nothing was measured … It is NOT a finding"), green after building
@objectstack/formulaand@objectstack/lint; andcheck-partof-closing-keyword.mjsrefused for want of
PR_BODY("a wiring or usage failure, NOT a verdict") — itis CI-wired to this PR's body and was additionally run locally against this text
before publication.
skip-changeset: the diff is.claude/**plus onescripts/pm/self-testharness. Nothing published to npm changes, so no changeset is owed.
Generated by Claude Code