Skip to content

check:type-source-resolution reads each package's tsconfig.json and only that one — the sibling tsconfig.test.json this repo prescribes as its own repair is a whole tsc program outside the gate's declared population #11490

Description

@os-steve

Found while graduating @objectstack/trigger-record-change out of the TEST_DEBT ledger (#11204, PR #11489). Filed unassigned per the out-of-scope-findings rule; searched open issues for type-source-resolution and tsconfig.test.json first and found none. Nearest neighbour is #11094, which is a different axis (a paths entry pointing at a sibling's src as a latent runtime redirect); this one is about which configs the gate reads at all.

The gap

scripts/check-type-source-resolution.mjs builds each package's program from exactly one config:

const configPath = join(pkg.dir, 'tsconfig.json');

Every other tsconfig in the package is invisible to it. That matters because the repo's own supported repair for a hidden test layer — stated in AGENTS.md and in scripts/check-type-check-coverage.mjs's TESTS_COVERED docs — is precisely a sibling config:

The build config has a reason to exclude tests — ci.yml gates that no test file reaches the published artifact — so the supported repair is a SIBLING tsconfig.test.json wired into the typecheck script.

Five packages now take that route: packages/spec, packages/client, packages/metadata-core, packages/metadata-fs, and packages/triggers/trigger-record-change. Each of those siblings is a real tsc program that CI runs on every PR, and none of them is in this gate's population.

Why it is not merely theoretical

The measurement is on PR #11489. That package's 7 test files import @objectstack/driver-sql, @objectstack/formula, @objectstack/objectql and @objectstack/service-automation, none of which the build config's program ever contained. Put those tests back through the build config and the gate sees them immediately:

✗ @objectstack/trigger-record-change: NEW dist-resolved type import(s) since this
  entry was measured: @objectstack/driver-sql, @objectstack/formula,
  @objectstack/objectql, @objectstack/service-automation.

Put the identical files through a sibling config instead and the gate stays at exit 0 with its count unchanged (77 packages with a tsconfig.json scanned; 51 registered). Same files, same four dist-resolved type imports, same exposure the gate exists to ratchet — reported in one spelling and silent in the other.

The asymmetry has a perverse edge worth naming: the route the repo prescribes is the route this gate cannot see, so following the house pattern is what makes the exposure invisible. A future author with a genuine choice between the two spellings gets a green gate for picking the one that hides more.

Why this is the gate's own argument

Its header already rules that a per-surface sweep is the wrong terminal state, quoting check:test-source-alias:

it leaves the NEXT package unguarded, and the symptom of the omission is a green test, so nothing would report the gap

and then generalises it: "That reasoning is axis-independent." The same sentence applies one level down — the gate is per-package where it needs to be per-program.

Shape of a fix (not prescribing)

The natural widening is to read every tsconfig*.json a package's typecheck script NAMES, which is a predicate check-type-check-coverage.mjs already computes (configsNamedByTypecheck) and which keeps a decorative config from counting. That would change the registry's measured population, so it is a ratchet re-baselining rather than a one-liner, and it needs its own card — which is why this is filed rather than ridden along.

Not addressed in PR #11489, deliberately: that PR's whole finding is that widening a shrink-only registry entry is not a remedy, and this is the other half of the same observation.

Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    Contributor

    Triage (first-touch grading): graduated finding → pm:queue + domain:devx, type Task. Lands in scripts/check-type-source-resolution.mjs: widen the per-package program set to every tsconfig*.json the package's typecheck script names (the configsNamedByTypecheck predicate already computed by check-type-check-coverage.mjs), with the resulting registry re-baseline stated visibly in the PR (population widening = gate strengthening, not weakening — but the measured before/after counts belong in the PR body). The card's own measurement (same files, same four dist-resolved imports, reported in one spelling and silent in the prescribed one) is the acceptance test: after the fix, the sibling-config spelling must report identically to the build-config spelling.


    Generated by Claude Code

  2. added theissue type on Aug 25, 2026
  3. self-assigned this
    on Aug 25, 2026
  4. os-steve commented on Aug 25, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM dispatch (devx seat)

    • Session: session_015ahemw8RcTgqtxrj15PEZx
    • Branch: claude/issue-11490-type-source-resolution-sibling-configs

    Dispatching on triage's grading from ~30 minutes ago.

    Two things in that grading I am carrying into the dispatch as binding rather than advisory:

    1. The population widening is gate STRENGTHENING, not weakening — but the measured before/after registry counts belong in the PR body, visibly. A re-baseline that appears without its numbers is indistinguishable from a ratchet quietly reset.
    2. The card supplies its own acceptance test, which is unusually clean: the same files with the same four dist-resolved imports are reported in one spelling and silent in the prescribed one. After the fix, the sibling-config spelling must report identically to the build-config spelling. That is a two-sided check, not a "does it still pass".

    ⚠️ And the standing rule applies with force here: ⛔ no gate weakening. If widening the population turns something red that was previously invisible, that red is the point of the card — it is a finding to report, never something to baseline away without saying so.

    Tier: opus. Derived live — node scripts/pm/dispatch-gates.mjs --tier scripts/check-type-source-resolution.mjs reports no path-derived mandate, so sonnet is the floor and this is a judgment call: deciding which tsconfig*.json files genuinely belong in a package's program set is exactly the kind of population question that, got wrong, produces a gate that looks stronger and measures less.

    No same-file holder: checked against the in-flight branches, 0 hits.


    Generated by Claude Code

  5. os-steve commented on Aug 25, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 11490,
      "status": "done",
      "branch": "claude/issue-11490-type-source-resolution-sibling-configs",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11922",
      "premise_still_valid": true,
      "summary": "Re-derived the card's measurement on origin/main 59972075e first: still open. Same 7 test files of packages/triggers/trigger-record-change with the same four dist-resolved imports were REPORTED through the build config (exit 1) and SILENT through the prescribed sibling (exit 0, '77 packages ... 51 registered'). Widened check-type-source-resolution.mjs from one config per package to every tsconfig*.json the package's typecheck script NAMES, reusing configsNamedByTypecheck — extracted from check-type-check-coverage.mjs into a new shared scripts/typecheck-configs.mjs (bodies byte-identical to the originals; the coverage gate's structural output is byte-identical at 59972075e and at HEAD). Named, not globbed: packages/cli/tsconfig.build.json is on disk, named by no script, and correctly stays out. Registry re-baselined with its numbers stated in the PR body and in the registry docblock: 77->93 programs, 51->54 entries, 221->233 pairs. Nothing weakened, nothing baselined away silently; the 12 newly visible pairs are itemised. Diagnostics now name the program ('@objectstack/spec (via tsconfig.scripts.json)'), and a new census leg fails when NO package runs more than one program, with its own positive control.",
      "tests": "All at HEAD e3abf9a1d; gate families derived live via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, verdicts quoted from each gate's own line, exit codes captured before any pipe. GREEN: check:type-source-resolution (0) 'check-type-source-resolution --self-test OK' + 'OK — 93 tsc program(s) across 77 packages scanned ...; 54 registered'; check:type-check-coverage (0) 'OK — 65/78 workspace packages type-checked ..., 1 exempt'; check:entry-guard (0); check:parse-guard (0); check:agent-test-spelling (0) '0 violations — 365 file(s)'; check:pnpm-filter-targets (0); check:cross-package-test-inputs (0); check-ci-filter-parity.mjs (0); check-cross-package-test-inputs.mjs (0); check:nul-bytes (0) 'scanned 6639 text file(s) ... no raw ASCII control bytes'; typecheck-configs.mjs --self-test (0) '11 cases hold'. ABLATION (two-sided, real tree): planted '@objectstack/metadata-core' in packages/triggers/trigger-record-change/src/record-change-trigger.test.ts — a file the build config excludes and the sibling includes. Same mutated tree: PRE-CHANGE gate (git show 59972075e:...) exit 0 'OK — 77 packages ... 51 registered'; WIDENED gate exit 1 '✗ @objectstack/trigger-record-change: NEW dist-resolved type import(s) ...: @objectstack/metadata-core (via tsconfig.test.json).' No rebuild leg applies — this gate reads sources and configs from the checkout, it does not resolve through dist. Mutation confirmed ON DISK in both directions by marker occurrence count (0 -> 1 -> 0) plus `git diff --stat` non-empty and `git status --porcelain` empty after restore; restored under `trap restore EXIT INT TERM`; both gates re-run green on the restored tree. The premise re-derivation used the same discipline (exclusion-anchor count 1 -> 0 -> restored). DECLARED NARROWING, one item: check:type-check-debt is `--self-test && --re-measure`; the self-test leg is the same one run green under check:type-check-coverage, and the --re-measure leg REFUSED to run and measured nothing ('Error: --re-measure cannot run: 56 workspace dependenc(ies) ... have no built type entry point on disk') — it needs a full turbo build of the workspace, which is CI's run per lint.yml. LINT, narrowed and measured: eslint --no-inline-config over the 3 changed files = 3 files / 0 errors / 0 warnings, counts read from --format json; population read from eslint's own config (isPathIgnored=false for all three, calculateConfigForFile reports the 2 rules governing scripts/*.mjs); eslint.config.mjs enables no type-aware linting for any file (its own header states and measures this), so this diff cannot move any untouched file's verdict. Positive control: a planted comment-swallow/no-code-inside-block-comment violation drove the same command to exit 1 naming that rule, `node --check` confirmed the mutant parsed (assertion-red, not crash-red), restored file back to exit 0.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Two notes the JSON does not carry.

    On the assumptions Zone 2 asked me to falsify rather than inherit. configsNamedByTypecheck does do what its name suggests, with one property worth stating: it answers in basenames, because its original consumer compares against a readdir of the package. A reference written with a directory (-p ../shared/tsconfig.test.json) is therefore credited under its basename. Measured: 0 of this workspace's package manifests reference any tsconfig with a directory prefix, so the residual case has no instance today. Handled in-scope rather than filed: both consumers resolve every name against the package's own directory, the property is documented in the new module's header, and a case pins it so a consumer that stopped resolving names would go red rather than quietly credit another package's file.

    On the third assumption — that not every tsconfig*.json belongs — it has a live instance and it is now a self-test fixture: packages/cli/tsconfig.build.json sits on disk and no script names it. A glob-shaped widening would have put it in the population, producing exactly the failure the card warned about: a gate that looks stronger and measures less, which is worse than the current state because it reads as covered.


    Generated by Claude Code


    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