Skip to content

check-self-test-workflow-commands walks only the root scripts/, so the one package-local gate CI self-tests is in no population and its output is never scanned #15414

Closed

Description

@baozhoutao

Found while measuring #15342, out of that card's scope (which is the two anchors in scripts/pm/dispatch-gates.mjs and scripts/check-self-test-wired.mjs). Filed rather than fixed: closing it changes which self-tests that gate SPAWNS, which is new verification surface and a different card.

The gap

scripts/check-self-test-workflow-commands.mjs imports the shared extraction from check-self-test-wired.mjs (collectInvocations, carriesSelfTest, codeOf) — "one definition, two gates", as its header puts it — but it does not import the population. It builds its own:

const files = walkScripts(scriptsDir);   // scriptsDir = join(ROOT, 'scripts')
...
const population = [...carriers].filter((s) => named.has(s)).sort();

walkScripts is anchored at the repo-root scripts/ directory, so a gate CI runs by a package-local path can never enter carriers and therefore never enters population, however CI names it.

Measured on 2c22f5716

lint.yml runs one such gate, twice:

- name: Relationship carriers are spelled as the string the spec declares
  run: |
    node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test
    node packages/lint/scripts/check-reference-carrier-shape.mjs

Read through the shared extraction:

named by CI: true | self-tested by CI: true | carries --self-test in code: true
in the workflow-commands population (root walk only): false

The two gates' own scope lines disagree by exactly one, which is this file:

check-self-test-wired:            169 of those are run by 30 workflow(s)
check-self-test-workflow-commands: 168 script(s) CI runs ship a `--self-test`

Why it matters

That gate's whole subject is what a self-test PRINTS on a runner: a line the Actions runner parses as a workflow command mints a failure-level annotation on a green required check, and its header records that the transformation happens on a runner and nowhere else. check-reference-carrier-shape.mjs --self-test is run by CI on every pull request, and its output is in no sweep. The blind spot is one script wide today and grows with the package-local gate lane.

Note the asymmetry that keeps this quiet: the gate's #4690 refusals all fire on an EMPTY population or an empty candidate set. A population that is complete-minus-one refuses nothing and prints a confident scope line.

Direction, not a prescription

check-self-test-wired now admits a second population source on the root walk's own terms — the file must exist and its code must carry the literal — and deliberately does NOT add a second walk. The obvious repair is to export that population from the one gate that owns it rather than to widen a second walk here, which is what the "one definition, two gates" header already promises for the extraction half. Whoever takes it should also decide whether isCandidate then selects the file, and price the extra subprocess.

Related: #15342 (the same root-anchoring class, in the two tools that card names).

Activity

  1. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk, dispatching to an os-dev subagent on branch claude/issue-15414-workflow-commands-population. S/M. Ruling for the dispatch: follow the card's direction — export the population from check-self-test-wired.mjs (the gate that owns it, "one definition, two gates") and consume it in check-self-test-workflow-commands.mjs; ⛔ no second walk. Decide isCandidate for the package-local file explicitly and price the extra subprocess in the PR body. Serial on dispatch-gates.mjs / check-self-test-wired.mjs: #15466 merged 17:49Z, #15342 landed; no open PR touches either gate or check-self-test-workflow-commands.mjs (scan across all open PRs at claim time). Main at 8a12067bd. Assignee field was empty at the read immediately before this write.


    Generated by Claude Code

  2. added a commit that references this issue on Sep 4, 2026
    d74490f
  3. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15414,
      "status": "done",
      "branch": "claude/issue-15414-workflow-commands-population",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15506",
      "premise_still_valid": true,
      "summary": "Premise held on current main: the two gates' scope lines read 169 (wired) vs 168 (workflow-commands) at a06faebbe, and the exact set difference was computed rather than inferred -- packages/lint/scripts/check-reference-carrier-shape.mjs, which lint.yml self-tests on every PR. check-self-test-wired.mjs now exports collectPopulation() (walk, sources, workflow corpus, alias expansion, package-local admission) plus refusalFor(), the #4690 floors as a pure function over a completed reading; check-self-test-workflow-commands.mjs consumes it and takes no walk -- walkScripts, readdirSync and statSync are gone from that file and an own-source case pins that they stay gone. Both scope lines now read 169. isCandidate DECISION: package-local members are filtered on exactly the same terms as root ones, no lane of their own, because the predicate's subject is the file's bytes; recorded in the code above isCandidate and in the PR body. SUBPROCESS PRICE: zero today -- candidates measured 17 before and 17 after, because that file's code carries neither form; deferred price if it ever gains a token is 0.68s wall (measured) against ~14s for the 17 already selected. dispatch-gates.mjs was NOT edited and no ledger row needed moving; its COMPOUND_ANCHOR_LEDGER rows key on carriesSelfTest and runSelfTest, both unchanged, and its bounded-subtree self-test case runs on a hand-written fixture string, not on this file. WORKFLOW_DIR stays declared in the workflow-commands gate although nothing there reads it any more, because dispatch-gates derives that gate's family from its source literals; it is pinned against read.workflowDir in main() so it is a live coupling, and the derived command list for a .github/workflows/lint.yml card is byte-identical before and after. One #4690 floor moved into check-self-test-wired with the population (the empty-population refusal), which cannot fire on a tree where any script CI runs ships a --self-test.",
      "tests": "All exit codes captured before any pipe (cmd > log 2>&1; EXIT=$?); every verdict quoted is the gate's own printed line. Union run at HEAD d74490f5c on a clean tree (git diff HEAD = 0 bytes). SCOPE LINES -- before: wired '169 of those are run by 30 workflow(s)' / workflow-commands '168 script(s) CI runs ship a --self-test; 17 mention a workflow-command token'; after: wired unchanged at 169 / workflow-commands '169 script(s) CI runs ship a --self-test (1 of them package-local gate(s) CI names by path...); 17 mention a workflow-command token'. Set difference computed directly: root-only 168, full 169, difference exactly ['packages/lint/scripts/check-reference-carrier-shape.mjs']; candidates 17 -> 17, candidate difference []. DERIVED FAMILY -- node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths passed; it took the changeset itself) yielded 26 commands, ALL EXIT 0, including check:pm-dispatch-gates ('dispatch-gates self-test: 1402 cases pass.'), check:watch-hint-literal ('52 declaration(s) across 4 rostered name(s) ... no unrostered spelling of the idiom in the tree.'), check:nul-bytes ('OK (scanned 7494 text file(s) ... no raw ASCII control bytes).'), check:cross-package-test-inputs, check:ratchet-remedy-authority, check:cli-command-ids, check:entry-guard, check:parse-guard, check:refd-timer-probe and the rest. Run additionally by name: pnpm check:declared-population-live -> '207 of 254 famil(ies) declare a path population, and every one of them reaches this tree's 7501 tracked file(s).' SELF-TESTS -- 'check-self-test-wired --self-test: ... 10 declared batteries, 67 cases registered, every battery at or above its pinned floor.' (was 9 / 59) and 'check-self-test-workflow-commands --self-test: ... 7 declared batteries, 31 cases registered, every battery at or above its pinned floor.' (was 6 / 23). Floors raised for cases ADDED, never lowered. LINT -- full repo-wide pnpm lint (eslint . --no-inline-config) ran to completion under the shared verify lock: VERDICT command-exit 0, held 91s. Narrowed reading agrees: eslint --format json over the two edited files reports 2 files, 0 errors, 0 warnings; this config sets no parserOptions.project and enables no typed rules, so this diff cannot move an untouched file's verdict. CONSUMERS -- this PR changes an imported module's exports, so its other importers were run too: check-comment-mask-adoption EXIT=0, check-slot-lookup-ratchet EXIT=0, measure-self-test-floor --self-test EXIT=0, check-ratchet-remedy-authority in the derived family. Neither edited script has a vitest suite; git grep over *.test.* found none, and their instrument is their own --self-test, both in the union. ABLATIONS -- two, both taken from the COMMITTED state, each with a restore trap on EXIT INT TERM on ABSOLUTE paths, each mutation confirmed on disk by anchored grep counts and a blob-hash comparison before any reading, each restore proved by git diff HEAD = 0 bytes AND restored blob == HEAD blob AND leftover marker count 0. These are plain .mjs scripts executed directly, so there is no dist/ leg to rebuild; what is proved instead is that the bytes changed and changed back. (1) Neutralise the package-local admission in collectPopulation: injected marker 1, original anchor 1 -> 0, mutated blob 93d60275 vs HEAD blob cfca4ddc. Readings: check-self-test-wired --self-test EXIT=1 naming 'the EXPORT dropped the package-local half'; check-self-test-workflow-commands --self-test EXIT=1 naming the specimen by path; and -- the direction that matters -- the PRODUCTION wired gate stayed EXIT=0 and GREEN at 168. With one definition both production gates go to 168 together and AGREE, so the 169/168 disagreement that made this card findable is no longer available as an accidental signal and these self-test pins are now the only instrument. (2) Re-add a private walk to the workflow-commands gate: injected marker 1, walk call 1, blob differs from HEAD; check-self-test-workflow-commands --self-test EXIT=1 naming 'this gate walks a directory again'. Both trees restored byte-for-byte. NO NOT-MEASURED RESULTS: no command returned exit 3, PREREQUISITE NOT MET, ERR_MODULE_NOT_FOUND, or a queue timeout in any reading above. One os-verify-lock acquisition did return exit 99 (queue-timeout, never acquired) mid-run; the slot was resumed and every command was re-run to completion under a real hold, so no reading rests on it.",
      "mcp_calls": "1 -- a single mcp__github__search_issues for dedup, which FAILED with 'API rate limit already exceeded for user ID 6194462' and returned no data. Everything else went over repo-scoped REST via GH_TOKEN. Channel probe recorded: GET /search/issues returns HTTP 403 ('sessions are bound to their configured repositories'), while the control GET /repos/objectstack-ai/objectstack/issues/15414 returns HTTP 200.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15509: check-self-test-workflow-commands declares no scripts/** population to the dispatch derivation (unlike its sibling, which declares ROOT_DIR_WATCH_HINTS), so it is derived onto no card that edits the scripts whose self-test output it scans -- measured: a scripts/check-nul-bytes.mjs card derives check-self-test-wired and not this gate. Left out of the PR because adding the hints would newly place a ~14s gate on every scripts/** card, which is a dispatch-cost policy call; the issue also records the counter-argument from check-declared-population-live's own doctrine that an inherited population is judged in the module it was declared in.",
        "filed as #15510: scripts/pm/dispatch-gates.mjs cites check-self-test-workflow-commands.mjs twice in prose as the LIVE specimen for a walk seeded at a bounded subtree; after this PR that gate holds no walk at all. The outcome both sentences report is still true (the gate is selected by none of the limbs, the census is still five) but the reason given is now false, and nothing reds because the self-test case beside it runs on a hand-written fixture string rather than on the real file. Not fixed here: it is a third file, prose only, and the PM's dispatch scoped this card to the two gates.",
        "DEDUP CAVEAT for both of the above: the usual dedup channels were unavailable -- /search/issues is HTTP 403 in this session and the one MCP search hit the account rate limit. The search was done instead over the bounded REST list channel (repos/.../issues?state=open&sort=updated, 3 pages, 272 open issues, newest-updated first) with local grep; no title hit for either finding, and #15414 itself was found as the must-hit control proving the scan was live. That is a bounded read, not the whole open backlog -- an older duplicate could exist beyond those 272."
      ]
    }

    Generated by Claude Code

  4. claude commented on Sep 4, 2026

    @claude
    Contributor

    LANDED — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

    PR #15506 merged 2026-09-04T20:48Z (merge-queue, on the one re-run after the eviction recorded on the PR — #15545 carries the flake; merged: true, main a4ef7dc1f). Probe on the re-fetched main: collectPopulation / refusalFor are exported from scripts/check-self-test-wired.mjs, and check-self-test-workflow-commands.mjs carries no walk (0 hits for walkScripts / readdirSync / statSync); on this seat's checkout (no typescript needed by these two):

      scope: 235 file(s) under scripts/, 185 carrying `--self-test` in code (comments masked, 1 of them package-local gate(s) CI names by path); 170 of those are ru
    check-self-test-workflow-commands --self-test: both measured parse rules pinned (legacy form anywhere in a line, current form only at line start), the innocent-
    

    Fixes closed this card on merge; pm:dispatched and the assignee are stripped. Follow-up on the workflow-commands gate: #15509 (declares no population), dispatchable now.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 9, 2026
    a4ef7dc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions