Skip to content

fix(pm): key dispatch-gates' dropped-declaration probe on the derived marker roster - #18856

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-18825-dropped-marker-probe-roster
Sep 18, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-18825-dropped-marker-probe-roster

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18825

Clause-②: no

The defect

scripts/pm/dispatch-gates.mjs built its dropped-declaration probe's pattern —
POPULATION_MARKER_LOOKALIKE — out of POPULATION_MARKER_KEYS alone, while the tool reads
six reason-bearing dispatch-gates: keys. So unparsedPopulationMarkers was a probe over
three of them and a silence over the other three: a no-check-families, inherited-population
or self-test-reads line that READS as a declaration and does not PARSE produced exactly what a
file that declares nothing produces — no reason, no refusal, no row, no count. That is #18661's
own sentence, measured on the keys its repair never reached. The expensive member is
self-test-reads: a dropped declaration takes a family out of the derived set, and --ran then
renders its zero-NOT-MEASURED verdict over a set that no longer contains it (#18673's hole, from
the parsing side).

The before-reading, re-derived at the tip

Taken through the exported unparsedPopulationMarkers on origin/main 42f8df1723 (the card's
own reading was on ad1f94e8ec / 05389952e; PR #18822 has landed on this file since, so every
line number and the roster were re-derived rather than inherited). One line each, file =
scripts/x.mjs:

probe line before after
// dispatch-gates: no-path-population reason without separator (CONTROL) 1 row, form // 1 row, form //
-- dispatch-gates: no-path-population -- x (CONTROL, unknown opener) 1 row, form -- 1 row, form --
# dispatch-gates: no-check-families reason without separator 0 rows 1 row, form #
// dispatch-gates: no-check-families -- x (a // line in YAML) 0 rows 0 rows — document content, deliberately
// dispatch-gates: inherited-population a b reason 0 rows 1 row, form //
-- dispatch-gates: self-test-reads a -- x 0 rows 1 row, form --

The two controls are unmoved. The three shapes the old roster never reached now sound by file,
line, form and text. The //-in-YAML shape stays silent on purpose — see below.

The roster, derived

MARKER_LOOKALIKES is keyed on Object.keys(MARKER_REASON_GRAMMARS) — the roster PR #18822
already derives from both grammar builders (REASON_TAIL_MARKER_KEYS + PATH_LIST_MARKER_KEYS)
— never a hand-list. A hand-list here would be a second copy of "which keys carry a reason that
can be dropped", and it would be wrong in the silent direction the moment a seventh key arrived:
the key would parse, and the probe would not know it existed. The self-test holds the two rosters
EQUAL, so neither can grow alone.

The per-key forms

markerLookalikeHead(key) is the counterpart of markerLineHead(key) one question further out:
that one is built from the forms the grammar ACCEPTS, this one from the forms the key's LANGUAGE
has. A key MARKER_KEY_FORMS restricts gets exactly its own openers; every other key keeps the
wide punctuation class (MARKER_LOOKALIKE_OPENER, unchanged bytes).

So on no-check-families a lookalike is a #-form line that fails to parse, and a // or
block-form line in a workflow is document content, not a dropped declaration — YAML has no
other comment, so a line spelled that way was never a comment to have been dropped out of.
Admitting one would report a line the workflow's own parser never treats as a remark, which is
the same reading error the grammar refuses one level up. A consequence stated in the docblock
rather than left to be found: on a restricted key only the missing--- REASON tail half of the
probe exists, because the unrecognised-comment-form half has nothing to find.

unparsedPopulationMarkerRefusal now names that key's admissible forms per row
(markerFormsFor(key)), not the whole roster. A refusal that offered // as the repair for a
dropped workflow line would prescribe a spelling YAML has no comment for: the author would take
the advice, the line would parse as document content, and the declaration would still be dropped
— the same silence, now with the tool's endorsement.

