Skip to content

build(repo): typecheck the test sources and close the last Phase 1 items - #8

Merged
evk-soft merged 1 commit into
mainfrom
chore/typecheck-tests
Aug 8, 2026
Merged

build(repo): typecheck the test sources and close the last Phase 1 items#8
evk-soft merged 1 commit into
mainfrom
chore/typecheck-tests

Conversation

@evk-soft

@evk-soft evk-soft commented Aug 8, 2026

Copy link
Copy Markdown
Owner

What lands and why

The last two Phase 1 deferred items — packet 4B step 5 and packet 5B step 5 — are @ts-expect-error
and type-identity assertions. Writing them exposed that they could not have meant anything.

packages/ai-tooling/tsconfig.json has include: ["src/**/*.ts"], and Vitest transpiles without
checking types. No .spec.ts file had ever been seen by a compiler. An @ts-expect-error would
have suppressed nothing and failed nothing. The plan assumed otherwise; its expected result is
"every @ts-expect-error is exercised", which requires a compiler that sees them.

Why the guard lives at the repository root

Two shapes were rejected first:

  • Extending the workspace tsconfig.jsonrootDir: src with composite and outDir: dist
    would emit the tests into dist, and pack:check asserts the published tarball's contents
    exactly.
  • A tsconfig.test.json beside the testscheck-stage1-artifacts.mjs reports any file under
    its owned roots (packages/ai-tooling, configs/ai) that no phase manifest declares as
    undeclared-entry, and those manifests are committed records of what each phase delivered, not
    lists to be reopened.

So it is scripts/check-test-types.mjs, beside the four guards that already exist, outside the
scanner's roots.

It derives its compiler options from each workspace's own tsconfig.json rather than restating them,
minus the emit-shaped ones, so the two cannot drift apart. allowJs is the single added option and
it is load-bearing: two specs import the repository's own scripts/*.mjs guards directly, and
without it every such import is an implicit any (TS7016) and the assertions written against them
check nothing. checkJs stays off.

It is deliberately not part of pnpm check, which Stage 1 phase gates invoke literally and which
must not acquire new failure modes. It runs as its own script and its own CI step, exactly like
check:structure and check:supply-chain — inside the existing Node 24 (pnpm) job, which is
already a required check on both platforms, so no branch-protection change is needed.

Two TypeScript 7 behaviours shape the invocation, both measured rather than assumed:

diagnostic consequence
TS5112 — command-line file arguments refused while a tsconfig.json is present --ignoreConfig is required
TS6053 — file arguments are not glob-expanded the file list is enumerated through git ls-files

Six real type errors, none introduced here

Turning the guard on surfaced six errors in four existing spec files: four noUncheckedIndexedAccess
violations indexing a regex capture and a Uint8Array, one unchecked sources[0], and one
exactOptionalPropertyTypes violation passing candidate: undefined explicitly where omitting the
property was meant. All six are fixed by narrowing, never by loosening a compiler option.

How it was verified

The guard fails on a real error. A deliberate const x: number = "string" added to a spec makes
it exit 1 with tests/unit/render-json.spec.ts(66,7): error TS2322; removing it restores exit 0,
and the file was confirmed byte-identical afterwards.

Every suppression is real. Removing the @ts-expect-error markers makes all five underlying
errors surface — which is the only thing that shows the assertions are load-bearing rather than
decorative:

diagnostic what it establishes packet
TS2558: Expected 0 type arguments, but got 1 parseStrictJson<ConfigV1> is not expressible 4B
TS2741: Property '[strictJsonBrand]' is missing StrictJsonDocument cannot be built from a literal 4B
TS2739: 'StrictJsonDocument' is missing … from 'ConfigV1' parser output is not a domain type 4B
TS2739: 'LockV1' is missing sources, platforms validate('lock', …) is not ConfigV1 5B
TS2345: '"lock"' is not assignable to '"config"' the generic follows the name, not the caller 5B

That is packet 4B step 5's three requirements and packet 5B step 5's three, and it closes the last of
the Phase 1 deferred items.

Full gate, every command exiting 0 against the committed tree:

pnpm install --frozen-lockfile --ignore-scripts
pnpm check                    # 149 unit/package/security + 7 integration = 156 tests
pnpm run check:structure      # 5 workspaces no cycle; 37 source files no boundary escape
pnpm run check:supply-chain   # 0 advisories; 0 licence conflicts
pnpm run check:test-types     # 1 workspace, 14 test sources
pnpm run changelog:check      # 0 fragments valid
node packages/ai-tooling/scripts/check-stage1-artifacts.mjs --phase 1 --tree
git diff --check

Deliberately not included

No branch-protection change. The new step runs inside the Node 24 (pnpm) job, which is already
a required check on ubuntu-latest and windows-latest; a failing step fails that job. Adding a
fourth required check would mean splitting it into its own job for no additional guarantee.

No changelog fragment. Build tooling and test corrections ship no behaviour change, and
changelog.d/ stays dormant until Stage 1 Phase 5 completes.

No product code. packages/ai-tooling/src/** is untouched; only test sources changed under that
tree.

The last two Phase 1 deferred items are packet 4B step 5 and packet 5B step 5,
both @ts-expect-error and type-identity assertions. Writing them exposed that
they could not have meant anything: packages/ai-tooling/tsconfig.json has
include: ["src/**/*.ts"] and Vitest transpiles without checking types, so no
.spec.ts file had ever been seen by a compiler. An @ts-expect-error would have
suppressed nothing and failed nothing. The plan assumed otherwise -- its expected
result is "every @ts-expect-error is exercised", which requires a compiler that
sees them.

Two shapes were rejected before the third was chosen. Extending the workspace
tsconfig fails because rootDir: src with composite and outDir: dist would emit
the tests into dist, and pack:check asserts the tarball contents exactly. A
tsconfig.test.json beside the tests fails because check-stage1-artifacts.mjs
reports any file under its owned roots -- packages/ai-tooling and configs/ai --
that no phase manifest declares as undeclared-entry, and those manifests are
committed records of what each phase delivered, not lists to be reopened. So the
guard lives at scripts/check-test-types.mjs, beside the four that already exist,
outside the scanner's roots.

It derives its compiler options from each workspace's own tsconfig.json rather
than restating them, minus the emit-shaped ones, so the two cannot drift apart.
allowJs is the single added option and it is load-bearing: two specs import the
repository's own scripts/*.mjs guards directly, and without it every such import
is an implicit any and the assertions written against them check nothing. checkJs
stays off. The guard is deliberately not part of pnpm check, which Stage 1 phase
gates invoke literally and which must not acquire new failure modes; it is its
own script and its own CI step, like check:structure and check:supply-chain.

Two TypeScript 7 behaviours shape the invocation, both measured: TS5112 refuses
command-line file arguments while a tsconfig.json is present, so --ignoreConfig
is required, and TS6053 shows that file arguments are not glob-expanded, so the
file list is enumerated through git ls-files.

Turning it on surfaced 6 real type errors in four existing spec files, none of
them introduced here: four noUncheckedIndexedAccess violations indexing a regex
capture and a Uint8Array, one unchecked sources[0], and one
exactOptionalPropertyTypes violation passing candidate: undefined explicitly
where omitting the property was meant. All six are fixed by narrowing rather than
by loosening a compiler option.

The guard is proven in both directions rather than assumed. A deliberate
const x: number = "string" in a spec makes it exit 1 with the exact diagnostic,
and removing it restores exit 0. Removing the @ts-expect-error markers makes all
five suppressed errors surface, which is what shows they are real: TS2558 that
parseStrictJson<ConfigV1> is not expressible, TS2741 that StrictJsonDocument
cannot be built from a literal, TS2739 that parser output is not a domain type,
TS2739 that validate('lock', ...) is not ConfigV1, and TS2345 that the generic
follows the name argument rather than the caller. That is packet 4B step 5's
three requirements and packet 5B step 5's three.
@evk-soft
evk-soft merged commit 8f2769e into main Aug 8, 2026
3 checks passed
@evk-soft
evk-soft deleted the chore/typecheck-tests branch August 8, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant