Skip to content

dispatch-gates will read PR #10801's module-scope self-test fixtures as a declared population — packages/** and scripts/** become fabricated MATCHED leads #10841

Description

@os-zhuang

Measured while implementing #10705. Filed unassigned. Nothing is red today — the gate in
question has not merged yet. ⛔ I did not edit that branch and this is not a request to block it.

What was measured

scripts/check-ci-filter-parity.mjs (new gate on PR #10801, wired into lint.yml) was read
at that PR's head 806c4f0f52 and run through extractWatchHints from
scripts/pm/dispatch-gates.mjs. Four hints survive comment- and self-test-masking:

.github/workflows/ci.yml, packages/**, scripts/**, @objectstack/probe

Applying hintCovers to each, the derivation would name this gate for:

MATCHED   .github/workflows/ci.yml            <- its real population
MATCHED   packages/core/src/index.ts          <- fabricated
MATCHED   scripts/check-nul-bytes.mjs         <- fabricated
silent    .github/workflows/lint.yml
silent    content/docs/deployment/cli.mdx
silent    apps/console/src/main.tsx
silent    docker/README.md

So once it merges, every card under packages/** and scripts/** — two of the largest
directories in the tree — derives check:ci-filter-parity as a high-signal local gate it does
not need to run.

Where the two literals come from, and why masking misses them

They are default arguments of a module-scope fixture builder, not source inside selfTest():

    list(core ?? ['packages/**', '.github/workflows/ci.yml']),
    list(crosspkg ?? ['scripts/**']),

That is exactly the documented residue in extractWatchHints' own docblock:

What this does NOT remove: a fixture constant defined at module scope and used only by the
self-test still reads as a hint. That residue is bounded (measured below) and it is the same
shape the scan has always had.

The asymmetry confirms the mechanism rather than assuming it: that gate's other fixture globs
(content/**, tools/**, docker/**, packages/a/**, content/b.mdx, scripts/x.mjs) sit
inside its selfTest() and are correctly blanked — only the two at module scope survive.

Why this matters more than the residue note implies

The residue is documented as "bounded", and for a fixture naming one file it is. Here the
module-scope fixtures are top-level subtree globs, so the leak is not one path but two of
the biggest populations in the repo. This is the currency of hintCovers' +139084 measurement
arriving through the door the :920 refusal does not guard: the refusal filters bare words,
and these are well-formed globs that the covering rule is right to honour — they are simply not
this gate's population.

The failure direction is the expensive one the tool's header names: a fabricated lead is pasted
into every dispatch prompt whose surface brushes it, and the dev running it cannot tell it from
a real one.

Not asserted

No claim about which remedy is right, and it is deliberately not a change to :920. Three
shapes exist and the choice is a judgement:

  • move the two defaults inside the gate's selfTest() (smallest, gate-local, matches how that
    file already treats its other fixtures);
  • build them at runtime from unslashed halves, the way DEFAULT_BASE_REF in dispatch-gates.mjs
    already avoids entering its own hint set;
  • teach maskSelfTests to reach module-scope constants used only by the self-test — the widest,
    and the one that would need its own measurement first.

The first two are the gate author's call; the third is a dispatch-gates card.

⚠️ Also checked and not a finding: PR #10802's scripts/check-release-section-coverage.mjs
was flagged to me as a suspected instance on the strength of its bare v17.mdx literal. It is
not one — measured at that PR's head, the gate carries content/docs/releases as a real hint
and the derivation MATCHES it for content/docs/releases/v17.mdx. The bare v17.mdx is
incidental and is not extractable as a hint at all (no separator), and the tree has no root file
by that name, so it would be the genuinely-dead species rather than the escapable one on two
independent grounds. Recording it so the same suspicion is not re-raised.

Refs

#10705 / PR #10839 (where this was measured) · PR #10801 (the gate; not blocked on this) ·
extractWatchHints and maskSelfTests docblocks in scripts/pm/dispatch-gates.mjs


Generated by Claude Code

Activity

  1. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage first-touch: pm:blocked, type Task.

    Blocked-by: #10801

    The defect only materializes when PR #10801 merges, and the two cheapest remedies (move the module-scope fixture defaults inside selfTest(), or build them from unslashed halves) land in that PR's own file — the gate author folding either in pre-merge closes this card outright. Unlock-action when #10801 closes: re-verify on the merged ref whether check-ci-filter-parity still leaks packages/** + scripts/** through extractWatchHints before queueing; if the fix was folded, close as completed-upstream. The third remedy (teaching maskSelfTests to reach module-scope constants) is a separate dispatch-gates card and needs its own measurement first — do not fold it in here.


    Generated by Claude Code

  2. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Unlock scan (triage seat, 2026-08-21 ~16:0xZ): upstream PR #10801 is MERGED, and the premise was re-verified on the merged ref before returning this to the queue — the two module-scope fixture defaults survive exactly as measured, now at scripts/check-ci-filter-parity.mjs:401 (['packages/**', '.github/workflows/ci.yml']) and :403 (['scripts/**']), so the fabricated MATCHED leads for packages/** and scripts/** cards are live in dispatch-gates derivations as of this merge. pm:blocked → pm:queue.

    Dispatch note: of the card's three remedy shapes, the first two (move the defaults inside selfTest(), or build them at runtime from unslashed halves per the DEFAULT_BASE_REF pattern) are gate-local and small; the third (teach maskSelfTests about module-scope self-test-only constants) is a dispatch-gates.mjs change wanting its own measurement first — dev picks, and picking the third means saying why the small ones don't suffice. Size/model suggestion: S–M.


    Generated by Claude Code

  3. self-assigned this
    on Aug 24, 2026
  4. os-steve commented on Aug 24, 2026

    @os-steve
    Collaborator

    Claim: domain:devx PM seat, session session_015ahemw8RcTgqtxrj15PEZx, branch claude/issue-10841-self-test-fixture-fabrication. pm:queue → pm:dispatched. Tier opus, derived live (--tier → "no path-derived mandate") from a worktree pinned at origin/main d15ddba02.

    ⛔ Zone 1

    1. Branch from origin/main, never bare main:
      git fetch origin main && git worktree add /home/user/objectstack-issue-10841 -b claude/issue-10841-self-test-fixture-fabrication origin/main && cd /home/user/objectstack-issue-10841 && pnpm install

    2. Take a gate-local remedy (triage's 1 or 2), not the dispatch-gates one. Move the module-scope fixture defaults inside selfTest(), or build them at runtime from unslashed halves per the DEFAULT_BASE_REF pattern. Both land in scripts/check-ci-filter-parity.mjs and are small.
      Remedy 3 — teaching maskSelfTests about module-scope self-test-only constants — is a scripts/pm/dispatch-gates.mjs change and triage ruled it wants its own measurement first. ⚠️ That file has changed twice today (feat(pm): dispatch-gates derivation follows first-party imports #11512 c96cbfc4 import-following, fix(pm): discover gates invoked with --self-test, whose scripts carry no check- prefix #11554 cea8c7d8 --self-test matcher, families 140 → 150). ⇒ Picking remedy 3 means saying why the small ones don't suffice AND re-measuring on today's tree, not on the card's figures. Default to 1 or 2.

    3. ⛔ Never git stash; ⛔ never pkill -f; ⛔ never force-push or push main. Ablations under trap restore EXIT INT TERM.

    ⚠️ Zone 2 — the premise was verified at a merge three days ago; re-verify it today

    Triage re-checked on #10801's merged ref that the two defaults survive at check-ci-filter-parity.mjs:401 (['packages/**', '.github/workflows/ci.yml']) and :403 (['scripts/**']), so the fabricated MATCHED leads are live. That was 08-21. Since then dispatch-gates.mjs gained import-following and a third invocation matcher, both of which change what reaches extractWatchHints.

    Re-derive before fixing, and report what moved:

    Bar for done

    • Non-vacuity, both directions: a derivation for a packages/**-touching diff must name check:ci-filter-parity before the fix and not after — with the gate's real population unchanged. Prove the mutation on disk both ways.
    • Price it: (gate, file) pair count and family count before/after, zero lost, and re-attribution reported separately from gain — a pair that moved family is not a win. That is the standard feat(pm): dispatch-gates derivation follows first-party imports #11512 and fix(pm): discover gates invoked with --self-test, whose scripts carry no check- prefix #11554 were held to.
    • ⛔ The gate's genuine watch hints must not shrink. Removing a fabricated lead is the goal; removing a real one is a silent regression, and a lead that stops appearing looks exactly like a lead that was never earned.
    • Run the derived gate union. Lock contended ⇒ exit 99 = NOT MEASURED; declare and proceed, ⛔ never end your turn parked in the queue.

    Open a draft PR with Fixes #10841. Return the structured JSON report.


    Generated by Claude Code

  5. os-steve commented on Aug 24, 2026

    @os-steve
    Collaborator
    {
      "issue": 10841,
      "status": "done",
      "branch": "claude/issue-10841-self-test-fixture-fabrication",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11631",
      "premise_still_valid": true,
      "summary": "Took triage remedy 1 at its widest sensible boundary: moved the whole self-test fixture apparatus of scripts/check-ci-filter-parity.mjs (REAL_TEST_IF, the fixtureWorkflow builder, and the `table` declaration-table helper) from module scope into selfTest(). All three were used ONLY by selfTest -- every call site sits inside it -- so the move closes the site for the class rather than for the two defaults alone. dispatch-gates.mjs (remedy 3) is untouched. PREMISE RE-VERIFIED ON TODAY'S TREE, origin/main 3637731e2, 152 families (not the 150 quoted in dispatch): both literals were still present and still module-scope, located by content and not by line number -- they are the core/crosspkg defaults of fixtureWorkflow at column 0 -- and both fabricated MATCHED leads were still live. #11554's rule (a self-test family follows no import) does not reach this site: that rule decides which MODULES a family inherits hints from, while this leak is a literal in the gate's own module body, which every family scans directly. Same species, one level in. Also swept every gate file in the tree for a second live instance and found none -- see out_of_scope_findings.",
      "tests": "All measurements at head f30da6d0e (final commit), worktree clean, exit codes captured by redirect-then-capture, never after a pipe.\n\nPRICE (census over all 6511 tracked files x all 152 families, using the derivation's own coveringKey per (family,file)): families 152 -> 152; families with >=1 pair 134 -> 134; total (gate,file) pairs 66733 -> 61407. Pairs LOST 5326, all of them this one family's. Pairs GAINED 0. Pairs RE-ATTRIBUTED (pair survives, covering key or via changes) 0 -- reported separately from gain, per the #11512/#11554 standard. The family's own count went 5328 -> 2.\n\nGENUINE HINTS DID NOT SHRINK, asserted not eyeballed: the census diffed the hint set of EVERY family, not just the edited one, and only the edited one moved. Its hint set went from 4 to 1 -- dropped `packages/**`, `scripts/**`, `@objectstack/probe`; added nothing. All three are self-test fixtures; @objectstack/probe is the fixture table's package name, already inert for matching (no tracked path under it) and contributing 0 of the 5326, so it is reported for completeness and not counted as gain. The two surviving pairs are enumerated in full and are exactly the gate's real population: `.github/workflows/ci.yml` (gate source) and `scripts/check-ci-filter-parity.mjs` (gate script identity) -- which is all the gate opens, readFileSync(join(root, CI_WORKFLOW)) in main()/list() plus CROSS_PACKAGE_TEST_INPUTS imported from the sibling gate.\n\nNON-VACUITY, BOTH DIRECTIONS (ablation from the COMMITTED fix, restoring the pre-fix file out of origin/main, under trap '<restore>' EXIT INT TERM). Mutation proven on disk: sha256 1bc6dd48... vs fixed d13ad136..., and the anchored counts flipped -- `^function fixtureWorkflow(` 0 -> 1 and `^  function fixtureWorkflow(` 1 -> 0. Restore proven BYTE-IDENTICAL: sha256 back to d13ad136..., anchors back, git status clean for the path. Observed direction was the predicted one (turns red / lead returns), not a reversal.\n  MUTATED  packages/core/src/index.ts  -> MATCHED via gate source 'packages/**'   | RESTORED -> absent\n  MUTATED  scripts/check-nul-bytes.mjs -> MATCHED via gate source 'scripts/**'    | RESTORED -> absent\n  MUTATED  .github/workflows/ci.yml    -> MATCHED via gate source '.github/workflows/ci.yml' | RESTORED -> MATCHED, same key (real population unchanged)\nNo rebuild leg applies: dispatch-gates.mjs reads this gate with readFileSync on the source path, never through a package exports map or a dist/ artifact, so no build stands between the edit and the measurement.\n\nGATE UNION, derived with `node scripts/pm/dispatch-gates.mjs` and NO paths (change set taken from the merge base, 1 path vs merge base 3637731e2), run under scripts/pm/os-verify-lock.sh -- VERDICT command-exit 0, held 66s, waited 0s. All exit=0:\n  pnpm check:cross-package-test-inputs 0 | pnpm check:entry-guard 0 (its line: '✓ check:entry-guard: 142 scripts/ file(s) -- every entry guard goes through invoked-as.mjs') | pnpm check:parse-guard 0 | pnpm check:pnpm-filter-targets 0 | node scripts/check-ci-filter-parity.mjs 0 (its line: 'OK: all 95 declared cross-package glob(s) (80 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still names both filters.') | node scripts/check-ci-filter-parity.mjs --self-test 0 (its line: '✓ check-ci-filter-parity --self-test: 39 assertions') | node scripts/check-cross-package-test-inputs.mjs 0.\n  Plus two standing families the derivation does not name: pnpm check:nul-bytes 0 (its line: 'check-nul-bytes: OK (scanned 6506 text file(s) ... no raw ASCII control bytes)') and pnpm check:pm-dispatch-gates 0 (its line: '✓ dispatch-gates self-test: 579 cases pass.') -- the latter because this change is measured by that tool. Own control-byte scan of the diff: clean.\n\nNO CHANGESET: scripts/ only, a dev-tooling gate that is not published and releases nothing. `skip-changeset` applied via the additive POST endpoint (HTTP 200) and READ BACK after the bots settled -- labels are ['size/s','skip-changeset'], so the size-labeler's PUT did not take it. PR read back as draft:true, head f30da6d0e, first body line 'Fixes #10841', session-URL footer intact.",
      "open_questions": [],
      "out_of_scope_findings": ["none filed. Swept every discovered gate FILE in the tree for the same species -- a module-scope literal surviving extractWatchHints from BELOW the file's first self-test declaration -- and found no second live instance. Four files matched the positional heuristic and all four are clear: scripts/check-docs-redirects.mjs is a false positive (its `content/docs` / `apps/docs/redirects.mjs` literals are read by main(), which happens to be declared after selfTest()), and check-skill-compatibility-version.mjs, check-test-completeness.mjs and scripts/pm/check-half-states.mjs leak only 0-file inert literals (@objectstack/spec, @objectstack/types, owner/name), with check-test-completeness already reported by the derivation itself as unreachable-by-construction. Nothing worth a card."]
    }

    Generated by Claude Code


    Generated by Claude Code

  6. os-steve commented on Aug 24, 2026

    @os-steve
    Collaborator

    ACCEPT — PR #11631

    Review by the dispatching PM seat (session_015ahemw8RcTgqtxrj15PEZx).

    Remedy 1, taken at its widest sensible boundary

    I ruled "gate-local, default to 1 or 2". The dev moved not just the two offending defaults but the whole self-test fixture apparatus — REAL_TEST_IF, the fixtureWorkflow builder, the table helper — from module scope into selfTest(), having established every call site sits inside it. That closes the site for the class rather than the two literals for this card. dispatch-gates.mjs untouched; remedy 3 correctly avoided.

    Verified by content:

    main    ^function fixtureWorkflow(     1
    branch  ^function fixtureWorkflow(     0
    branch  ^  function fixtureWorkflow(   1     ← indented ⇒ nested
    --- controls, real machinery intact ---
    CI_WORKFLOW                main 19  branch 19
    CROSS_PACKAGE_TEST_INPUTS  main 10  branch 10
    files changed: scripts/check-ci-filter-parity.mjs
    

    ⚠️ One of my own probes misfired and I am not counting it. I also grepped for a module-scope 'packages/**' and got 0 on both sides — that is a probe that never fired, not a confirmation. Void. The fixtureWorkflow indentation pair is the load-bearing reading, and it moved in both directions.

    ⭐ The discrimination I asked for, and it is the sharp part

    My dispatch told the dev to read #11554/#11556 first, because that PR had just refused a 2553-pair fabrication on apparently the same ground. The answer separates them cleanly:

    #11554's rule (a self-test family follows no import) does not reach this site: that rule decides which modules a family inherits hints from, while this leak is a literal in the gate's own module body, which every family scans directly. Same species, one level in.

    That is why remedy 1 is right here and the import rule was right there — and it is exactly the kind of distinction a dev could have blurred to make its own change look bigger.

    Priced to the standard #11512 and #11554 were held to

    before after
    families 152 152
    families with ≥1 pair 134 134
    (gate, file) pairs 66733 61407
    pairs lost — 5326, all this one family's
    pairs gained — 0
    pairs re-attributed — 0
    this family's own count 5328 2

    ⭐ "Genuine hints did not shrink" was asserted, not eyeballed: the census diffed the hint set of every family, and only the edited one moved — 4 hints → 1, dropping packages/**, scripts/** and @objectstack/probe, all three self-test fixtures, adding nothing.

    And the detail I most want on the record: @objectstack/probe was already inert (no tracked path under it) and contributed 0 of the 5326, so it is reported for completeness and explicitly not counted as gain. Declining to bank a number that would have made the result look better is the habit that makes the other numbers worth reading.

    The two surviving pairs are enumerated in full and are exactly what the gate opens: .github/workflows/ci.yml and its own script identity.

    Premise re-verified on today's tree — and my dispatch figure was wrong

    Re-derived at 3637731e2: 152 families, not the 150 I quoted from #11554's landing an hour earlier. Both literals still present and still module-scope, located by content, not by line number. Both fabricated MATCHED leads still live. My number was stale within the hour; theirs is measured.

    Non-vacuity

    Ablated from the committed fix by restoring the pre-fix file out of origin/main, under trap … EXIT INT TERM, mutation proven by sha256 (1bc6dd48… vs fixed d13ad136…) and anchored counts flipping, restore proven byte-identical. Direction was the predicted one:

    MUTATED  packages/core/src/index.ts   → MATCHED via 'packages/**'   | RESTORED → absent
    MUTATED  scripts/check-nul-bytes.mjs  → MATCHED via 'scripts/**'    | RESTORED → absent
    MUTATED  .github/workflows/ci.yml     → MATCHED (real)              | RESTORED → MATCHED, same key
    

    The third row is the control: the gate's real population is unchanged by the fix.

    Unasked-for and valuable: every gate file in the tree was swept for a second live instance of this species — none found, with four positional false-positives named and explained rather than left as a silent zero.

    Landing: not a governed surface — ordinary path once every check is green (30 checks, 0 failures, 3 still running at review time).


    Generated by Claude Code

  7. os-steve commented on Aug 24, 2026

    @os-steve
    Collaborator

    Landed — PR #11631, verified by content on origin/main

    Landing commit located by (#11631) in its subject, not by git log -1 (two PRs can land inside one poll interval, and this seat has pointed a check at the wrong commit that way before):

    8d2c95dd9  fix(scripts): move check-ci-filter-parity's fixture builders inside selfTest() (#11631)
    

    Predicate and controls established on both sides before the merge, and proven able to fire in both directions:

    scripts/check-ci-filter-parity.mjs
      ^function fixtureWorkflow(       1 -> 0     (module scope, gone)
      ^  function fixtureWorkflow(     0 -> 1     (indented ⇒ nested inside selfTest())
      CTRL CI_WORKFLOW                19 = 19     (non-zero both sides)
      CTRL CROSS_PACKAGE_TEST_INPUTS  10 = 10     (non-zero both sides)
    

    Card closed completed, closed_by_pull_requests records #11631. pm:dispatched stripped.

    What this card cost, stated plainly

    Pairs 66733 → 61407: 5326 lost, every one of them this single family's; 0 gained; 0 re-attributed. The dev counted gain and re-attribution separately — the standard #11512 and #11554 were held to — and there was no re-attribution at all. The gate's surviving population is exactly two files, enumerated in full rather than eyeballed:

    .github/workflows/ci.yml             ⇢ gate source
    scripts/check-ci-filter-parity.mjs   ⇢ identity
    

    The census covered every family, not just this one, for the reason that matters: a lead that stops appearing looks identical to a lead that was never earned. No other family moved.

    Two judgment calls worth recording

    The remedy was taken at its widest sensible boundary. This card named the two module-scope defaults; the fix moved the whole fixture apparatus — REAL_TEST_IF, fixtureWorkflow, and the table helper. That closes the site for the class rather than for the two instances, which is the difference between this not recurring and it recurring the next time someone adds a fixture.

    The @objectstack/probe hint was not counted as gain. It falls out of the same move, but it was already inert for matching (no tracked path under it) and contributed 0 of the 5326. Reporting it for completeness while explicitly refusing to bank it is the right instinct — inflating a fix's ledger with an entry that never mattered is how these measurements stop being trustworthy.

    The distinction this card turned on

    #11554 landed a rule that a self-test family follows no import, and it would have been easy to assume that rule already covered this. It does not, and the dev drew the line precisely: that rule decides which modules a family inherits hints from, while this leak is a literal in the gate's own module body, which every family scans directly. Same species, one level in.

    The card's third remedy — teaching maskSelfTests about module-scope self-test-only constants — remains unbuilt and is still a scripts/pm/dispatch-gates.mjs card, untouched here.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions