👷 Type-check the TS floor in CI

Add a `compat` job that type-checks the whole suite and a consumer fixture
against the minimum supported TypeScript (5.9), reusing the `dist/` artifact
`build` produced and gating `publish`. The compiler is resolved by npx, so it
never enters `devDependencies` or the local `check`/`verify` loop.

The fixture imports the package by name, resolving the emitted declarations
through the `exports` map; `expectTypeOf` / `.not.toBeAny()` make it reject an
`any`-typed declaration, which a bare compile would accept.

Correct the README consumer floor from >= 5.0 to >= 5.9 (set by `type-fest`)
and drop the `node10` resolution claim, which the exports-only entry never
satisfied.
This commit is contained in:
tmu committed 2026-09-29 20:51:41 +00:00
1 parent 45df45df4b
commit 75807c4bd8
10 files changed
+218 -8

No files matched your search

+69
View File
@@ -6,6 +6,9 @@ the job graph; this file records why it is shaped the way it is.
## Pipeline
- **`build`** (push to `main` / tag) — build + correctness + packaging.
- **`compat`** (push to `main` / tag) — type-check the suite and a consumer
fixture against the minimum supported TypeScript; consumes `build`'s `dist/`
and gates `publish`. See [§ TypeScript compatibility](#typescript-compatibility).
- **`maintain`** (push to `main`, non-blocking) — `npm run maintain`; reports,
never fails the build.
- **`publish`** (tag) — packaging checks + `publish:publint` / `publish:attw`,
@@ -130,6 +133,72 @@ the `build` job; `npm run verify` stays coverage-free.
lists it under `--all`. It carries a file-level `/* c8 ignore start */` with
the reason. Adding runtime code there means removing that directive.
## TypeScript compatibility
#### Decision (2026-09)
A dedicated `compat` job runs `npm run test:compat` — the `npx`-pinned
TypeScript 5.9 compiler (`typescript@5.9.2`) over `compat/tsconfig.json` —
against the `dist/` artifact `build` produced, and `publish` requires it. The
floor is TypeScript 5.9, pinned in the `test:compat` script itself (the single
source of truth) and documented in [README § Requirements](../README.md#requirements).
#### Why
- The compiler is a **different major** from the repo's TypeScript 7, so it is
resolved by `npx` at run time. It must never appear in `devDependencies`: that
would install it for every local `npm ci` and drift the lockfile, which would
put a second compiler in the local `check` / `verify` loop and every editor.
- **CI-only is the point.** `npx` fetches over the network — like `maintain`'s
scans, a network-bound check is never a local feedback tier (see
[workflow.md § Feedback tiers](./workflow.md#feedback-tiers)). Locally the
same gate is reproducible with `npm run build && npm run test:compat`.
- **`compat` consumes `build`'s artifact** rather than rebuilding, so it judges
the exact bytes `check`, `test:ci` and `publint` saw.
- **`compat` gates `publish`** because the types are the feature: an artifact
that is not consumable at the advertised floor must not ship.
- **The fixture is a consumer, not a unit test.** `compat/fixture.ts` imports
the package by name (`tiny-pattern-ts`), so it resolves through the `exports`
map to `dist/index.d.ts` and exercises the emitted declarations' relative
`.ts` specifiers — not the source. `expectTypeOf` / `.not.toBeAny()` are
load-bearing: a bare compile would also pass if a declaration collapsed to
`any`; the exact-equality assertions reject that.
#### Rejected
- **A `devDependencies` alias** (`npm:typescript@5.9`): installs the legacy
compiler locally, defeating "CI only".
- **A second lockfile / sub-project** (`compat/` with its own `npm ci`): a
pinned, reproducible matrix, but a whole extra lockfile to maintain for one
compiler. `npx -p` is enough.
- **A `paths` / `moduleSuffixes` redirect** to typecheck the _existing_ suite
against `dist/` without touching it: `paths` cannot remap the relative
`./index.ts` imports the tests use; `moduleSuffixes` only lets _missing_
source resolve to suffixed copies, so it would need generated `.compat.ts`
declarations staged into `src/` (plus excludes). Both spend more than the
fixture buys. See the [handover](../backlog.tasks) discussion.
- **Writing the fixture against source** (relative import): it would prove the
source compiles under 5.9, not that the _published_ declarations do, which is
the promise consumers rely on.
- **Replacing `attw`**: `attw` owns the full resolution matrix
(`node10`/`node16`/`nodenext`/`bundler`); `compat` answers only "does the
documented floor compile the artifact".
#### Known issue
- `@tsconfig/node26` cannot be extended: its `lib: ["es2025", ...]` and
`target: es2025` are rejected by 5.9 (`TS6046`). `compat/tsconfig.json`
extends only `@tsconfig/strictest` and sets `lib` / `target: es2024`, the
ceiling 5.9 accepts.
- `skipLibCheck: false` is deliberate — it is what makes the floor honest
(`type-fest` pins it at 5.9), rather than hiding a broken dependency d.ts
behind `true`.
- The version appears in both the `test:compat` script and the README; a floor
bump is a two-file change. The script is authoritative.
- `src/doc-test` is excluded from `compat/tsconfig.json`. The generated examples
are checked against the source by their own project; `test:compat` covers the
suite plus the fixture.
## Coverage serving
#### Decision (2026-09)