Skip to content

Commit c20069e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-10877-self-test-wiring-presence
# Conflicts: # scripts/check-required-contexts.mjs
2 parents 957ea2a + 7c02a45 commit c20069e

9 files changed

Lines changed: 1555 additions & 95 deletions

apps/docs/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
"build": "pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs && NODE_OPTIONS='--max-old-space-size=4096' next build",
1010
"start": "next start",
1111
"site:lint": "next lint",
12-
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
1312
"postinstall": "fumadocs-mdx",
14-
"typecheck": "tsc --noEmit"
13+
"typecheck": "fumadocs-mdx && next typegen && tsc --noEmit"
1514
},
1615
"dependencies": {
1716
"fumadocs-core": "16.14.4",

apps/docs/tsconfig.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@
3434
]
3535
}
3636
},
37+
// `typecheck` runs `next typegen` before tsc on purpose, and these two
38+
// `.next` globs are why. Next writes both of them itself
39+
// (`writeConfigurationDefaults`) and re-adds either one on the next
40+
// `next dev` / `next build` if you delete it -- measured by running that
41+
// routine against a narrowed copy of this file: the `dev` glob came
42+
// straight back and the file was rewritten. This array cannot be narrowed,
43+
// so the honesty has to come from the script instead.
44+
//
45+
// .next/types/** produced by the `next typegen` that the `typecheck`
46+
// script now runs itself, so the checked program is a
47+
// function of source rather than of whatever `.next` an
48+
// earlier build happened to leave behind. A bare
49+
// `tsc --noEmit` here passed over a broken route
50+
// signature precisely because these files were absent.
51+
// .next/dev/types/** written only by `next dev`. Next's own build-mode
52+
// type check filters this directory OUT of the program
53+
// (`getDevTypesPath`, lib/typescript/runTypeCheck.js)
54+
// "to prevent stale dev types from causing errors when
55+
// routes have been deleted since the last dev session".
56+
// Plain tsc has no such filter, so a leftover dev
57+
// session can only add a local false RED here, never a
58+
// false green. Remedy: `rm -rf apps/docs/.next`.
59+
//
60+
// Do not reduce `typecheck` back to a bare `tsc --noEmit`.
3761
"include": [
3862
"next-env.d.ts",
3963
"**/*.ts",

packages/objectql/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"scripts": {
2121
"build": "tsup",
2222
"test": "vitest run",
23-
"typecheck": "tsc --noEmit"
23+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json"
2424
},
2525
"dependencies": {
2626
"@objectstack/core": "workspace:*",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// The SCRIPTS-layer type-check program for @objectstack/objectql (#10756).
2+
//
3+
// `packages/objectql/scripts/` held a compatibility checker with a documented
4+
// CLI -- `dry-run-hash-compat.ts`, which `src/dry-run-hash-compat.test.ts`
5+
// imports `runDryRun` from -- and no tsc program this package's `typecheck`
6+
// runs had ever read a line of it. `tsconfig.json` selects `src/**/*` and the
7+
// directory is not under it, so the package passed `check:type-check-coverage`
8+
// as COVERED with a whole source directory unchecked. Injecting a `number`
9+
// initialised with a string into that file moved no gate.
10+
//
11+
// A SIBLING rather than a wider `include` on `tsconfig.json`, which is the
12+
// distinction #5475 drew for `packages/spec` and it holds for the same reason
13+
// here: that config emits (`rootDir: "src"`, `outDir: "dist"`, and `dev` runs
14+
// `tsc -w` through it), so widening it to reach `scripts/` would put the
15+
// directory in front of the emit and `rootDir` would reject it. This program
16+
// emits nothing, so it can neutralise `rootDir` without touching what ships.
17+
//
18+
// STRICTNESS IS INHERITED and deliberately not relaxed: `strict`,
19+
// `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns` and the rest come
20+
// from the root config through `tsconfig.json`. The directory type-checks clean
21+
// under them today -- it entered with ZERO recorded debt, and there is no
22+
// ledger here to record any in.
23+
{
24+
"extends": "./tsconfig.json",
25+
"compilerOptions": {
26+
"noEmit": true,
27+
// `.` rather than the inherited `src`, because the files this program
28+
// checks are the ones outside `src`. Safe precisely because nothing is
29+
// emitted from here -- see the header.
30+
"rootDir": "."
31+
},
32+
"include": ["scripts/**/*"]
33+
}

scripts/check-entry-guard.mjs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,60 @@ const KNOWN_IMPORT_UNSAFE = new Set([
447447
'scripts/ts-parse.mjs',
448448
]);
449449

450+
/**
451+
* The SCAN SURFACE, written in the syntax `scripts/pm/dispatch-gates.mjs` can
452+
* read. It is a DIFFERENT claim from the roster above, and keeping the two
453+
* apart is the whole of this declaration.
454+
*
455+
* ── The defect this repairs (#10784) ────────────────────────────────────────
456+
*
457+
* That derivation scans a gate's module body for path-ish string literals and
458+
* reads what it finds as the gate's DECLARED POPULATION. The only literals this
459+
* file carried were the ten KNOWN_IMPORT_UNSAFE entries — an enumeration of the
460+
* files that ALREADY violate the import-safety half. So the derivation believed
461+
* this gate's population was a roster of ten existing files, and a NEWLY ADDED
462+
* file under the walked root could never appear in it, BY CONSTRUCTION.
463+
* Measured on a card whose surface was one new script:
464+
*
465+
* pnpm check:entry-guard [lint.yml] names: scripts/check-changeset-no-major.mjs,
466+
* scripts/check-empty-changeset.mjs, scripts/check-error-status-conformance.mjs, …
467+
*
468+
* — filed under `Silent`, "source names paths, none of which cover yours", for
469+
* exactly the input this gate is most likely to fail on. Silent is that
470+
* derivation's weakest claim; here it was not weak but INVERTED, and the cost
471+
* was paid rather than hypothetical: a new script went green in a local gate
472+
* union and red in CI on `Lint & Repo Gates`, after the dev had reported.
473+
*
474+
* `main()` walks the root and judges every file it finds, new ones included.
475+
* The walk is the population; the roster is DATA the walk is compared against.
476+
* This declares the first in a form the derivation can match and leaves the
477+
* second alone — the same separation the derivation's own docblock draws for
478+
* gates that "compute their own population and name only their baseline
479+
* artifact".
480+
*
481+
* ── Why the subtree spelling, and not a wider extractor ─────────────────────
482+
*
483+
* `hintCovers` refuses a bare single-segment literal as too generic, and the
484+
* refusal is measured rather than incidental: teaching the extractor to accept
485+
* bare top-level directory words was priced at +139084 fabricated (gate, file)
486+
* pairs, because `packages`, `apps` and `examples` are path COMPONENTS in
487+
* dozens of gates that never read those roots. A declared subtree is a
488+
* different claim — an author stating what this gate reads — and the glob
489+
* collapse reduces it back to this one root and to nothing else. One gate pays
490+
* for its own precision instead of every gate paying for one gate's.
491+
*
492+
* ── Provenance, never a lookup key ──────────────────────────────────────────
493+
*
494+
* Nothing in this gate reads this array; `walk(SCRIPTS)` does the walking, and
495+
* the glob form handed to `walk` would name a directory that does not exist.
496+
* The self-test derives BOTH directions from SCRIPTS rather than re-spelling
497+
* the root, so moving or renaming the scanned directory cannot leave the
498+
* declaration describing the old one. The literal has to be written out here —
499+
* assembling it at runtime would put it out of reach of the very extractor it
500+
* exists for, which is a silent way to keep the defect while looking fixed.
501+
*/
502+
const ROOT_DIR_WATCH_HINTS = ['scripts/**'];
503+
450504
/** Every exporting file, with the statements that would run on import. */
451505
function importSafetyCensus(files) {
452506
const rows = [];
@@ -674,6 +728,45 @@ export function selfTest() {
674728
t('else continues the statement before it', topLevelStatements(codeOnly('if (a) { x(); } else { y(); }\n')).length === 1);
675729
t('catch continues the statement before it', topLevelStatements(codeOnly('try { x(); } catch (e) { y(); }\n')).length === 1);
676730

731+
// ── the dispatch-gates scan surface (#10784) ─────────────────────────────
732+
//
733+
// Enforcement cannot hold any of these: ROOT_DIR_WATCH_HINTS is read by
734+
// another tool entirely, so a wrong or stale one runs green here forever and
735+
// pays itself out as a dev dispatched on a new-script card with this gate
736+
// missing from the brief — which is the round that was actually paid. Both
737+
// directions are derived from the walked root rather than re-spelled, so
738+
// moving or renaming it cannot leave the declaration describing the old one.
739+
const walkedRoot = relative(REPO_ROOT, SCRIPTS);
740+
const declaredRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, ''));
741+
t(
742+
'the scan surface is declared for the root this gate actually walks',
743+
ROOT_DIR_WATCH_HINTS.includes(`${walkedRoot}/**`),
744+
JSON.stringify({ walkedRoot, ROOT_DIR_WATCH_HINTS }),
745+
);
746+
t(
747+
'and it declares no root this gate does not walk (a declaration that can drift from the scan is worse than none — it replaces a silent gate with a lying one)',
748+
declaredRoots.every((r) => r === walkedRoot),
749+
JSON.stringify(declaredRoots),
750+
);
751+
// The load-bearing one for #10784: the roster and the surface must not be the
752+
// same claim. A file this gate would judge tomorrow is covered by the
753+
// declaration and, by construction, can never be in a list of the members it
754+
// already has — so a derivation reading only the roster answers "silent" for
755+
// the one input most likely to fail here.
756+
const unwritten = `${walkedRoot}/the-one-nobody-has-written-yet.mjs`;
757+
t(
758+
'the declared surface covers a file this gate has never seen…',
759+
declaredRoots.some((r) => unwritten.startsWith(`${r}/`)),
760+
unwritten,
761+
);
762+
t(
763+
'…which the baseline roster can never contain, which is why the two are separate declarations',
764+
!KNOWN_IMPORT_UNSAFE.has(unwritten) && [...KNOWN_IMPORT_UNSAFE].every((rel) => rel !== `${walkedRoot}/**`),
765+
);
766+
// Provenance, never a lookup key: the glob form appearing where the walk root
767+
// is read would send readdirSync at a directory that does not exist.
768+
t('the declared form is NOT the walk root itself', !ROOT_DIR_WATCH_HINTS.includes(walkedRoot));
769+
677770
const failed = cases.filter((c) => !c.ok);
678771
for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
679772
if (failed.length) {
@@ -682,7 +775,8 @@ export function selfTest() {
682775
}
683776
console.log(
684777
`✓ check-entry-guard self-test: ${cases.length} cases pass — all 11 measured spellings rejected, canonical form and masked prose/payloads accepted, ` +
685-
`and the import-safety rule recognised on both sides (dispatch/exit/argv-branch/try rejected; declarations, non-exporters and all three guard spellings accepted).`,
778+
`and the import-safety rule recognised on both sides (dispatch/exit/argv-branch/try rejected; declarations, non-exporters and all three guard spellings accepted) — ` +
779+
`plus the dispatch-gates scan surface, derived from the walked root and held apart from the baseline roster.`,
686780
);
687781
return 0;
688782
}

scripts/check-parse-guard.mjs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,66 @@ const SCRIPTS = HERE;
131131
/** The one module allowed to reach the TypeScript parser directly. */
132132
const PARSER_HOME = join(SCRIPTS, 'ts-parse.mjs');
133133

134+
/**
135+
* The SCAN SURFACE, written in the syntax `scripts/pm/dispatch-gates.mjs` can
136+
* read.
137+
*
138+
* ── The defect this repairs (#10784) ────────────────────────────────────────
139+
*
140+
* That derivation scans a gate's module body for path-ish string literals. The
141+
* only literal describing this gate's population was the bare single-segment
142+
* word `scripts` (from the `'scripts/'` prefix test in `packsScripts`, whose
143+
* trailing slash the extractor trims), and `hintCovers` refuses a
144+
* separator-less literal as too generic. So this gate scored, for EVERY card in
145+
* the tree:
146+
*
147+
* pnpm check:parse-guard [lint.yml] dead: 'scripts' — the tree HAS it; the
148+
* covering rule refuses the literal as too generic (no path separator)
149+
*
150+
* A gate in that state is named by no dispatch brief — including a brief for
151+
* the one edit most likely to break it. It is the sibling half of the same
152+
* blind spot #10784 records for `check-entry-guard`: both gates walk this
153+
* directory, and both were invisible to the derivation, by two different
154+
* routes — one declaring a population too generic to match, the other declaring
155+
* a roster of the files it already has. Anyone adding a script got neither.
156+
*
157+
* The row this discharges lived in that tool's `ESCAPABLE_LITERAL_LEDGER`,
158+
* which is SHRINK-ONLY and fails a discharged row as STALE by name. Declaring
159+
* the subtree here is the sanctioned remedy; deleting the row is the other half
160+
* of the same step, and both land together.
161+
*
162+
* ── Why the subtree spelling, and not a wider extractor ─────────────────────
163+
*
164+
* The refusal is measured, not incidental, and it is not this file's to relax:
165+
* `hintCovers`' docblock prices teaching the extractor to accept bare top-level
166+
* directory words at +139084 fabricated (gate, file) pairs, precisely because
167+
* `packages`, `apps` and `examples` are path COMPONENTS in dozens of gates that
168+
* never read those roots. A declared subtree is a different claim — an author
169+
* stating what this gate reads — and the glob collapse reduces it back to this
170+
* root and to nothing else.
171+
*
172+
* ── Why the OUTSIDE walk is deliberately NOT declared ───────────────────────
173+
*
174+
* `walkOutside(REPO_ROOT)` really does read the whole repo, but it CENSUSES;
175+
* it cannot fail this gate. The failing population is the scanned root alone,
176+
* which is what the green line claims and what `TIERS` exists to keep straight.
177+
* Declaring the repo root would name this gate for every card in the tree to
178+
* reach the one directory whose edits can turn it red — the "22 leads is the
179+
* same as none" failure the derivation's own header prices a fabricated lead
180+
* against. The refusal is pinned in the self-test rather than left in this
181+
* paragraph.
182+
*
183+
* ── Provenance, never a lookup key ──────────────────────────────────────────
184+
*
185+
* Nothing in this gate reads this array; `walk(SCRIPTS)` does the walking, and
186+
* the glob form handed to `walk` would name a directory that does not exist.
187+
* The self-test derives both directions from SCRIPTS rather than re-spelling
188+
* the root. The literal has to be written out — assembling it at runtime would
189+
* put it out of reach of the very extractor it exists for, which is a silent
190+
* way to keep the defect while looking fixed.
191+
*/
192+
const ROOT_DIR_WATCH_HINTS = ['scripts/**'];
193+
134194
/**
135195
* The three parser entry points, each with the checked call that replaces it.
136196
*
@@ -644,6 +704,34 @@ export function selfTest() {
644704
codeOnly('// gone\nconst a = 1;\n').split('\n').length === 3
645705
&& !codeOnly('// gone\nconst a = 1;\n').includes('gone'));
646706

707+
// -- the dispatch-gates scan surface (#10784) -----------------------------
708+
//
709+
// Enforcement cannot hold any of these: ROOT_DIR_WATCH_HINTS is read by
710+
// another tool entirely, so a wrong or stale one runs green here forever and
711+
// pays itself out as a dev dispatched on a scripts/ card with this gate
712+
// missing from the brief. Both directions are derived from the walked root
713+
// rather than re-spelled.
714+
const walkedRoot = relative(REPO_ROOT, SCRIPTS);
715+
const declaredRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, ''));
716+
t('the scan surface is declared for the root this gate actually walks',
717+
ROOT_DIR_WATCH_HINTS.includes(`${walkedRoot}/**`),
718+
JSON.stringify({ walkedRoot, ROOT_DIR_WATCH_HINTS }));
719+
t('and it declares no root this gate does not walk (a declaration that can drift from the scan is worse '
720+
+ 'than none -- it replaces a silent gate with a lying one)',
721+
declaredRoots.every((r) => r === walkedRoot),
722+
JSON.stringify(declaredRoots));
723+
t('the declared literal carries a path separator, which is the whole reason it is written this way -- a '
724+
+ 'bare root word is refused as too generic and reaches nothing',
725+
ROOT_DIR_WATCH_HINTS.every((h) => h.includes('/')));
726+
// The census side stays undeclared: walkOutside reads the whole repo but
727+
// cannot fail this gate, and naming the repo root would put this gate in
728+
// every card's brief to reach the one directory whose edits turn it red.
729+
t('the repo root is NOT declared -- the outside walk is a census, not the failing population',
730+
!declaredRoots.some((r) => r === '' || r === '.' || relative(REPO_ROOT, join(REPO_ROOT, r)) === ''));
731+
// Provenance, never a lookup key: the glob form appearing where the walk root
732+
// is read would send readdirSync at a directory that does not exist.
733+
t('the declared form is NOT the walk root itself', !ROOT_DIR_WATCH_HINTS.includes(walkedRoot));
734+
647735
const failed = cases.filter((c) => !c.ok);
648736
for (const c of failed) console.error(` x ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
649737
if (failed.length) {
@@ -654,7 +742,8 @@ export function selfTest() {
654742
`✓ check:parse-guard self-test: ${cases.length} cases pass (every spelling of all three parser entry `
655743
+ `points is caught, their checked replacements are not, prose and payloads are not, only ts-parse.mjs `
656744
+ `is exempt, and the out-of-tree census counts what this gate does not govern — TIERED by a read of `
657-
+ `the owning package.json, so no row is printed under a reason that is false of it).`,
745+
+ `the owning package.json, so no row is printed under a reason that is false of it) -- plus the `
746+
+ `dispatch-gates scan surface, derived from the walked root, with the census side held out of it.`,
658747
);
659748
return 0;
660749
}

0 commit comments

Comments
 (0)