Skip to content

[finding] A package can pass check:type-check-coverage with a whole source directory outside tsc — packages/objectql/scripts/** is invisible to its own typecheck #10756

Description

@claude

Measured while implementing #10269 (PR #10755). Not repaired there: that card's surface
is four entry guards, and this is a property of the tsconfig include globs.

The measurement

@objectstack/objectql declares a typecheck script, passes it, and is counted as
covered by check:type-check-coverage (64/77 workspace packages type-checked ... 13 in the DEBT ledger, 1 exempt — objectql is in neither ledger). But its tsconfig is

{ "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] }

so packages/objectql/scripts/** is outside the program the package's own
typecheck compiles (see the CORRECTION below: that is narrower than "no tsc program
at all", which is what this body first claimed). Ablation on
origin/main + the #10269 branch, at ca022387f2:

inject  const OS_ABLATION_TYPE_ERROR: number = "definitely not a number";
        into packages/objectql/scripts/dry-run-hash-compat.ts
confirm on disk        grep -c OS_ABLATION_TYPE_ERROR  ->  1
pnpm --filter @objectstack/objectql typecheck  ->  EXIT 0, zero `error TS` lines
pnpm exec tsc --noEmit --listFiles | grep -c scripts/dry-run-hash-compat.ts  ->  0
restore, confirm on disk  grep -c OS_ABLATION_TYPE_ERROR  ->  0

The file appears zero times in tsc's own file list, and a number initialised with a
string sits in it without moving the gate.

CORRECTION (2026-08-21) — "in no tsc program at all" was too broad

This body originally read "so packages/objectql/scripts/** is in no tsc program at
all". That is wrong as written, and PR #10755 going red is what measured it. The
accurate statement is the one already in the title: that directory is invisible to
the package's own tsc -p, and it is not invisible to every tsc program.

The TEST_DEBT re-measure builds a different project. measureTestDebt calls
remeasureProject (scripts/check-type-check-coverage.mjs), which copies the package
tsconfig and drops every test glob from exclude — so
packages/objectql/src/dry-run-hash-compat.test.ts becomes a root file, and its
line 5 is:

import { runDryRun, type LegacyMetadataRow } from '../scripts/dry-run-hash-compat';

tsc pulls every transitively imported file into the program regardless of include,
so the script is compiled there. It is reached through the test, not through
include.

Measured: the import.meta.url that PR #10755 added to that script (the package
declares no "type", so under the repo-wide module: NodeNext it compiles as
CommonJS) produced exactly one TS1470 in the re-measure and drove
@objectstack/objectql from its frozen TEST_DEBT of 355 to 356 — a red
Type Check · debt ledger — while pnpm --filter @objectstack/objectql typecheck
stayed at exit 0 the whole time.

Why the ablation above could not see it: it ran the package's typecheck and a
--listFiles over the package's own config, and that is precisely the program the
file is missing from. It never built the test-inclusive project.

The finding stands and this card stays open. The blind spot is real, and the
accidental coverage described here argues for the gate rather than against it: it is
not a property anyone declared. It holds only for the files some test happens to
import, only while that import exists, and it reports through a shrink-only debt
ledger — as a drifted count on a package the ratchet calls covered — rather than
through the package's own typecheck.

Why this is worth a card rather than a shrug

packages/objectql/scripts/dry-run-hash-compat.ts is not scratch: it is a
compatibility checker with a documented CLI, and packages/objectql/src/dry-run-hash-compat.test.ts
imports runDryRun from it. So a package test depends on a module whose types nothing
verifies.

The same shape holds for packages/core/examples/** (include: ["src/**/*"],
rootDir: "./src"), which docs/qa/platform-checklist/FOLLOW-UPS.md already notes in
passing for phase2-integration.ts — but @objectstack/core has no typecheck script at
all and is a ledgered DEBT entry (#4311), so that half is already tracked. The objectql
half is not: it is a package the ratchet reports as covered.

The gap in the ratchet, stated precisely

check:type-check-coverage is package-granular: it asks whether a package declares a
typecheck script or carries a ledger entry. It cannot see a package that declares one
and whose tsconfig include omits a real source directory. That is the same class AGENTS.md
already warns about for test files —

Never exclude *.test.ts / *.spec.ts from a package's tsconfig.json — tsc --noEmit reads that config, so the exclusion hides the tests from the very check the
typecheck script advertises (a green gate over source nothing read).

— one level up: a whole directory rather than a file glob, and reached through include
rather than exclude, so the existing prose does not cover it.

Not proposing the remedy here

Several shapes are possible (a sibling tsconfig.scripts.json named in the typecheck
script, the packages/spec pattern; widening include; or a ratchet limb that compares a
package's tracked .ts files against tsc --listFiles), and which one is right depends on
how many packages are in this state — which nobody has counted. Filed so the count gets
made rather than guessed.

Dedupe

Grepped the titles and bodies of all 299 open issues for packages/objectql/scripts,
packages/core/examples, include.{0,20}src/\*\*, outside (every|any) tsconfig and
--listFiles. Only #10269 matches, and only because it names the two paths.
(search_issues was unavailable to this seat.)


Generated by Claude Code

Activity

  1. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor

    Correction: the extent was overstated — the blind spot itself is real, and this stays open

    Filed from PR #10755, which went red on Type Check · debt ledger at ca022387f2 and
    is what measured this.

    What this card claimed: packages/objectql/scripts/** "is in no tsc program at
    all".

    What is true: packages/objectql/scripts/** is invisible to the package's own
    tsc -p — that half is exactly right, include is ["src/**/*"] and the directory is
    not under it — but it is not invisible to every tsc program.

    The TEST_DEBT re-measure builds a different project. measureTestDebt calls
    remeasureProject (scripts/check-type-check-coverage.mjs), which copies the package
    tsconfig and drops every test glob from exclude. That makes
    packages/objectql/src/dry-run-hash-compat.test.ts a root file, and its line 5 is:

    import { runDryRun, type LegacyMetadataRow } from '../scripts/dry-run-hash-compat';

    tsc pulls every transitively imported file into the program regardless of include, so
    the script is compiled there — reached through the test, not through include.

    Measured, both directions, with objectql's dependency closure built first:

    re-measure project (include src/**/*, test globs dropped from exclude):
      with    import.meta.url in scripts/dry-run-hash-compat.ts   356 raw tsc errors
      without it (seeded from __filename instead)                 355 raw tsc errors
      the one line that leaves:
        packages/objectql/scripts/dry-run-hash-compat.ts(279,40): error TS1470: The
        'import.meta' meta-property is not allowed in files which will build into
        CommonJS output.
    
    pnpm --filter @objectstack/objectql typecheck   exit 0 in BOTH states
    

    TEST_DEBT['@objectstack/objectql'] is frozen at 355, so the extra error read as an
    upward ledger drift on a package the coverage ratchet reports as covered.

    Why the ablation in the body could not see this: it ran the package's own typecheck
    and a --listFiles over the package's own config, and that is precisely the program
    the file is missing from. It never built the test-inclusive project. The ablation was
    sound; only the conclusion drawn from it reached past what it measured.

    Not closing this, and the correction argues for the gate rather than against it.
    The accidental coverage is not a property anyone declared: it holds only for the files
    some test happens to import, only while that import exists, and it reports as a drifted
    count on a shrink-only debt ledger rather than through the package's own typecheck.
    A source directory unread by the check its package advertises is still the finding.
    The body has been corrected in place; the title was already accurate.

    Generated by Claude Code


    Generated by Claude Code

  2. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage (finding round): promoted → pm:queue, type Task. Scope as the card frames it: first the census (how many "covered" packages have a real source directory outside their own tsc program), then the remedy sized to the count — the tsconfig.scripts.json sibling pattern if few, a ratchet limb comparing tracked .ts files against tsc --listFiles if many. Strengthening only; no gate weakening in scope. Fold-or-serial note for the devx seat: pairs with #10779 (same instrument, scripts/check-type-check-coverage.mjs / remeasureProject) — answer the five-gate test for the pair before dispatch; different defect shapes (include-glob blind spot vs TS6059 billing), so serial with shared context is the likely answer, but one dev taking both with per-card commits is defensible.


    Generated by Claude Code

  3. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor

    Claim: /pm-dispatch domain:devx execution seat — folded dispatch: #10756 + #10779.
    Session: session_01DdCnBGcHeufjrq7drTD3wt · Branch: claude/issue-10756-type-check-coverage-blind-spot · Worktree: objectstack-issue-10756
    Container & model: M-L, mode:subagent, model: opus
    File surface: scripts/check-type-check-coverage.mjs (+ possibly packages/objectql/tsconfig*.json).

    fold-or-serial — answered, as triage required

    FOLD. Both cards are the same instrument (scripts/check-type-check-coverage.mjs) and #10779 is specifically remeasureProject, which #10756's own census has to reason about. Serialising them would make the second agent re-derive the first's measurements on a file the first just changed. ⛔ Neither is dispatched separately; this claim covers both.

    ⭐ Serial cleared: PR #10820 rewrote this file (+469/−19, adding compositionAt and a composition check over TEST_DEBT) and merged at ~13:45Z. Branch from current origin/main; every line number and count on both cards predates it, so re-derive everything.

    ⚠️ #10756's body was corrected by its own author (comment 1): packages/objectql/scripts/** is invisible to the package's own tsc -p, which is true — but it is not invisible to every tsc program, because the TEST_DEBT re-measure builds a different project. Carry the corrected extent, not the body's first claim.

    Scope from triage: census first (how many "covered" packages have a real source directory outside their own tsc program), then a remedy sized to the count — the tsconfig.scripts.json sibling pattern if few, a ratchet limb comparing tracked .ts files against tsc --listFiles if many. ⛔ Strengthening only; no gate weakening in scope, and ⛔ never raise the shrink-only TEST_DEBT baseline.

    Standing constraints

    • ⛔ Governed, human-merge-only: docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md — propose wording in the report.
    • ⛔ Never edit content/docs/releases/**. ⛔ Never git stash — refs/stash is shared by every worktree of the repo.
    • ⚠️ A pipe swallows the exit code. Capture as cmd > log 2>&1; ec=$?.
    • ⚠️ ⭐ NEW, measured today ([finding] pnpm --filter <typo> exits 0 with "No projects matched the filters" — a whole verification round can be fictitious and read as green #10853): pnpm --filter <name> with a name that does not exist prints No projects matched the filters and EXITS 0. A whole verification round can be fictitious and read as green. Verify your filter actually matched before trusting any run. (My own briefing carried a wrong package name this morning and a dev caught it.)
    • ⚠️ Apply labels additively (POST /issues/N/labels), never a whole-set PUT; read them back after the size labeler runs.

    Report back to me; I review against GitHub and land it.


    Generated by Claude Code

  4. claude commented on Aug 21, 2026

    @claude
    ContributorAuthor

    Folded dispatch #10756 + #10779 — draft PR #10876

    {
      "issue": 10756,
      "status": "done",
      "branch": "claude/issue-10756-type-check-coverage-blind-spot",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10876",
      "premise_still_valid": true,
      "summary": "Folded #10756 + #10779, one draft PR, per-card commits' worth of reasoning in one commit. #10756: censused all 64 covered packages and found 65 non-test .ts files outside every accounting program — 54 package-root tool configs vs 11 in a real source directory. The count chose the RATCHET LIMB (new SOURCES_COVERED invariant + shrink-only UNCHECKED_SOURCE_DEBT seeded at 10), because 11 directories across 11 packages is too many for the sibling-config route to close and repeating it would still leave the gate blind to the 12th; the limb needs no `tsc --listFiles` since the gate already walks every package tree. The 11th — packages/objectql/scripts, the card's own instance — is REPAIRED with a sibling tsconfig.scripts.json named in the typecheck script (0 errors). #10779: the remedy triage favoured is REFUTED by measurement — setting compilerOptions.rootDir = pkgAbs unconditionally RAISES @objectstack/rest 155 -> 175, because rest deliberately sets rootDir '..' for a paths rule pulling a sibling package's source in (#9960), and pkgAbs is NARROWER than that. Implemented option 2 instead: TS6059 is dropped where it is counted, for the generated re-measure project only. Two ledger entries moved, both DOWN and both exactly attributable, both notes re-tallied so neither carries a compositionAt: objectql TEST_DEBT 355 -> 354, lint 19 -> 16 (lint's 3 were a discovery of this change; its note already named them). No entry rose, and the surplus is unchanged at 12 on plugin-auth — the same pre-existing figure as the base tree — so this opens no new permission allowance.",
      "tests": "All final numbers at sha 52d0c746d2. (1) Gate union derived with `node scripts/pm/dispatch-gates.mjs` with NO hand-written path list (14 matched families), plus check:entry-guard and check:parse-guard by hand (both invisible to the derivation) and check:nul-bytes: 18/18 EXIT=0, each captured as `cmd > log 2>&1; ec=$?` with no pipe. (2) check:type-check-coverage verdict line: 'check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root), 13 in the DEBT ledger (436 frozen raw errors...), 1 exempt.' + 'test layer: 20 package(s) ... 1484 frozen raw errors in TEST_DEBT' + NEW 'source layer: 10 directory(ies) of non-test source in 10 ledgered entr(y/ies) sit outside every tsc program their package's own `typecheck` runs (10 files as counted by this run).' (3) check:type-check-debt verdict line at 52d0c746d2: 'check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 274.4s, 1908 raw tsc error(s) total, none above its recorded number.' + 'surplus: 12 raw error(s) across 1 entr(ies)'. Base tree measured 1912 with surplus 12 on plugin-auth; delta -4 = objectql -1 + lint -3 exactly. (4) Self-test: '✓ check:type-check-coverage --self-test — 38 semantic case(s) + 38 observation case(s) + 29 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) hold.' (grew 34->38, 24->38, 25->29). (5) `pnpm --filter @objectstack/objectql typecheck` EXIT=0 and the log echoes 'tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json', proving the filter matched (#10853 trap). (6) THE REFUTATION was a real measured run, not an argument: with rootDir=pkgAbs the re-measure exited 1 with '• @objectstack/rest: TEST_DEBT records 155 raw tsc error(s), `tsc --noEmit` now reports 175 (+20)'. ABLATIONS — three, each mutated and RESTORED, every leg's mutation confirmed on disk by marker count before any result was read, never by an editor's exit code (one python anchor assertion did fail and correctly refused to write, so that ablation was re-anchored rather than silently skipped): (a) drop the plugin-auth/examples ledger key, count 1->0, gate goes red naming that directory; restored 0->1, green. (b) revert objectql's typecheck to `tsc --noEmit`, marker 1->0, gate goes red on packages/objectql/scripts — proving the sibling config is coverage only because the script NAMES it; restored 0->1, green. (c) remove `dropRootDirDiagnostics: true`, call-site count 1->0, self-test goes red with both #10779 wiring assertions firing; restored 0->1, green. No dist/ or build leg is involved — the subject is a CI gate script, so ablation-dist-preflight does not apply and no rebuild was needed or claimed.",
      "open_questions": [
        {
          "question": "SOURCES_COVERED governs source files in SUBDIRECTORIES (depth > 0), so the 54 package-root tool configs (vitest.config.ts x32, tsup.config.ts x16, objectstack.config.ts x5, vitest.integration.config.ts x1) across 42 packages are unchecked TypeScript that no invariant names. Should they be governed too? The depth line can also be walked around by moving a file up into the package root. Filed as #10870 rather than decided in this PR; the gate's header argues it both ways.",
          "options": [
            "A — leave as-is: they are one repo-wide convention, not 42 decisions; each is loaded by a tool that transpiles, and ledgering them seeds 42 identical entries, which is the 'ledger nobody trusts' failure #4311's own header warns about",
            "B — bring them into a program via a shared tsconfig.tools.json named in each typecheck script: closes the class and the evasion, costs 42 manifests plus an unmeasured number of surfaced errors",
            "C — a separate, deliberately coarse invariant over root tool configs with its own ledger for stragglers (B with a seed)"
          ],
          "recommendation": "A for now, which is what shipped — but as a recorded decision (#10870) rather than an oversight. Real business need is the weak axis: nothing is pulling on type-checked vitest configs, and no defect has been traced to one. Startup scope discipline cuts the same way — B expands gate surface across 42 packages with no measured pull. The AI-correctness axis is the one that argues for B eventually (agents add config files constantly), so this is worth revisiting the first time a broken tool config actually costs something, not before."
        }
      ],
      "out_of_scope_findings": [
        "filed as #10868: 8 packages' scripts/i18n-extract.config.ts fail to type-check, 1-3 x TS2883 each — the inferred type of `default` names hash-suffixed internal chunks of @objectstack/spec's dist, so it is non-portable by construction; two candidate repairs, one of which lives in spec rather than in the 8 consumers",
        "filed as #10869: packages/plugins/plugin-auth/examples/basic-usage.ts imports @objectstack/plugin-hono-server, which the package declares in NO dependency block (1 x TS2307) — the census's only file in no tsc program at all, and a genuinely broken example the blind spot hid",
        "filed as #10870: [finding] the 54 package-root tool configs and the depth > 0 scoping line, for a maintainer to rule on (see open_questions)"
      ]
    }

    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