Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5106,6 +5106,16 @@ jobs:
continue-on-error: true
run: pnpm check:type-check-coverage

# The workspace ROOT's own program (`tsconfig.json`: `scripts/` and the
# top-level configs) is outside every package's `typecheck`, so the turbo
# typecheck never reads it. This step does (#26236), and the coverage gate
# above requires it once `typecheck:root` exists. The program imports no
# built `dist/`, so it needs no build. Sub-second to a few seconds.
- name: Type-check the workspace root program
id: type-check-the-workspace-root-program
continue-on-error: true
run: pnpm typecheck:root

# Conformance meta-gate (#4363): `spec/src/data/*-conformance.ts` holds the
# shared case-sets that exist so the independent drivers answer ONE
# standard, and three separate changesets claimed a future driver "is held
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test": "VITEST_MAX_WORKERS=$(node scripts/vitest-worker-cap.mjs) turbo run test test:repo --concurrency=50%",
"test:smoke": "turbo run test:smoke",
"typecheck": "turbo run typecheck",
"typecheck:root": "tsc --noEmit -p tsconfig.json",
"clean": "turbo run clean && rm -rf dist",
"setup": "pnpm install && pnpm --filter @objectstack/spec build",
"prepare": "node scripts/setup-git-hooks.mjs",
Expand Down
39 changes: 17 additions & 22 deletions scripts/check-type-check-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -663,12 +663,12 @@ const SPEC_SCRIPTS_PRECEDENT = 'packages/spec/tsconfig.scripts.json';
const TEST_ROOTDIR_PRECEDENT = '`packages/client/tsconfig.test.json` and `packages/cli/tsconfig.test.json`';
const GENERATED_INCLUDE_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/10880';

// A path in the root program whose edits move the `@objectstack/spec-monorepo`
// count below, declared as a bare, whole-literal path so the dispatch
// derivation can read it. It accounted for 29 of that entry's 80, and is 1 of
// the 26 standing now -- still the root script an ordinary card edits. The note on
// that entry carries the accounting and references this constant instead of
// spelling the path inside its sentence.
// A path in the root program, declared as a bare, whole-literal path so the
// dispatch derivation can read it. It accounted for 29 of the
// `@objectstack/spec-monorepo` DEBT entry's 80 errors, and for 1 of the 26 left
// when that entry graduated (#26236). The root program is now type-checked by the
// root's `typecheck:root` script, which lint.yml runs, and this is still the
// root script an ordinary card edits.
//
// Why the SHAPE and not just the mention: scripts/pm/dispatch-gates.mjs scans
// each gate's own module body for path literals and accepts one only when the
Expand Down Expand Up @@ -832,22 +832,17 @@ const ROOT_PROGRAM_COUPLED_SCRIPT = 'scripts/check-test-typecheck.mts';
// fixed the test config showed 4 more (TS2883 on the `vi.fn()` fields of
// `src/__mocks__/runtime.ts`, annotated `Mock`) that the build config never shows.
// No cast, no `@ts-expect-error`, no `src` signature widened.
const DEBT = {
'@objectstack/spec-monorepo': {
errors: 26,
note: 'the workspace root itself: config-tier 18 (TS2307 x17, TS2550 x1); noise 8 (TS7006 x7, '
+ 'TS6133 x1). The root `tsconfig.json` is the program for `scripts/` and the top-level configs and '
+ 'declares `types:["node"]`; packages extend `tsconfig.base.json` instead. Declaring the Node types '
+ 'moved this count by zero -- the TS2591 / TS2584 pile an older note blamed on their absence was '
+ 'already gone. What stands: the analytics-reconcile tree imports `@objectstack/*` and example '
+ 'sources the root program cannot resolve (the root package declares no workspace dependency), '
+ 'which is all 17 TS2307 and the 7 TS7006 they cascade into; ' + ROOT_PROGRAM_COUPLED_SCRIPT
+ ' calls `Object.hasOwn` under lib ES2020 (TS2550); and `exclude` only drops files from the '
+ 'initial walk, so an example source a script imports is still compiled here -- the TS6133 is in '
+ 'examples/app-showcase/src, and this entry moves with the showcase as well as with scripts/. '
+ 'Graduating also needs a `typecheck:root` script and its step in .github/workflows/lint.yml.',
},
};
//
// `@objectstack/spec-monorepo` -- the workspace ROOT itself -- GRADUATED from this
// ledger (#26236; entry: 26 raw, repaired to 0). Its program is the root
// `tsconfig.json`: `scripts/` and the top-level configs. 24 of the 26 were the
// obsolete ADR-0021 analytics-reconcile harness, which was deleted (its imports hid
// 31 more errors on keys the spec had removed). The other two were `Object.hasOwn`
// under lib ES2020, fixed by giving the root program lib ES2022, and an unused
// const in an example source a script imported. Graduated the root's way: a
// `typecheck:root` script plus its step in .github/workflows/lint.yml, both of
// which the root branch of `evaluate` requires.
const DEBT = {};

// Package name -> why running tsc over it is not applicable at all. An EXEMPT
// entry is a statement about the package's nature, not about its debt; if the
Expand Down
10 changes: 5 additions & 5 deletions scripts/pm/dispatch-gates.data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -759,9 +759,9 @@ export const ESCAPABLE_LITERAL_LEDGER_ROWS = Object.freeze([
*
* ⚠ Its known limit, stated here rather than discovered later: `exclude` drops
* files only from tsc's INITIAL WALK, so sources under an excluded directory
* that a root script IMPORTS are pulled into the program anyway — the ledger
* note for this entry records 4 of its 80 errors arriving exactly that way,
* from the showcase example. A path-shaped trigger cannot see an import graph,
* that a root script IMPORTS are pulled into the program anyway — the root's
* DEBT note recorded 4 of its 80 errors arriving exactly that way, from the
* showcase example, before the root graduated. A path-shaped trigger cannot see an import graph,
* so a card editing only such a file is still not sent here. That is
* deliberately NOT closed on this card: the direction is the safe one (this
* entry under-covers rather than over-covers), and closing it means resolving
Expand Down Expand Up @@ -978,8 +978,8 @@ export const CHANGE_KIND_ROWS = [
matches: 'root-ts-program',
gates: [
{
name: 'check:type-check-debt',
why: 'the ROOT ledger entry (@objectstack/spec-monorepo) IS this program, so a file here moves its raw tsc count even though your diff touches no package — measured, one added bench file put it 19 over and cost a CI round. It is a shrink-only ratchet: the repair is to make the file typecheck, and raising the entry is maintainer-only — ⛔ MAINTAINER-ONLY under the #8435 convention — never the co-equal option. Most of this class is one missing setting rather than real breakage — the root config carries lib ES2020 and no types, so process and console are absent unless the file declares them ambiently. Needs the workspace closure BUILT — on an unbuilt worktree it refuses outright, and that throw means NOT MEASURED, never `not applicable to me`. Build first, exactly as lint.yml does: pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/* (quote the filter values for your shell)',
name: 'typecheck:root',
why: 'nothing else type-checks this program: it belongs to no package, so the turbo typecheck never reads a file here, and the root has no ledger entry left to absorb an error. lint.yml runs this script in the Type Check source-gates lane, and one new error reds that lane. The repair is to make the file typecheck, never to add an exclude or bring back a DEBT entry. It needs no build: the program reads no package dist. The program carries lib ES2022 and types node, so `process`, `console` and the `node:*` builtins resolve without a reference line',
},
],
},
Expand Down
10 changes: 7 additions & 3 deletions scripts/pm/dispatch-gates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2223,7 +2223,10 @@ export function extractCheckInvocations(workflowText, workflowFile, { via = null
// this card is about: a value that reaches the program only through
// `process.env`, which no reader of the command line can see.
const carriedEnv = envNamesNotSpelledInCommand(cmd, stepEnv);
for (const m of cmd.matchAll(/pnpm\s+(?:--filter\s+(\S+)\s+)?(?:run\s+)?(check:[\w:-]+)/g)) {
// `typecheck:root` is the one non-`check:` script read here: it is the ROOT
// program's only type check, and the root-program change kind names it, so it
// has to resolve to a family or that row renders STALE.
for (const m of cmd.matchAll(/pnpm\s+(?:--filter\s+(\S+)\s+)?(?:run\s+)?(check:[\w:-]+|typecheck:root(?![\w:-]))/g)) {
out.push(withVia({ check: m[2], filter: m[1] ?? null, workflow: workflowFile, envVariables: carriedEnv }));
}
for (const m of cmd.matchAll(DIRECT_CHECK_INVOCATION)) {
Expand Down Expand Up @@ -10779,8 +10782,9 @@ export function guardsADurabilityCriticalCall(path, readSource = readTrackedSour
}

/**
* Is this path inside the ROOT package's tsc program — the population behind
* the `@objectstack/spec-monorepo` entry of `check:type-check-debt`?
* Is this path inside the ROOT package's tsc program — the population the
* root's `typecheck:root` script checks (until the root graduated, the
* `@objectstack/spec-monorepo` entry of `check:type-check-debt`)?
*
* This is the one gate population in the tree that no path literal can ever
* describe, in principle rather than by omission. The root program is declared
Expand Down
34 changes: 25 additions & 9 deletions scripts/pm/dispatch-gates.self-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,14 @@ export function selfTest({ tier = 'full' } = {}) {
+ 'right names a file this ROOT does not hold, and every audit downstream then passes for the wrong reason',
!pkgInvs.some((i) => String(i.script ?? i.check).startsWith('scripts/')),
);
t(
'the root program\'s `typecheck:root` is read as a family, so the root change kind can name it',
extractCheckInvocations(' - run: pnpm typecheck:root\n', 'lint.yml').some((i) => i.check === 'typecheck:root' && !i.direct),
);
t(
'control — that exception is the one script name, not every `typecheck` spelling',
extractCheckInvocations(' - run: pnpm typecheck\n - run: pnpm typecheck:rooted\n - run: pnpm --filter ./examples/x run typecheck\n', 'x.yml').length === 0,
);
t(
'control — a CLIMBING spelling is refused rather than resolved: `..` is not a prefix segment, because '
+ 'a path this ROOT cannot resolve is exactly how a phantom identity key gets minted',
Expand Down Expand Up @@ -1488,7 +1496,12 @@ export function selfTest({ tier = 'full' } = {}) {
// resolve.
const rootScriptsMap = JSON.parse(readFileSync(nodePath.join(ROOT, 'package.json'), 'utf8')).scripts ?? {};
const zeroFile = tsLiveFamilies.filter(([, e]) => (e.files ?? []).length === 0);
// `typecheck:root` is the one other zero-file family, by construction: it is a
// tsc program over the root config, not a script file, and the cards it owes
// are named by the root-program change kind (`root-ts-program`), never by a
// path read out of a script.
const unexplained = zeroFile
.filter(([check]) => check !== 'typecheck:root')
.map(([check]) => [check, rootScriptsMap[check] ?? ''])
.filter(([, cmd]) => !/\bpnpm\b[^&|]*?(?:--filter|--recursive|-r)\b[^&|]*?\brun\b/.test(cmd))
.map(([check, cmd]) => `${check} (${cmd || 'no root script'})`);
Expand Down Expand Up @@ -4423,10 +4436,12 @@ export function selfTest({ tier = 'full' } = {}) {
// rename, which is the one rot class the STALE branch exists to report.
const rootKind = changeKindLines(['scripts/bench/runtime-publish-gate.bench.mts'], resolved);
t('a root-program path emits the convention section', rootKind.length === 2 && rootKind[0].includes('ROOT tsc program'));
t('and it names the RATCHET half — the invocation that re-measures', rootKind.some((l) => l.includes('- pnpm check:type-check-debt —')));
const rootLine = rootKind.find((l) => l.includes('- pnpm check:type-check-debt —')) ?? '';
t('the root-program line refuses the baseline raise and states the real repair', /shrink-only/.test(rootLine) && /maintainer-only/.test(rootLine));
t('and carries the built-closure prerequisite, like the other ratchet line', /closure BUILT/.test(rootLine) && rootLine.includes('turbo run build'));
// The root graduated from the DEBT ledger (#26236), so the gate that reads
// this program is its own `typecheck:root` script, which lint.yml runs.
t('and it names the root program\'s own type check', rootKind.some((l) => l.includes('- pnpm typecheck:root —')));
const rootLine = rootKind.find((l) => l.includes('- pnpm typecheck:root —')) ?? '';
t('the root-program line refuses an exclude or a returning ledger entry and states the real repair', /never to add an exclude/.test(rootLine) && /make the file typecheck/.test(rootLine));
t('and says it needs no build, unlike the ratchet lines', /needs no build/.test(rootLine) && !/closure BUILT/.test(rootLine));
// Re-pointed rather than deleted (#12074). This case pinned one property —
// a `.mjs` checker script is NOT in the ROOT tsc program, unlike the `.mts`
// bench file above — and the gate-script kind below now makes the same path
Expand All @@ -4436,7 +4451,7 @@ export function selfTest({ tier = 'full' } = {}) {
// hiding inside a `length` this case no longer checks.
const checkerKind = changeKindLines(['scripts/check-type-check-coverage.mjs'], resolved);
t('a checker script is still outside the ROOT tsc program', !checkerKind.some((l) => l.includes('ROOT tsc program')));
t('…and the root ratchet is not named for it', !checkerKind.some((l) => l.includes('- pnpm check:type-check-debt —')));
t('…and the root type check is not named for it', !checkerKind.some((l) => l.includes('- pnpm typecheck:root —')));
t('…and the ONE section it does emit is the gate-script kind', checkerKind.length === 3 && checkerKind[0].includes('GATE SCRIPT'));

// -- The GATE SCRIPT entry (#12074) --------------------------------------
Expand Down Expand Up @@ -5813,11 +5828,12 @@ export function selfTest({ tier = 'full' } = {}) {
// Seven, not six, since #10542 added check:cross-package-test-inputs to the
// test-file kind. `a.test.ts` is a root-level TypeScript file, so it is BOTH
// a test file and inside the root tsc program and legitimately hits two
// kinds. The ratchet therefore renders twice, under a different `why` each
// time — pinned just below, because a bare count cannot tell that apart from
// one kind rotting away.
// kinds. Each kind renders its own gate as STALE (the ratchet under the test
// kind, `typecheck:root` under the root kind since the root graduated) —
// pinned just below, because a bare count cannot tell that apart from one
// kind rotting away.
t('an undiscoverable gate renders as STALE', stale.filter((l) => l.includes('STALE')).length === 7);
t('a root-level test file hits both kinds, so the ratchet renders STALE under each', stale.filter((l) => l.includes('\u26a0 check:type-check-debt: STALE')).length === 2);
t('a root-level test file hits both kinds: the ratchet renders STALE under the test kind, the root type check under the root kind', stale.filter((l) => l.includes('\u26a0 check:type-check-debt: STALE')).length === 1 && stale.filter((l) => l.includes('\u26a0 typecheck:root: STALE')).length === 1);
// Per NAME, anchored on both sides of the rendered name (`⚠ x: STALE`), so the
// pair that shares one script is reported apart: a count alone stays green if
// one of the two is dropped from the table and something else is added, and a
Expand Down
Loading