.github/workflows/x.yml:1 declares no-check-families in form # — "..." (no-check-families may be written in: #)
scripts/x.mjs:1 declares wide-population in form ; — "..." (wide-population may be written in: //, #, /**, /*, *)

The pins (all in this file's own --self-test)

  • the lookalike roster and MARKER_REASON_GRAMMARS asserted EQUAL — neither can grow alone;
  • the two population controls, 1 row each, key and form named — unmoved by this card;
  • the card's own three shapes, each one row, by file / line / form / text;
  • derived over the roster rather than written out six times: every key sounds on a dropped
    declaration in its own first admissible form, and every key stays silent on one that parses
    — so a seventh key arrives probed;
  • the //-and-block forms on no-check-families pinned as NOT lookalikes, with the contrast
    beside it (the same four forms on an unrestricted key are one row each) and the positive leg
    (the # form there DOES sound) — the restriction narrows which spellings are lookalikes, it
    does not empty them;
  • the refusal's remedy is the key's own forms: the workflow refusal names # and does not offer
    //; the script refusal names all five.

The census, over the real tree

PR #18822's live census (the .github/workflows reader and the scripts/ maskSelfTests reader)
now retains its corpus, and the LOOKALIKE half is taken over exactly those files:

✓ no live file carries a DROPPED declaration on the three keys outside the population roster
  (37 workflow(s) + 296 script(s) swept; found none)
✓ no live gate source carries a declaration this grammar drops, on any of the 6 reason-bearing keys
  (250 source(s) swept)

Zero is the reading, not a dead instrument — three non-vacuity legs, each putting a dropped
line directly under a LIVE declaration so the corpus, the masking and the line numbering are the
real ones: a # line with no separator under the live no-check-families declaration is refused
by file and line and by # as its remedy; a // line with no -- separator under the live
self-test-reads declaration is found and nothing else is; and the SAME insertion spelled //
in the workflow is found by neither reading. That last one is where a fix that keyed six keys on
the wide opener would go red.

This answers the card's ⛔ Not measured: no live file in this tree carries a dropped declaration
on those three keys today.

The ablation

Leg 1 — the roster, from the committed fix, full suite

MARKER_LOOKALIKES put back on POPULATION_MARKER_KEYS on disk, from the committed tree, under
a trap … EXIT INT TERM restore:

HEAD blob      : 260f6e85df6c62798248bdb3e0fb66e17edcf554
on disk before : 260f6e85df6c62798248bdb3e0fb66e17edcf554   (equal — the mutation is FROM the fix)
anchor occurrences 1 -> 0 · injected occurrences 0 -> 1
on disk after  : 4028201a93c1369bdaeac6fa61449edffcbbaefb   MUTATION LANDED

The FIRST attempt at this leg was a no-op — the mutation step and the anchor had two different
spellings of the same string and nothing reached the disk. It is recorded rather than quietly
retried: the on-disk guard refused it (MUTATION FAILURE: the file on disk did not move — this ablation did NOT run, blob unmoved, exit 9) and the run below is the repaired one, with OLD and
NEW single-sourced.

Ablated suite: ✗ dispatch-gates self-test: 7 of 1848 case(s) failed (lock VERDICT
command-exit 1, held 742s). All seven are pins this PR adds; nothing pre-existing reds:

✗ the lookalike roster IS the derived grammar roster …
✗ and the three keys the old roster never reached now SOUND, each by file, line, form and text …
✗ every key in the roster sounds on a dropped declaration written in its own first admissible form …
✗ and the `#` form on `no-check-families` is the one that DOES sound …
✗ the refusal prescribes the forms of the KEY it names and not the whole roster …
✗ and that sweep is not vacuous: put a `#` line with no separator under the LIVE `no-check-families` declaration …
✗ and the same in a SCRIPT, on the path-list key: a `//` line under the LIVE `self-test-reads` declaration …

The population CONTROL pin stays GREEN under the ablation, which is the point of it; so does the
census row itself (0 either way — that row is a liveness reading, and its three non-vacuity legs
are what red).

RESTORE  head-blob=260f6e85df6c62798248bdb3e0fb66e17edcf554  on-disk=260f6e85df6c62798248bdb3e0fb66e17edcf554
git diff HEAD: empty · git status --porcelain: empty

Leg 2 — the per-key FORM restriction (targeted reading, NOT a suite run)

Said by name: this leg is a probe-level reading through the exported function, not a run of the
1,848-case battery. The six-key roster kept, the per-key restriction dropped
(const opener = MARKER_KEY_FORMS[key]const opener = false), on disk from the committed fix,
blob 260f6e85…d88df903…, same trap:

1 row(s) :: // dispatch-gates: no-check-families -- x
1 row(s) :: /* dispatch-gates: no-check-families -- x
1 row(s) :: # dispatch-gates: no-check-families reason without separator

That is the triage's second blind spot, reproduced: a fix that widened the roster and kept the
wide opener reports two lines of workflow DOCUMENT CONTENT as dropped declarations. With the
restriction in place the first two are 0 rows and the third is 1 — the pin
「a // or block-form line on no-check-families is DOCUMENT CONTENT …」 is what would red.
Restored and verified by blob hash; git diff HEAD empty.

Gates

Derived from the worktree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no hand-fed path list), every derived command run, exit code captured
redirect-then-$?, reconciled with --ran:

exit command
0 node scripts/check-ci-filter-parity.mjs
0 node scripts/check-closing-keyword-parity.mjs
0 node scripts/check-closing-keyword-parity.mjs --self-test
0 node scripts/check-comment-mask-corpus.mjs
0 node scripts/check-declaration-mirrors.mjs
0 node scripts/check-declaration-mirrors.mjs --self-test
0 node scripts/check-scripts-symbol-anchors.mjs
0 node scripts/check-scripts-symbol-anchors.mjs --self-test
0 node scripts/check-self-test-wired.mjs
0 node scripts/check-self-test-wired.mjs --self-test
0 node scripts/check-self-test-workflow-commands.mjs
0 node scripts/check-self-test-workflow-commands.mjs --self-test
0 node scripts/check-whole-set-label-write.mjs
0 node scripts/check-whole-set-label-write.mjs --self-test
0 pnpm check:agent-test-spelling
0 pnpm check:bash32-floor
0 pnpm check:cli-command-ids
0 pnpm check:cross-package-test-inputs
0 pnpm check:declared-population-live
0 pnpm check:driver-memory-census
0 pnpm check:entry-guard
0 pnpm check:nul-bytes
0 pnpm check:parse-guard
0 pnpm check:pnpm-filter-targets
0 pnpm check:ratchet-remedy-authority
0 pnpm check:refd-timer-probe
0 pnpm check:watch-hint-literal
0 pnpm check:pm-dispatch-gates — the same run as the self-test line below, taken under the shared verify lock

--ran reconciliation (exit codes recorded as command :: exit code, $? captured before any
pipe): 28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED zero — all 28 recorded an exit code and none of them is 3), exit 0.

Self-test line (detached, waited on with tail --pid, never under a foreground timeout):
✓ dispatch-gates self-test: 1848 cases pass — lock VERDICT command-exit 0, held 745s (12m25s).

Repo-wide pnpm lint (eslint . --no-inline-config): exit 0, no narrowing.

Tier, re-derived in the worktree: Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s).

Coordination

Acceptance notes

  • skip-changeset: nothing published moves. scripts/pm/** is the fast lane — not in any
    package's files[].

Generated by Claude Code

… marker roster

`POPULATION_MARKER_LOOKALIKE` was built from `POPULATION_MARKER_KEYS` alone
while the tool reads six reason-bearing keys, so `unparsedPopulationMarkers`
was a probe over three of them and a silence over the other three: a
`no-check-families`, `inherited-population` or `self-test-reads` line that
READS as a declaration and does not PARSE produced exactly what a file that
declares nothing produces.

The lookalike is now keyed on `Object.keys(MARKER_REASON_GRAMMARS)` — the
roster both grammar builders already derive — and each key's opener is the
forms ITS language has, so on `no-check-families` a lookalike is a `#`-form
line that fails to parse and a `//` line in a workflow stays document
content. The refusal prescribes `markerFormsFor(key)` rather than the whole
roster, so the remedy is one the author of that file can take.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude
@os-justin
os-justin marked this pull request as ready for review September 18, 2026 00:58
@os-justin
os-justin added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit d099764 Sep 18, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18825-dropped-marker-probe-roster branch September 18, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants