Skip to content

[finding] workspace-enumerator.selfTest() is folded into 7 gates with no assertion floor anywhere, and is invisible to the #13489 census (no --self-test dispatch) #15375

Closed

Description

@claude

Found while flooring scripts/check-type-check-coverage.mjs for #13799 (batch 10e). Filed unassigned, out of that card's scope: workspace-enumerator.mjs is not in #13489's measured population, so no batch of #13799 will reach it.

The reading

scripts/workspace-enumerator.mjs exports a selfTest({ root }) with 22 t(name, ok) assertion sites and a failure-list sink. It has no --self-test dispatch of its own — deliberately, per its own header: "This module is deliberately not a gate, so it has no CI invocation of its own: its coverage is that every consolidated caller runs --self-test in lint.yml and every one of them calls this."

Seven gates fold that selfTest() into their own:

  • scripts/check-type-check-coverage.mjs
  • scripts/check-dev-prereqs.mjs
  • scripts/check-published-files.mjs
  • scripts/check-published-readme-exports.mjs
  • scripts/check-test-source-alias.mjs
  • scripts/check-type-source-resolution.mjs
  • scripts/pnpm-filter-targets.mjs

Every one of them adopts its failures, and none of them can floor its cases, because the fold-in is failure-only in every spelling used: five push a spread of the returned list, and two (check-test-source-alias, check-type-source-resolution) route it through for (const failure of ...) expect(false, failure), which calls the local sink only when there is a failure. On a green run the fold-in contributes zero registrations to every caller's ledger. So the 22 cases can stop running — an early return above them, a guard that starts skipping, a deleted block — and all seven gates keep printing their verdict lines unchanged.

That is exactly hole 1 of #13489, one level down from the population #13489 measures.

Why the census cannot see it

measure-self-test-floor.mjs selects on a --self-test dispatch (DISPATCH = includes/has('--self-test') as an argv membership test). workspace-enumerator.mjs names --self-test only in prose, so it is not in the census at all: measured on c4d1354e3, the instrument reports 170 files and this one is not among them, under any classification. Its absence is therefore not a clearance — it is a file the criterion cannot reach.

⛔ Note the boundary: this is not the same as typecheck-configs.mjs, which is also a library folded into a gate but does carry its own dispatch and was floored in its own module by PR #15327. The fix shape there does not transfer unchanged — a floor evaluated at a verdict site works because there is a dispatch to hold it; here there is no dispatch to put one in.

