Skip to content

build(rest): give the package a test-layer tsc program and ledger its 37 errors - #12570

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-12542-rest-test-typecheck-program
Aug 26, 2026
Merged

build(rest): give the package a test-layer tsc program and ledger its 37 errors#12570
os-litant merged 1 commit into
mainfrom
claude/issue-12542-rest-test-typecheck-program

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#12542

packages/rest/tsconfig.json excludes **/*.spec.ts and **/*.test.ts, and the
package's typecheck script was tsc --noEmit against that config and nothing
else. So no tsc program compiled a single test file in the package — the
shape AGENTS.md names twice ("never exclude *.test.ts", "a @ts-expect-error
in a file no tsc program compiles is a phantom check"), sitting live in one of
the repo's largest packages.

Premise, re-derived rather than inherited

At 5fbd58e0d (my branch base; the card measured at 52a982388):

claimmeasured
include / exclude["src/**/*"] / ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
typecheck scripttsc --noEmit, that program and nothing else
test files149 (git ls-tree -r --name-only HEAD -- packages/rest | grep -cE '\.test\.ts$')
.spec.ts files0 — the **/*.spec.ts half of the exclusion covers nothing
all under src/?yes, 149/149 — nothing hidden by an unreachable include
tsconfig.test.json / test-typecheck-debt.jsonabsent
check:test-typecheck wiredno

The card's numbers hold exactly, at both shas. Only the .spec.ts line is an
addition: the exclusion's second glob was already inert.

What landed — an adoption, not an invention

packages/rest/tsconfig.test.json follows packages/client's and
packages/spec's landed shape: module semantics only, plus lib, with
strictness inherited and untouched. It is named by the typecheck script
through the shared scripts/check-test-typecheck.mts gate, because a config no
script invokes is exactly the phantom this change is about:

"typecheck": "tsc --noEmit && pnpm check:test-typecheck"

tsx joins devDependencies for the same reason packages/client and
packages/spec carry it — the shared gate runs through it.

What the program INHERITED, and did not re-declare

packages/rest/tsconfig.json already carries two #9960 settings, both
load-bearing, and this file deliberately re-declares neither:

  • rootDir: ".." — already widened to packages/ there, as a consequence
    of the paths rule rather than a preference. So the TS6059 pile that forced
    packages/client's sibling config to widen rootDir does not arise here; it
    was paid for already.
  • paths: { "@objectstack/metadata-protocol": [".../src/index.ts"] } — a
    child that declared its own paths would replace this map rather than
    merge into it, silently sending that specifier back to dist/. This file
    declares no paths at all, so the rule stands and 22 of the producer's source
    files are in the test program exactly as they are in the build one.

The number nobody had (ruling 5), measured at 5fbd58e0d

37 errors across 13 files, with the workspace closure built first
(pnpm --filter '@objectstack/rest^...' build, exit 0).

codecountnote
TS255414wrong arity, all fourteen "Expected 2-5 arguments, but got 1"
TS1804813"possibly undefined", all in src/export-integration.test.ts
TS23455
TS70064implicit any
TS61331unused local

Neither stop-and-report fork fires: 37 is above the "small, client-scale"
threshold and nowhere near "enormous", and it is not one producer-side defect
wearing many files' clothes — five codes across 13 files, the largest single
file holding 17.

Ruling 4's question, answered for all 37: would this error exist on
origin/main if the program had always been there? Yes, for every one.
No
test file is edited by this PR; the program differs from the build config only
in module semantics and lib, and each of the 37 sits on a line that predates
this branch. None is caused by the change, so all 37 are ledger entries in
packages/rest/test-typecheck-debt.json — EXACT and shrink-only.

Why the 155 in TEST_DEBT became 37

Not a rescale — the old entry predicted it. 121 of its 155 were TS2835 plus the
implicit-any pile TS2835 causes, and 16 more were one Array.prototype.at
message against a lib older than es2022. This package is "type": "module",
so NodeNext was compiling extensionless relative imports as unresolvable ESM and
every symbol they named became any. Under vitest-matching semantics TS2835
x72 goes to 0, TS2550 x16 to 0, TS7006 x49 to 4. What is left is a different
shape, and it grew in one place while collapsing in four: TS18048 x13 is a class
the 155 never contained at all — "possibly undefined" reads that only become
visible once the imports above them resolve to real types. Sizing this off the
TS2835 line alone would have said "155 − 121 = 34" and been wrong in both
directions. That is the #8612 lesson, measured a second time.

No test file is edited (ruling 3), and the #12421 coupling

Zero test files are touched: the deliverable is the program plus the measured
ledger. PR #12421 holds packages/rest/src/rest.test.ts, which carries 4 of
the 37 and therefore has a ledger entry — which collides with nothing, where an
edit would have been a serial breach.

⚠️The two interact by design, and the next reader needs to know it. The
ledger is EXACT and shrink-only, so once #12421 merges: an error it adds to
rest.test.ts reds the ledger on its run ("the debt GREW"); an error it
removes reds the entry as stale until re-recorded ("the debt SHRANK"). Both
are the pin working, and both land on the change that caused them.

Both @ts-expect-error directives in this layer — in src/rest.test.ts and
src/rest-api-plugin-slot-lookups.test.ts — were phantom checks that evaluated
never. Under this program neither reports TS2578, so both are live and each is
suppressing a real error.

Anti-vacuity: the program can say no (ruling 6)

Membership.tsc -p tsconfig.test.json --listFiles puts 149 of
packages/rest/src/**/*.test.ts in the program — the exact census — out of 489
files total.

Ablation. A real type error planted in src/analytics-routes.test.ts, an
unledgered file, so the everyday verdict is what gets exercised:

HEAD blob hash : d533503f6e47ac65d36be9066dcded31dc48aca8
mutated disk hash : 7a92cb594097f9f902f1a303ca83f637e7238a8a
injected marker occurrences: 1
MUTATION CONFIRMED ON DISK
GATE_EXIT=1
- src/analytics-routes.test.ts: 2 type error(s) in a file the ledger does not cover.
post-restore disk hash : d533503f6e47ac65d36be9066dcded31dc48aca8
RESTORE PROVEN: disk hash == HEAD blob hash, and `git diff HEAD` is empty for the path

The gate exits non-zero and names the file. The restore ran from a trap on
EXIT/INT/TERM against absolute paths, used git checkout HEAD -- path
(never the bare form, which restores from a polluted index), and is proven by
blob hash — never by an exit code. It reports 2 errors, not 1: the second is
noUnusedLocals on the planted binding, which is itself evidence that inherited
strictness is live. No build or dist/ is involved on either leg — tsc reads
these sources directly — so there is no artifact staleness to preflight.

Two mechanical consequences, both required by gates rather than chosen

1. @objectstack/rest graduates out of TEST_DEBT. Once the typecheck
script names the sibling config, check:type-check-coverage reports
has a TEST_DEBT entry but ... no longer hides its tests -- it graduated. The
entry is deleted and a graduation note recorded in its place, per the file's own
convention. Headline moves: 19 → 18 packages hiding tests, 1110 → 965
hidden files, 1461 → 1306 frozen errors — exactly −1 package, −149 files,
−155 errors.

2. check:type-source-resolution sees a new program, and this is the one
judgement call in the PR — flagged rather than buried.
Since #11490 that gate's
population is every tsconfig*.json a typecheck script names, so the new
program joins it and reports six workspace deps the build program never reached:
driver-sql, metadata, plugin-hono-server, plugin-security,
service-analytics, service-datasource — all six viatsconfig.test.json.

I recorded them in that package's registry entry, with the program-set numbers
stated in place as the registry's own rule requires: before, at 5fbd58e0d,
--list reported 93 programs / 77 packages / 54 entries / 233 pairs; after,
94 / 77 / 54 / 239. +1 program, +0 entries, +6 pairs.

The gate's failure text asks for paths rules instead and calls registry
widening "not the fix", so here is why I did not do that — measured both ways
on the same checkout
, not argued:

  • paths redirecting the six to source takes the test layer from 37 errors to
    42
    , and the +5 are TS6133 in
    ../plugins/plugin-hono-server/src/{hono-plugin,current-user-endpoints}.ts and
    ../drivers/driver-sql/src/sql-driver.tsother packages' source billed to
    packages/rest/test-typecheck-debt.json
    , where they would then go red on
    those packages' PRs. Worse, those five are not even real: both packages run
    pnpm --filter ... typecheck green on this same tree (exit 0, measured), so
    the borrowed program MANUFACTURES diagnostics that belong to nobody. A ledger
    seeded with those cannot be paid down by the package that owns the file.
  • It would also make the type program diverge from the runtime one:
    packages/rest/vitest.config.ts aliases exactly two of the six
    (plugin-hono-server, service-datasource) to source and resolves the other
    four through dist/. Blanket paths is not fidelity to vitest either.
  • The landed precedent for this exact shape is the registry entry, twice:
    @objectstack/client and @objectstack/trigger-record-change both carry
    test-program deps there, the latter being the package that took this very
    #5286 sibling route.
  • Ruling 1 is "module semantics ONLY". A six-entry paths block is not that.

⚠️ The tension is real and I am naming it, not resolving it unilaterally: the
registry's doc-block permits a widening when the set of programs changed
which is literally true here, 93 to 94 — but adds "that is a change to this file,
not to a package", written when the gate read only tsconfig.json and a package
could not move the population. This PR is the first case where a package does.
Happy to switch to paths if a maintainer reads that sentence as binding.

Verification

All runs below are at 2dfb401c1, the pushed head, taken after the final
commit. Every exit code captured before any pipe (redirect, then $?).

  • pnpm --filter @objectstack/rest typecheck — exit 0. Gate's own verdict line:
    check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 13 file(s) / 37 error(s) held in test-typecheck-debt.json (shrink-only).
  • 25 gate families from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack against the real change set, all exit 0
    including check:type-check-coverage, check:type-source-resolution,
    check:published-files, check:test-source-alias, check:nul-bytes,
    check:pm-dispatch-gates and scripts/pm/bare-root-worklist.mjs --self-test
    (the last two are the convention-triggered pair this diff incurs by editing
    gate scripts).
  • pnpm lint (eslint . --no-inline-config, the whole repo, not narrowed) —
    exit 0, VERDICT command-exit 0.
  • Heavy runs serialised through scripts/pm/os-verify-lock.sh; every one ended
    VERDICT command-exit 0.

One declared narrowing: pnpm check:type-check-debt was NOT run. It is
check-type-check-coverage.mjs --re-measure, and it refuses on this worktree —
--re-measure cannot run: 32 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk. That is a PREREQUISITE NOT MET, not a
red gate: it declines to measure rather than measuring a different world. Its
prerequisite is the full workspace build that lint.yml runs before the step,
which CI does on this PR regardless. Three things make the omission a
measurement rather than a gap: --re-measure scores each ledgered package under
its own tsconfig, and @objectstack/rest is in neither ledger after this
diff, so it is not among the projects measured; outside packages/rest this diff
changes only two gate scripts' data and prose — no package's tsconfig and no
package's source; and the lockfile delta is exactly three lines in
packages/rest's own importer, adding an already-resolved tsx@4.23.12, so no
other package's closure moved. The structural half of the same gate —
check:type-check-coverage, which owns TESTS_COVERED, PINS_CHECKED, RECONCILED
and the composition invariants, and is what this diff actually moves — ran green
above.

Changeset: deliberately none, and the rule applied

packages/rest's files is ["dist", "README.md", "CHANGELOG.md"]. This PR
changes no src/ file, so dist/ is byte-unaffected; tsconfig.test.json and
test-typecheck-debt.json are not published; and tsx is a devDependency no
consumer installs. Nothing is released, which is the skip-changeset label's
own definition in pr-automation.yml. The in-tree precedent for this exact
change shape is 951b025e4
build(trigger-record-change): graduate the package out of the TEST_DEBT ledger
(PR #11489), package.json + tsconfig.test.json + the coverage gate, no
changeset.


Generated by Claude Code

… 37 errors
`packages/rest/tsconfig.json` excluded `**/*.test.ts` and `typecheck` was
`tsc --noEmit` against that config, so no tsc program compiled any of the
package's 149 test files and both `@ts-expect-error` directives in that layer
were phantom checks.
Adopts the #5286 sibling route: `tsconfig.test.json` with vitest-matching
module semantics and untouched strictness, named by the `typecheck` script
through the shared `check-test-typecheck.mts` gate. No test file is edited;
the 37 errors the program finds (13 files) are recorded per file in
`test-typecheck-debt.json`, EXACT and shrink-only.
Two mechanical consequences, both required by gates rather than chosen:
`@objectstack/rest` graduates out of TEST_DEBT (155 raw, re-measured 37), and
its `check:type-source-resolution` registry entry gains the six workspace deps
the new program reaches, with the program-set numbers stated in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️1 changed file(s) yielded no anchor (packages/rest/test-typecheck-debt.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/test-typecheck-debt.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1b7e3d2ce20cca14357581b7697743fa67f1ee7epackageMentionDocs.

@github-actionsgithub-actionsBot added the dependencies Pull requests that update a dependency file label Aug 26, 2026
@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 26, 2026 — with Claude
@os-litant
os-litant marked this pull request as ready for review August 26, 2026 11:25
@os-litant
os-litant added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit 3f41a21Aug 26, 2026
38 checks passed
@os-litant
os-litant deleted the claude/issue-12542-rest-test-typecheck-program branch August 26, 2026 11:50
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-litant@claude