Skip to content

Commit ea92b6e

Browse files
dmealingclaude
andcommitted
feat(ci): scripts/ is typechecked — the gap that shipped two accessor defects
`bun test` transpiles per file and never typechecks, and `bun run --filter '*' typecheck` walks WORKSPACE PACKAGES only. Everything under scripts/ — including every gate in the `gates` lane — was checked by nothing. It cost twice in one session, both the same shape: a property read that does not exist on the type, so it was always undefined and the tool silently reported the wrong thing. `node.title` is not an accessor on MetaData, so the scaffolder printed a node's NAME where it meant the authored title; and the harness generator read its two prose fields the same way, emitting 109 empty slots and reporting success. Both are one tsc run away from impossible, and the gate was proven by reintroducing exactly that shape and watching it fail. FOUR REAL ERRORS surfaced in code committed earlier today. The loader's errors are typed `Error` while carrying a `code` at runtime, so the report reached for a field the type never promised; a regex capture group was read without narrowing under noUncheckedIndexedAccess; and — the interesting one — a bare import and a relative import of the same files gave TypeScript two separate declarations of one class, so MetaRoot was not assignable to MetaData over a private field. That is the type-level shadow of the cross-package `instanceof` defect. tsconfig.scripts.json's `paths` maps every @metaobjectsdev specifier onto the workspace source, which is what bun resolves to anyway, collapsing it to one identity. Switching those imports to bare instead typechecked cleanly and reddened all four requirement gates at runtime — scripts/ is outside the workspace, so bun finds no link to walk to. Caught by running the lane, which is what the lane is for. THE PRE-EXISTING DEBT IS PINNED, NOT EXCLUDED. scripts/site/ carries 14 errors, all noUncheckedIndexedAccess narrowing in real matching and highlighting logic where a careless `!` would mask a bug; fixing them is its own task. Excluding the directory would make the gate read as full coverage while covering less, so instead everything outside it must be CLEAN and it may hold at most the known count. New errors fail wherever they land, the debt can only shrink, and the gate asks for the pin to be tightened when it does. bun-types becomes a root devDependency so tsc's default typeRoots finds it; the alternative was a version-pinned path into bun's isolated install layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NynrRND6ZUwGvq3ZUTCfxG
1 parent 897f042 commit ea92b6e

7 files changed

Lines changed: 281 additions & 4 deletions

File tree

‎bun.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
"private": true,
55
"description": "MetaObjects — cross-language metadata standard. Repo-root JS/TS workspace.",
66
"type": "module",
7-
"workspaces": ["server/typescript/packages/*", "client/web/packages/*"],
7+
"workspaces": [
8+
"server/typescript/packages/*",
9+
"client/web/packages/*"
10+
],
811
"scripts": {
912
"build": "bun run --filter '*' build",
1013
"typecheck": "bun run --filter '*' typecheck",
@@ -15,6 +18,7 @@
1518
"prerelease:publish": "bun scripts/prerelease.mjs"
1619
},
1720
"devDependencies": {
21+
"bun-types": "latest",
1822
"highlight.js": "^11.11.0",
1923
"typescript": "^5.6.0"
2024
},

‎scripts/check-requirements-ledger.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ function main(): number {
9797
}
9898

9999
// Every diagnostic code the run mentioned, in first-seen order.
100-
const codes = [...output.matchAll(/\b(ERR_[A-Z0-9_]+|WARN_[A-Z0-9_]+)\b/g)].map((m) => m[1]);
100+
const codes = [...output.matchAll(/\b(ERR_[A-Z0-9_]+|WARN_[A-Z0-9_]+)\b/g)]
101+
.map((m) => m[1])
102+
.filter((c): c is string => c !== undefined);
101103
const errors = codes.filter((c) => c.startsWith("ERR_"));
102104
if (errors.length > 0) {
103105
console.error(`requirements-ledger: ${errors.length} error(s): ${[...new Set(errors)].join(", ")}\n`);

‎scripts/check-scripts-typecheck.ts‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Gate — repo-root `scripts/` typechecks.
4+
*
5+
* `bun test` transpiles per file and never typechecks, and `bun run --filter '*'
6+
* typecheck` walks WORKSPACE PACKAGES only. So everything under `scripts/` — including
7+
* every CI gate in the `gates` lane — was never typechecked by anything.
8+
*
9+
* That gap has already cost twice, in one session: `node.title` was read as an
10+
* accessor that does not exist on `MetaData`, so it was always `undefined` and the
11+
* tool silently reported a node's NAME where it meant the authored title; and the
12+
* requirement harness read its two prose fields through accessors that do not exist,
13+
* emitting 109 empty slots and reporting success. Both are one `tsc` run away from
14+
* being impossible.
15+
*
16+
* ── The pre-existing debt is PINNED, not excluded ───────────────────────────────
17+
*
18+
* `scripts/site/` carries 14 errors that predate this gate, all `noUncheckedIndexedAccess`
19+
* index-access narrowing in real matching and highlighting logic. Fixing them means
20+
* understanding invariants tsc cannot see, and a careless `!` there would mask a real
21+
* bug — so it is its own task.
22+
*
23+
* Excluding that directory would have made the gate read as full coverage while
24+
* silently covering less, which is the failure this repository has a rule against. So
25+
* the debt is pinned instead: everything outside `scripts/site/` must be CLEAN, and
26+
* `scripts/site/` may carry at most the known count. New errors fail wherever they
27+
* land, the debt can only shrink, and when it shrinks the gate says so and asks for
28+
* the pin to be tightened.
29+
*/
30+
import { spawnSync } from "node:child_process";
31+
import { fileURLToPath } from "node:url";
32+
import { dirname, resolve } from "node:path";
33+
34+
/** Pre-existing, all in `scripts/site/`. Lower this when you fix some; never raise it. */
35+
const KNOWN_SITE_ERRORS = 14;
36+
const DEBT_DIR = "scripts/site/";
37+
38+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
39+
40+
function main(): number {
41+
const run = spawnSync(
42+
"node_modules/.bin/tsc",
43+
["-p", "tsconfig.scripts.json"],
44+
{ cwd: REPO_ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
45+
);
46+
const output = `${run.stdout ?? ""}${run.stderr ?? ""}`;
47+
const errors = output.split("\n").filter((l) => /error TS\d+:/.test(l));
48+
49+
const fresh = errors.filter((l) => !l.startsWith(DEBT_DIR));
50+
if (fresh.length > 0) {
51+
console.error(`scripts-typecheck: ${fresh.length} type error(s) outside the known debt:\n`);
52+
for (const e of fresh) console.error(` ${e}`);
53+
console.error(
54+
`\nscripts/ is not covered by any package tsconfig, which is how two accessor\n` +
55+
`defects shipped in one session. Fix the error; do not widen this gate.\n`,
56+
);
57+
return 1;
58+
}
59+
60+
const debt = errors.length - fresh.length;
61+
if (debt > KNOWN_SITE_ERRORS) {
62+
console.error(
63+
`scripts-typecheck: ${DEBT_DIR} now has ${debt} type error(s), up from the pinned ` +
64+
`${KNOWN_SITE_ERRORS}.\nThe pre-existing debt may shrink, never grow.\n`,
65+
);
66+
for (const e of errors) console.error(` ${e}`);
67+
return 1;
68+
}
69+
if (debt < KNOWN_SITE_ERRORS) {
70+
console.error(
71+
`scripts-typecheck: ${DEBT_DIR} is down to ${debt} error(s) from ${KNOWN_SITE_ERRORS}. ` +
72+
`Lower KNOWN_SITE_ERRORS in this file to lock the improvement in.\n`,
73+
);
74+
return 1;
75+
}
76+
77+
console.log(
78+
`scripts-typecheck: OK — scripts/ is clean outside ${DEBT_DIR}, which holds ` +
79+
`${debt} pinned pre-existing error(s).`,
80+
);
81+
return 0;
82+
}
83+
84+
process.exit(main());

‎scripts/ci-local.sh‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ gate_peer_ranges() { bun scripts/check-peer-ranges.ts; }
201201
# partial fragment stays quiet. The self-test replays all three incidents.
202202
gate_doc_examples() { bun scripts/check-doc-examples.ts && bun scripts/test-doc-examples.ts; }
203203

204+
# ── repo-root scripts/ typechecks ─────────────────────────────────────────────
205+
# `bun test` transpiles per file and never typechecks, and `bun run --filter '*'
206+
# typecheck` walks WORKSPACE PACKAGES only — so every script here, including every
207+
# gate in this lane, was checked by nothing. Two accessor defects came straight out
208+
# of that gap in one session: a property read that does not exist on the type, so it
209+
# was always undefined and the tool silently reported the wrong field. The 14
210+
# pre-existing errors in scripts/site/ are PINNED rather than excluded, so the gate
211+
# cannot read as full coverage while covering less.
212+
gate_scripts_typecheck() { bun_install && bun scripts/check-scripts-typecheck.ts; }
213+
204214
# ── MetaObjects' own requirements ledger must load and verify ─────────────────
205215
# The requirement feature ships in all five ports and this repository did not use
206216
# it. The ledger is the dogfood, on the hardest available subject: this repo
@@ -477,6 +487,7 @@ if want gates; then step "script-name hook collisions" gate_script_name_
477487
if want gates; then step "metamodel-version bump" gate_metamodel_version; fi
478488
if want gates; then step_if bun "peer-range bounds" gate_peer_ranges; fi
479489
if want gates; then step_if bun "shipped doc examples load" gate_doc_examples; fi
490+
if want gates; then step_if bun "scripts/ typecheck" gate_scripts_typecheck; fi
480491
if want gates; then step_if bun "requirements ledger verifies" gate_requirements_ledger; fi
481492
if want gates; then step_if bun "requirements cover vocabulary" gate_requirements_vocabulary; fi
482493
if want gates; then step_if bun "metamodel scaffolder" gate_scaffold_metamodel; fi

‎scripts/lib/requirement-vocabulary.ts‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,21 @@
99
import { readdirSync, readFileSync } from "node:fs";
1010
import { fileURLToPath } from "node:url";
1111
import { dirname, join, resolve } from "node:path";
12-
import { MetaDataLoader, type MetaRoot } from "../../server/typescript/packages/metadata/src/index.js";
12+
// RELATIVE, and it has to be: `scripts/` sits outside the bun workspace, so a bare
13+
// `@metaobjectsdev/*` specifier does not resolve from here at all — bun walks up from
14+
// this file and finds no workspace link. (Switching these to bare typechecked fine and
15+
// reddened all four gates at runtime, which is what the lane is for.)
16+
//
17+
// The dual-identity problem a relative import used to cause — TypeScript seeing the
18+
// bare import inside `requirement-check.ts` and this one as two separate declarations
19+
// of the same class, so `MetaRoot` was not assignable to `MetaData` over a private
20+
// field — is closed by `tsconfig.scripts.json`'s `paths`, which maps every
21+
// @metaobjectsdev specifier onto these same source files. One identity, without
22+
// needing an import bun cannot follow.
23+
import {
24+
MetaDataLoader,
25+
type MetaRoot,
26+
} from "../../server/typescript/packages/metadata/src/index.js";
1327
import {
1428
REQUIREMENT_SUBTYPE_ARCHITECTURAL,
1529
} from "../../server/typescript/packages/metadata/src/core/requirement/requirement-constants.js";
@@ -28,6 +42,20 @@ export const DEFAULT_LEDGER_DIR = join(REPO_ROOT, "metaobjects");
2842
export const LEVEL_SUBTYPE = 4;
2943
export const LEVEL_ATTR = 5;
3044

45+
/**
46+
* The loader's `code` off an error the type system says is a plain `Error`.
47+
*
48+
* `LoadResult.errors` is `Error[]`, but what the loader actually puts there carries a
49+
* `code` (`ParseError` sets `ERR_*`). Narrowed through `unknown` rather than reached
50+
* for directly, because an unchecked property access reads as though the type had
51+
* promised the field — and a report that silently loses its diagnostic codes still
52+
* looks like a report.
53+
*/
54+
function errorCode(e: Error): string {
55+
const c = (e as unknown as { code?: unknown }).code;
56+
return typeof c === "string" ? c : "";
57+
}
58+
3159
/**
3260
* Load the ledger under `dir` in strict mode, reporting load errors the same way
3361
* everywhere and returning `undefined` so the caller can exit non-zero.
@@ -47,7 +75,10 @@ export async function loadLedgerOrReport(
4775
const { root, errors } = await MetaDataLoader.fromDirectory(dir, { strict: true });
4876
if (errors.length > 0 || root === undefined) {
4977
console.error(`${gate}: the ledger does not load${detail}.\n`);
50-
for (const e of errors) console.error(` ${e.code ?? ""} ${e.message}`);
78+
// The loader's errors are typed `Error` and carry a `code` at RUNTIME
79+
// (ParseError sets ERR_*). Narrowed rather than reached for blindly: an
80+
// unchecked property access reads as though the type promised it.
81+
for (const e of errors) console.error(` ${errorCode(e)} ${e.message}`);
5182
return undefined;
5283
}
5384
return root;

‎tsconfig.scripts.json‎

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
{
2+
"//": [
3+
"Typecheck for scripts/ \u2014 the repo-root tooling that no package tsconfig covers.",
4+
"",
5+
"`bun test` transpiles per file and never typechecks, and `bun run --filter '*'",
6+
"typecheck` walks WORKSPACE PACKAGES only, so every script here was unchecked. Two",
7+
"defects in one session came straight out of that gap: `node.title` read as an",
8+
"accessor that does not exist (always undefined, silently reporting the wrong",
9+
"field) and the same class again in the harness generator.",
10+
"",
11+
"`paths` maps every @metaobjectsdev specifier to the workspace SOURCE, which is what",
12+
"bun resolves to at runtime once `bun install` has run. Without it tsc sees a bare",
13+
"import and a relative import to the same files as two separate class declarations",
14+
"\u2014 the type-level shadow of the cross-package `instanceof` defect."
15+
],
16+
"extends": "./server/typescript/tsconfig.base.json",
17+
"compilerOptions": {
18+
"noEmit": true,
19+
"declaration": false,
20+
"declarationMap": false,
21+
"sourceMap": false,
22+
"rootDir": ".",
23+
"baseUrl": ".",
24+
"paths": {
25+
"@metaobjectsdev/angular": [
26+
"./client/web/packages/angular/src/index.ts"
27+
],
28+
"@metaobjectsdev/angular/*": [
29+
"./client/web/packages/angular/src/*"
30+
],
31+
"@metaobjectsdev/react": [
32+
"./client/web/packages/react/src/index.ts"
33+
],
34+
"@metaobjectsdev/react/*": [
35+
"./client/web/packages/react/src/*"
36+
],
37+
"@metaobjectsdev/runtime-web": [
38+
"./client/web/packages/runtime-web/src/index.ts"
39+
],
40+
"@metaobjectsdev/runtime-web/*": [
41+
"./client/web/packages/runtime-web/src/*"
42+
],
43+
"@metaobjectsdev/tanstack": [
44+
"./client/web/packages/tanstack/src/index.ts"
45+
],
46+
"@metaobjectsdev/tanstack/*": [
47+
"./client/web/packages/tanstack/src/*"
48+
],
49+
"@metaobjectsdev/ai-runtime": [
50+
"./server/typescript/packages/ai-runtime/src/index.ts"
51+
],
52+
"@metaobjectsdev/ai-runtime/*": [
53+
"./server/typescript/packages/ai-runtime/src/*"
54+
],
55+
"@metaobjectsdev/cli": [
56+
"./server/typescript/packages/cli/src/index.ts"
57+
],
58+
"@metaobjectsdev/cli/*": [
59+
"./server/typescript/packages/cli/src/*"
60+
],
61+
"@metaobjectsdev/codegen-ts-angular": [
62+
"./server/typescript/packages/codegen-ts-angular/src/index.ts"
63+
],
64+
"@metaobjectsdev/codegen-ts-angular/*": [
65+
"./server/typescript/packages/codegen-ts-angular/src/*"
66+
],
67+
"@metaobjectsdev/codegen-ts-react": [
68+
"./server/typescript/packages/codegen-ts-react/src/index.ts"
69+
],
70+
"@metaobjectsdev/codegen-ts-react/*": [
71+
"./server/typescript/packages/codegen-ts-react/src/*"
72+
],
73+
"@metaobjectsdev/codegen-ts-tanstack": [
74+
"./server/typescript/packages/codegen-ts-tanstack/src/index.ts"
75+
],
76+
"@metaobjectsdev/codegen-ts-tanstack/*": [
77+
"./server/typescript/packages/codegen-ts-tanstack/src/*"
78+
],
79+
"@metaobjectsdev/codegen-ts": [
80+
"./server/typescript/packages/codegen-ts/src/index.ts"
81+
],
82+
"@metaobjectsdev/codegen-ts/*": [
83+
"./server/typescript/packages/codegen-ts/src/*"
84+
],
85+
"@metaobjectsdev/conformance": [
86+
"./server/typescript/packages/conformance/src/index.ts"
87+
],
88+
"@metaobjectsdev/conformance/*": [
89+
"./server/typescript/packages/conformance/src/*"
90+
],
91+
"@metaobjectsdev/docs-site": [
92+
"./server/typescript/packages/docs-site/src/index.ts"
93+
],
94+
"@metaobjectsdev/docs-site/*": [
95+
"./server/typescript/packages/docs-site/src/*"
96+
],
97+
"@metaobjectsdev/forge": [
98+
"./server/typescript/packages/forge/src/index.ts"
99+
],
100+
"@metaobjectsdev/forge/*": [
101+
"./server/typescript/packages/forge/src/*"
102+
],
103+
"@metaobjectsdev/integration-tests": [
104+
"./server/typescript/packages/integration-tests/src/index.ts"
105+
],
106+
"@metaobjectsdev/integration-tests/*": [
107+
"./server/typescript/packages/integration-tests/src/*"
108+
],
109+
"@metaobjectsdev/metadata": [
110+
"./server/typescript/packages/metadata/src/index.ts"
111+
],
112+
"@metaobjectsdev/metadata/*": [
113+
"./server/typescript/packages/metadata/src/*"
114+
],
115+
"@metaobjectsdev/migrate-ts": [
116+
"./server/typescript/packages/migrate-ts/src/index.ts"
117+
],
118+
"@metaobjectsdev/migrate-ts/*": [
119+
"./server/typescript/packages/migrate-ts/src/*"
120+
],
121+
"@metaobjectsdev/render": [
122+
"./server/typescript/packages/render/src/index.ts"
123+
],
124+
"@metaobjectsdev/render/*": [
125+
"./server/typescript/packages/render/src/*"
126+
],
127+
"@metaobjectsdev/runtime-ts": [
128+
"./server/typescript/packages/runtime-ts/src/index.ts"
129+
],
130+
"@metaobjectsdev/runtime-ts/*": [
131+
"./server/typescript/packages/runtime-ts/src/*"
132+
],
133+
"@metaobjectsdev/sdk": [
134+
"./server/typescript/packages/sdk/src/index.ts"
135+
],
136+
"@metaobjectsdev/sdk/*": [
137+
"./server/typescript/packages/sdk/src/*"
138+
]
139+
}
140+
},
141+
"include": [
142+
"scripts/**/*.ts"
143+
]
144+
}

0 commit comments

Comments
 (0)