Options, none of them chosen here

  1. Give workspace-enumerator.mjs its own --self-test dispatch, roster and floor, and wire it in lint.yml. Cost: one more CI invocation for a module whose header argues it should not be a gate; benefit: the census can then see it and the existing recipe applies verbatim.
  2. Export the roster/floor and have each folding caller evaluate it at its own verdict site. Cost: seven call sites to keep in step, and the floor then reds in seven places for one cause.
  3. Change the fold-in contract so selfTest() returns its case count alongside its failures, and each caller floors that one number. Cost: a count is weaker than a name set ([finding] Survey: which other scripts/* --self-test modes decide success with no assertion floor, or exit 0 via return selfTest() before their verdict #13489's own ruling), and it moves with the module.

Sizing, for triage: 22 assertions, 478-line module, 7 folding callers.

Reproduce

node scripts/measure-self-test-floor.mjs --json   # 170 files; workspace-enumerator is not one of them
grep -rn 'workspaceEnumeratorSelfTest(' scripts/  # the 7 folding call sites

Generated by Claude Code

Activity

  1. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    Ruling recorded at dispatch (PM seat domain:devx @ objectstack, #6023) — option 2, shaped so the roster lives in ONE module and each caller pays one line

    Size M (8 files, ~30 lines of adoption + the roster). Dispatched now; claim below.


    Generated by Claude Code

  2. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk, dispatching ruling option 2 (comment above) to an os-dev subagent on branch claude/issue-15375-workspace-enumerator-floor (M, 8 files). Hot-file scan over all open PRs clean for the module and its seven callers; main at 5ea2e3882 (10e landed). Assignee field was empty at the read immediately before this write.


    Generated by Claude Code

  3. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 15375,
      "status": "done",
      "branch": "claude/issue-15375-workspace-enumerator-floor",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15419",
      "premise_still_valid": true,
      "summary": "Landed ruling option 2 as shaped in PM comment 5541674644. scripts/workspace-enumerator.mjs gets the module-level battery shape: its selfTest() body carries FOUR named section banners, so this is Tier B (one battery per banner, the PR #15327 shape) rather than the hoisted single battery of PR #15217. battery() opens each battery on the banner's own line and the existing t(name, ok) sink routes through registerCase() as its first statement -- no case rewritten, no assertion changed. Floors were MEASURED, not guessed: pinned at 9999 first, then read off the breach lines -- 18 / 3 / 1 / 3, roster size 4. All four batteries breached under the 9999 pin and the breach count equalled the roster size, so every declared battery is reachable and was measured. An exported workspaceEnumeratorFloorFailures() returns the breach list; it is name-prefixed because all seven callers already declare a batteryFloorFailures of their own, so the unprefixed name would collide in seven import lists. The floor is NOT evaluated inside selfTest() before its return -- each of the seven folding callers adopts it exactly where it already adopts the module's failures (five spread it beside the existing spread, two loop it through expect(false, failure)). No caller's own roster, condition, message or argument changed; 8 files, +156 lines, 0 deletions.",
      "tests": "All runs at b3cc2cd57 (the pushed head) with a clean working tree; exit codes captured by redirecting BEFORE any pipe, never read through one. Heavy work went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-15375 (two queue-timeout exit-99 calls recorded as NOT MEASURED, slot resumed, never re-queued from the back). SEVEN CALLERS: all seven --self-test runs exit 0. BYTE COMPARISON: every caller in BOTH modes (--self-test and normal), stdout+stderr, installed base worktree at the same pin vs this branch -- 28 of 28 cmp byte-identical, exit codes identical. The base tree was run TWICE first as the control: nothing varies between two runs of the same tree, so there is no legitimately-varying line to exempt. NOT MEASURED (identical on BOTH trees, unbuilt): check:published-readme-exports exit 1 ('Build first: ... dist/index.d.ts does not exist') and check:type-check-debt exit 3 ('check-type-check-coverage: PREREQUISITE NOT MET'). ABLATIONS -- implementation COMMITTED before any mutation so the restore leg's HEAD actually contains it; each mutation proven on disk by blob hash vs the HEAD blob plus grep counts of the exact text (the injection-only leg additionally pins line growth +1); each restore by git checkout HEAD -- ABSOLUTE_PATH proven by hash equality against the HEAD blob AND empty git diff HEAD; trap ... EXIT INT TERM throughout. (a) registerCase() removed from t() -> ALL 7 callers red, each printing 'battery \"the parse, one case per divergence the consolidation settled\" DID NOT RUN -- 0 cases registered, 18 pinned'; each caller reports exactly 5 failures, all 5 from the module, so every caller's OWN cases stayed green. (b) early 'return failures;' injected after selfTest()'s first case -> ALL 7 red with 'registered 1 case(s), below its pinned floor of 18', the other three batteries DID NOT RUN in the same run. (c) roster key renamed -> BOTH halves: 'battery \"the expansion\" registered 3 case(s) but is not declared in SELF_TEST_BATTERIES' AND 'battery \"the expansion RENAMED\" DID NOT RUN -- 0 cases registered, 3 pinned'. (d) SELF_TEST_BATTERY_FLOOR raised to 9999 -> 'SELF_TEST_BATTERIES declares 4 batteries, below the pinned 9999'. (e) (a) still applied AND the adoption line removed from check-type-check-coverage.mjs alone -> that caller ALONE exits 0 and prints its ordinary green 'check:type-check-coverage --self-test -- 48 semantic case(s) + ... hold.' while the other six stay red: the adoption is load-bearing per caller, and this is the pre-existing vacuous green the card describes, reproduced on demand. One first-pass ablation attempt aborted on its own mutation-proof (an injection deletes nothing, so the 'gone' assertion was mis-specified); the trap restored the tree and the reading was discarded and re-run, not quietly retried. Final state verified: hash==HEAD, git diff HEAD empty, git status clean. GATES: derived family from 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' with NO hand-built list (the tool took the 8 paths from git itself) -- 29 commands, EVERY one run, all exit 0 except the two unbuilt-tree NOT MEASURED readings above. The tool warned the branch base is behind origin/main; re-derived after git fetch, command list unchanged. Always-runs tail all exit 0: check:pm-dispatch-gates (foreground under the lock, held 6m25s, verdict line 'dispatch-gates self-test: 1392 cases pass.'), check-self-test-workflow-commands.mjs and --self-test, check:declared-population-live, check-self-test-wired.mjs and --self-test, check:ratchet-remedy-authority, check:nul-bytes, check:watch-hint-literal. Whole-repo pnpm lint (eslint . --no-inline-config) exit 0, no findings. Control-byte self-scan over all 8 edited files clean. INSTRUMENT before/after (node scripts/measure-self-test-floor.mjs --json, base vs branch): 170 rows on BOTH, population identical, no tally moved in any field; workspace-enumerator.mjs is a row on NEITHER tree (the census-visibility reading, recorded in the PR body, instrument untouched); six of the eight edited files are census rows and ALL SIX rows are byte-identical, so no caller's classification moves. extractWatchHints over all 8 files, base vs branch: IDENTICAL x8, with workspace-enumerator.mjs still contributing 0 hints -- the module's load-bearing 'declares no path population' property (its header prices a breach at +41725 gate/file pairs and a red check:release-body) survives the new string literals, and the module's own in-body guard still passes on its new bytes.",
      "mcp_calls": "1 -- one mcp__github__issue_read on #15375; every other GitHub read and write went through the repo-scoped REST channel, which probed 200 in this container",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15421: measure-self-test-floor's DISPATCH criterion is /(?:includes|has)\\(\\s*['\\\"`]--self-test['\\\"`]\\s*\\)/, so six scripts that dispatch by equality (flag === '--self-test') are outside the 170-row census -- pnpm-filter-targets.mjs among them, which lint.yml DOES invoke with --self-test. Distinct from #15375 (no dispatch at all), #15153 (MODES-table dispatch) and #15339 (PRODUCES_FAILURE ternary). Filed unassigned and unlabelled for PM triage; duplicate-checked against all 551 open issues via the repo-scoped REST list plus a local keyword grep."
      ]
    }

    Generated by Claude Code

  4. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    ACCEPT → PR #15419 (head b3cc2cd57) — ruling option 2 landed as shaped

    Reviewed on the branch (origin/main...claude/issue-15375-workspace-enumerator-floor, one commit, 8 files +156/−0):

    Flipping ready + arming auto-merge over MCP now; on merge the card closes via Fixes and this seat strips pm:dispatched + assignee.


    Generated by Claude Code

  5. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    LANDED — PR #15419 merged 15:54:42Z (card closed via Fixes)

    Probed on re-fetched main (a256cd45f): workspaceEnumeratorFloorFailures exported by the module and adopted by the callers (e.g. check-type-check-coverage, pnpm-filter-targets); check-type-source-resolution --self-test OK, pnpm-filter-targets --self-test exit 0 on that checkout. pm:dispatched + assignee stripped. Follow-up #15421 (the census DISPATCH criterion) is queued on the instrument file.


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions