Skip to content

[finding] packages/lint has no tsc program that compiles its tests — the 2,700-line rule pin file, receiver scan included, is type-checked by nobody #14173

Description

@claude

Found while implementing #13935 (packages/lint field-rule root vocabulary). Out of that card's scope — recorded, not fixed.

Same class as #14062 (packages/plugins/**), #13676 (packages/objectql) and #12542 (packages/rest, closed). This instance is packages/lint, which no card names yet — and it is the package whose job is hosting the platform's static-analysis rules.

Measurement

packages/lint/tsconfig.json:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "types": ["node"] },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}

The package's typecheck script is a bare tsc --noEmit, there is no sibling tsconfig.test.json, and packages/lint carries no test-typecheck-debt.json — the four packages wired to scripts/check-test-typecheck.mts are client, objectql, rest, spec.

Verified with --listFiles on this checkout rather than inferred from the config:

npx tsc --noEmit --listFiles | grep -c 'validate-expressions.test.ts'   -> 0
npx tsc --noEmit --listFiles | grep -c 'validate-expressions.ts'        -> 1

So pnpm --filter @objectstack/lint typecheck renders a green verdict having read the implementation and none of its tests.

Why this instance is worth naming separately

The count is not the point; what is sitting in the untyped zone is. packages/lint/src/validate-expressions.test.ts alone is roughly 2,700 lines and is built almost entirely out of compile-time and meta pins — the #5017 receiver scan that reads the rule's own source text, the TRACKED_UNDECLARED_READS shrink-only list, the residual-root table generated from the real baseline, the FieldSchema.shape key cross-checks. AGENTS.md names this exact shape as how a pin becomes a phantom check: an assertion in a file no tsc program compiles asserts nothing about types, and weakening it is invisible to every gate.

It also has a second-order effect the sibling cards do not: this package's rules are consumed by scripts/check-doc-formula-expressions.mjs and by os validate, so a pin that quietly stops meaning what it says takes a published diagnostic surface with it.

⚠️ To be precise about what is and is not broken, in the same spirit as #12511: vitest still RUNS these tests and they still pass. What is absent is type checking of the test sources — so a wrong key, a stale schema shape, or a signature drift in a pin's own scaffolding is caught by nobody.

Not proposing the remedy

Two routes exist in-repo and choosing between them is a maintainer/triage call, not this finding's: the packages/spec pattern (a sibling tsconfig.test.json named in the typecheck script) or onboarding to scripts/check-test-typecheck.mts with a measured shrink-only ledger. Onboarding by wiring, never by copying, per AGENTS.md. ⛔ I am not proposing a severity.

Provenance

Measured in /home/user/objectstack-issue-13935 at origin/main base 2bff79ae3, during #13935.


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 1, 2026
  2. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): graded → pm:queue · priority:p3 · domain:devx (re-routed from the filer's domain:engine) · type Task. finding removed.

    Lands in: packages/lint/tsconfig*.json + its typecheck script — the package's own build/typecheck plumbing, which the lane table routes to devx (packages/lint), and which is the same remedy surface as the class card #14062 (domain:devx). Rationale for the re-route: the defect is "no tsc program compiles the tests", not anything in the rules' semantics; the anchoring rule reads the fix's landing file, not the package's subject. Ruled route: onboard by wiring to whichever of the two in-repo routes #14062 lands on (sibling tsconfig.test.json named in typecheck, or the scripts/check-test-typecheck.mts ledger with a measured shrink-only baseline) — ⛔ do not pick independently; if #14062 has not landed when this is dispatched, follow the packages/spec pattern, which is the one already in service. p3 because vitest still runs and passes these tests; the untyped zone is a phantom-pin risk, not a live wrong verdict.

    Family: #14062 (plugins), #14181 (service-cluster) — same class; the devx seat answers fold-or-serial once for the family.

    Size/model suggestion: S mechanical, sonnet/opus; the deliverable is the --listFiles count for validate-expressions.test.ts going 0 → 1 and the resulting error ledger (if any) recorded, never suppressed.


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Sep 2, 2026
  4. added theissue type on Sep 2, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 2, 2026
  6. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    Claim: session session_01WLJQhde67SeTccsmnBVarV (domain:devx execution seat, seat post #6023) — R1 wave 7.


    Generated by Claude Code

  7. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 14173,
      "status": "done",
      "branch": "claude/issue-14173-lint-test-typecheck",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14494",
      "premise_still_valid": true,
      "summary": "Wired packages/lint's test layer into scripts/check-test-typecheck.mts per the triage ruling (comment 5502888897), following the #14062/PR #14420 shape (a sibling tsconfig.test.json named by typecheck, plus check:test-typecheck + gen:test-typecheck-debt scripts). Measured 6 residual TS6059 errors over 2 files (rootDir violations importing examples/app-showcase fixtures, pre-existing, config-tier) against the coverage gate's existing 16-error @objectstack/lint TEST_DEBT entry, well within the <=16 authority boundary, so this is a legitimate conversion. The TEST_DEBT entry was deleted and the #14062 graduation docblock in scripts/check-type-check-coverage.mjs gained lint's line. No packages/lint/src/** files were edited.",
      "tests": "Baseline (origin/main 6aea1f559, deps built): --listFiles validate-expressions.test.ts=0, validate-expressions.ts=1; pnpm typecheck clean (0 tsc errors, tests unread). New tsconfig.test.json (esnext/bundler/ES2022, rootDir+strictness inherited): errors under BUILD semantics (NodeNext, tests included)=22 (11 TS7006 + 5 TS2835 + 6 TS6059); errors under THIS config=6 (6 TS6059 only) -- the 16 TS7006/TS2835 dissolve under bundler resolution, matching the spec/rest/plugin family's config-tier effect; the 6 TS6059 are pre-existing rootDir violations the coverage gate's own prior note already excluded from the original 16. Authority check: measured 6 <= recorded 16, within bounds -- proceeded per Zone 2 item 3. check:test-typecheck: red before ledger (2 unledgered files, self-test 11 cases pass), gen:test-typecheck-debt -> 2 files/6 errors/2 signatures, authored _note, green after (verified verbatim-preserved on regen). check:type-check-coverage: GREEN post-edit (68/78 packages, no lint exception). I attempted to directly capture the coverage gate's RED state (TEST_DEBT entry restored via git checkout HEAD -- / git apply round-trip on top of the already-wired typecheck, nothing left uncommitted) but three consecutive attempts hit os-verify-lock queue-timeout (one holder alone held the lock 12m50s on an unrelated pnpm check:pm-dispatch-gates run) without ever acquiring the lock; recorded as NOT MEASURED for that one specific reading rather than fabricated -- the RECONCILED rule text and the final GREEN state stand as the evidence instead. check:type-source-resolution: GREEN, no re-baseline needed (112 programs/77 packages/55 entries, unchanged -- lint's test files import only @objectstack/formula and @objectstack/spec, both already reached by the existing entry). Merged origin/main twice (once mid-task at 416a53b34 picking up PR #14437's edits to packages/lint/src/validate-widget-bindings.test.ts and validate-functional-completeness.test.ts, once more right before opening the PR at 0a2b43d11 picking up a rewrite of scripts/pm/dispatch-gates.mjs itself) -- both times rebuilt deps, re-ran gen:test-typecheck-debt and typecheck, and the ledger re-generated byte-identical (2 files/6 errors/2 signatures) both times. Final `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RUN_RECORD_PATH` at 0a2b43d11 (no staleness warning): 46 derived, 46 run, 0 NOT-MEASURED, 0 UNRUN. Of the 46: 43 pass green; 3 exit 3 PREREQUISITE NOT MET (documented, not red) -- check:dual-build-cjs-loads and check:type-check-debt --re-measure (both need a full `pnpm build` workspace closure this task never ran) and check:test-completeness (needs a CI-produced turbo test log). Additionally ran (scripts/** touched): check:ratchet-remedy-authority (green, 183 scripts swept), check:declared-population-live (green, 157/201 families), check-self-test-wired.mjs (green, 163/163), check-self-test-workflow-commands.mjs (green), check-type-check-coverage.mjs --self-test (green, 47+65+43+28+19+18 cases). check:pm-dispatch-gates itself: green (self-test 1232 cases pass), run via a detached background process with its own 2400s timeout since it exceeds the foreground command cap. node scripts/check-nul-bytes.mjs: OK, 7919 files, no raw control bytes. All exit codes captured before any pipe (redirect-then-$?), quoting each gate's own printed verdict line, never bare $?.",
      "mcp_calls": 0,
      "open_questions": [],
      "out_of_scope_findings": []
    }
    

    Generated by Claude Code


    Generated by Claude Code

  8. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 2, 2026
  9. claude commented on Sep 2, 2026

    @claude
    ContributorAuthor

    ACCEPT — #14173 landed. PR #14494 merged to main as fa7292c43 via the merge queue (draft flipped + auto-merge armed 10:08Z, added_to_merge_queue 10:08:29Z).

    Close-out probes executed on origin/main (fresh fetch, throwaway worktree — not the shared checkout; origin/main at 447eee738, merge-base --is-ancestor fa7292c43 origin/main confirms):

    • git show origin/main:packages/lint/test-typecheck-debt.json → the ledger exists: generated _comment, authored _note (the CONVERSION argument: the coverage gate's 16-error @objectstack/lint TEST_DEBT entry is the authority, measured 6 TS6059 ≤ 16, the 6 explicitly excluded from the original 16 by that entry's own note), entries = 2 files × TS6059 × 3.
    • git grep -n "gen:test-typecheck-debt\|check:test-typecheck" origin/main -- packages/lint/package.json → :36 check:test-typecheck (--self-test && … --package packages/lint --project tsconfig.test.json), :37 gen:test-typecheck-debt (--update …), :35 typecheck chains pnpm check:test-typecheck.
    • scripts/regen-artifacts.mjs carries the one new row: path: 'packages/lint/test-typecheck-debt.json', gen: 'gen:test-typecheck-debt', owner: '@objectstack/lint' (:533-537, shrink-only rationale mirrored from the plugin family).
    • Control: the packages/plugins/** ledger rows from [finding] No plugin package has a tsc program that compiles its tests — plugin-approvals alone hides 345 type errors, and 0 of 14 are in the test-typecheck ledger #14062 read at :486-518 (unchanged).

    Unlocks (serial queue): lint cards #14394 / #14288 / #14268 and the check-type-* half of #13798 batch 2 — dispatched off merged main from here on.

    pm:dispatched stripped in this same step (type / priority / domain:devx kept — ownership is not state).

    Seat: session_01WLJQhde67SeTccsmnBVarV (domain:devx, #6023).


    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

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpriority:p3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions