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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
386 changes: 386 additions & 0 deletions packages/types/src/driver-error-classification.callers.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #13440 — every in-repo call of `isMissingTableError` must name the object it
* was reading. This file is the mechanism that makes that true; the JSDoc on
* the function is only the explanation.
*
* ── The defect class ─────────────────────────────────────────────────────────
*
* #13324 repaired the predicate by giving it `readObject`, so a driver fault
* naming a DIFFERENT relation can no longer be answered "this table is not
* provisioned yet". The parameter had to ship OPTIONAL: `@objectstack/types` is
* published (17.2.0, `exports` `.` and `./node`), and re-exported again from
* `@objectstack/metadata/errors`, so a required parameter is a breaking change
* to a published API — a major bump, which is a maintainer's call and not a
* side effect of a bug fix.
*
* Optional is right for the world outside this repo and wrong for the inside of
* it. `isMissingTableError(err)` still compiles, still type-checks, and still
* returns the pre-#13324 WIDE verdict — silently. On the authz path
* (`packages/core/src/security/resolve-authz-context.ts`) that verdict resolves
* a permission-store OUTAGE to `[]` permissions instead of failing loud, so the
* omission fails in the OPEN direction. That is the same declared-but-not-
* enforced shape #13324 existed to close, one level up: the obligation is
* stated in prose, and prose is exactly what #13324 proved insufficient.
*
* ── Why a gate and not a required parameter ──────────────────────────────────
*
* A gate binds only callers inside this repository, so it buys the enforcement
* without the major bump: external consumers keep the optional form the
* published API promises them. Making the parameter required remains available
* as a follow-up and stays a human decision.
*
* ── The exemption axis, and why it is exactly this narrow ────────────────────
*
* `driver-error-classification.test.ts` calls the one-argument form ~30 times
* ON PURPOSE: those are the tests OF the optional form, pinning that
* `isMissingTableError(err)` still behaves for the external consumers the
* optional parameter protects. A gate written to the naive rule would fail
* every one of them, and the obvious "fix" — passing a read object — would
* delete the coverage of the published one-argument contract.
*
* So the exemption is the DEFINING PACKAGE'S OWN CONTRACT TESTS and nothing
* else: `packages/types/src/driver-error-classification*.test.ts`. Everything
* else under `packages/` — production and test code alike — must pass the read
* object. The defining module itself is deliberately NOT exempt: the predicate
* delegates to `matchesDriverError` and never calls itself, so a
* self-referential one-argument call there would be a new fact worth failing on.
*
* ── Why the checks below are not just "green on the current tree" ────────────
*
* A scanner that silently stops matching yields the same empty violation set as
* a clean repo, and the assertion cannot tell them apart. Two positive controls
* separate them, and they fail in different directions:
*
* SEES THE EXEMPT FILE — with the exemption disabled, the defining test file
* must yield a substantial one-argument population
* (30 on the commit this landed). Zero there means the
* call matcher is broken, not that the repo is clean.
* REACHES OTHER PACKAGES — the two-argument production population outside
* `packages/types` must be substantial (18 on the same
* commit). Zero there means the directory walk never
* left the defining package, which is the failure that
* would make the whole gate vacuous.
*
* A third check guards the matcher's one structural blind spot. Callees are
* matched BY NAME, so a renamed import binding
* (`import { isMissingTableError as x }`) would be invisible. None exists today;
* if one appears, this fails and asks for the matcher to be taught about it,
* rather than letting the population quietly shrink.
*
* ── Boundary, stated rather than discovered later ────────────────────────────
*
* The scanned surface is `packages/` — the surface the ruling on #13440 names.
* Measured when this landed, `apps/`, `examples/`, `e2e/` and `scripts/` call
* the predicate zero times in total, so the narrower surface loses nothing
* today; widening it is the `SCANNED_TREE` constant below plus a wider glob in
* `CROSS_PACKAGE_TEST_INPUTS` (and, for a NEW top-level root, a matching entry
* in ci.yml's `crosspkg:` filter — `check-ci-filter-parity.mjs` is the gate
* that says so).
*
* ⚠️ The EXTENSION boundary is `.ts` alone, and unlike the tree above that one
* is not free — it is a deliberate trade with a second gate. Measured under
* `packages/` on f60061a460, which is a NAMED commit rather than "now" on
* purpose: only the three zeros are load-bearing, and the `.ts` total moves
* with every merge, so a reading with no commit on it rots silently.
*
* .ts 5193 tracked, 48 mention the predicate <- the scanned set
* .tsx 8 tracked, 0 mention the predicate <- excluded
* .mts 17 tracked, 0 mention the predicate <- excluded
* .cts 0 tracked, 0 mention the predicate <- excluded
*
* So nothing is lost today. What forbids simply widening it is that this
* package's declared radius is INHERITED as watch hints by
* `check:cross-package-test-inputs`, and the dispatch-gates self-test pins that
* no hint of that family reaches the `realtime-hooks.test.tsx` file in
* `packages/client-react` — the live specimen for "a test class the hint route
* cannot reach". A glob here that covers `.tsx` makes that case fail. It is a
* real red and not a nuisance: the specimen is how that tool proves its residue
* classes are not empty.
*
* (That file is named in two halves rather than as one quoted path on purpose.
* This gate's own registrar collects quoted whole paths out of COMMENTS, so
* spelling it here would put it on this package's roster and demand the very
* `.tsx` glob the paragraph exists to forbid — measured, it fails exactly that
* way.)
*
* ⇒ If a `.tsx` (or `.mts`) caller of this predicate ever appears, widening
* `SOURCE_FILE` below is only HALF the change: the declared glob must widen
* with it, and re-pointing that self-test specimen is a `scripts/pm/` edit
* owned by another lane. Do not widen the scanner alone — that reads as
* coverage while turbo never re-runs this test for the files it now claims to
* judge, which is the #7802 shape the declaration table exists to prevent. The
* two pins below keep this paragraph honest rather than decorative.
*/

import { describe, expect, it } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import ts from 'typescript';

/**
* This package is CJS-typed (no `"type": "module"`), so `module: NodeNext`
* forbids `import.meta` here — the same constraint `node-isolation.test.ts`
* records. Walk up from the CWD to this package's own manifest instead, which
* works wherever vitest is invoked from.
*/
function findUp(marker: (dir: string) => boolean, what: string): string {
let dir = process.cwd();
for (;;) {
if (marker(dir)) return dir;
const parent = dirname(dir);
if (parent === dir) throw new Error(`could not locate ${what} walking up from ${process.cwd()}`);
dir = parent;
}
}

const PACKAGE_ROOT = findUp((dir) => {
const manifest = join(dir, 'package.json');
if (!existsSync(manifest)) return false;
const { name } = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: string };
return name === '@objectstack/types';
}, 'the @objectstack/types package root');

/**
* The repo root reached by ARITHMETIC from this package rather than by a second
* marker-file walk, and that is deliberate. A walk keyed on a workspace-root
* marker would NAME that root file, which
* `check-cross-package-test-inputs.mjs` then requires this package to declare —
* and a declared root-level path is a top-level root that
* `check-ci-filter-parity.mjs` in turn requires in ci.yml's `crosspkg:` filter.
* (Both gates named bare rather than by path on purpose: the first one's
* literal collector takes quoted whole paths out of COMMENTS too, so spelling
* one here would force this package to declare a radius it never reads.)
* Anchoring off the manifest keeps this gate's whole declared radius inside
* `packages/**`, which ci.yml's `core:` filter already covers, so the gate costs
* one table entry and one turbo task and no scheduler surgery.
*
* The arithmetic is not trusted on faith: the anchor test below requires the
* walk to find this package's OWN defining module, which no wrong root can
* satisfy.
*/
const REPO_ROOT = resolve(PACKAGE_ROOT, '../..');

/** The tree this gate binds. See the boundary note in the header. */
const SCANNED_TREE = join(REPO_ROOT, 'packages');

/** Build output and vendored code are not in-repo call sites. */
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.turbo', '.next']);

/**
* The scanned extension set, spelled ONCE so the pins below can assert it and
* so it stays in exact correspondence with this package's declared glob in
* `CROSS_PACKAGE_TEST_INPUTS` (`packages/**\/*.ts`). Read the extension
* boundary in the header before changing either — they widen together or not
* at all.
*/
const SOURCE_FILE = (name: string): boolean => name.endsWith('.ts') && !name.endsWith('.d.ts');

/**
* The defining package's own contract tests — the tests OF the optional form.
* The glob is deliberately anchored to the whole repo-relative path: a
* same-named file in another package is not a contract test of this predicate.
*/
const EXEMPT = /^packages\/types\/src\/driver-error-classification[^/]*\.test\.ts$/;

const PREDICATE = 'isMissingTableError';

interface CallSite {
readonly path: string;
readonly line: number;
readonly column: number;
readonly text: string;
readonly argumentCount: number;
/** A second argument written as `undefined` / `null` / `void 0`. */
readonly readObjectDiscarded: boolean;
}

interface RenamedImport {
readonly path: string;
readonly line: number;
readonly local: string;
}

function sourceFilesUnder(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
continue;
}
if (!entry.isFile()) continue;
if (!SOURCE_FILE(entry.name)) continue;
out.push(join(dir, entry.name));
}
};
walk(root);
return out;
}

/** `undefined`, `null` and `void 0` all mean "cannot say" to the predicate. */
function discardsReadObject(argument: ts.Expression): boolean {
if (ts.isIdentifier(argument) && argument.text === 'undefined') return true;
if (argument.kind === ts.SyntaxKind.NullKeyword) return true;
return ts.isVoidExpression(argument);
}

function analyse(files: readonly string[]): { calls: CallSite[]; renamedImports: RenamedImport[] } {
const calls: CallSite[] = [];
const renamedImports: RenamedImport[] = [];

for (const file of files) {
const text = readFileSync(file, 'utf8');
if (!text.includes(PREDICATE)) continue;
const path = relative(REPO_ROOT, file).split(sep).join('/');
const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (name === PREDICATE) {
const start = node.getStart(sourceFile);
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
const second = node.arguments[1];
calls.push({
path,
line: line + 1,
column: character + 1,
text: node.getText(sourceFile).replace(/\s+/g, ' '),
argumentCount: node.arguments.length,
readObjectDiscarded: second !== undefined && discardsReadObject(second),
});
}
}
// A renamed binding would make the by-name match above blind.
if (ts.isImportSpecifier(node) && node.propertyName?.text === PREDICATE) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
renamedImports.push({ path, line: line + 1, local: node.name.text });
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}

return { calls, renamedImports };
}

const FILES = sourceFilesUnder(SCANNED_TREE);
const { calls: ALL_CALLS, renamedImports: RENAMED_IMPORTS } = analyse(FILES);

const offends = (call: CallSite): boolean => call.argumentCount < 2 || call.readObjectDiscarded;

const REMEDY =
'Pass the object you were reading as the second argument — ' +
"`isMissingTableError(err, object)`. Without it the predicate returns the pre-#13324 WIDE verdict: " +
'a fault naming some OTHER relation is answered "this table is not provisioned yet", ' +
'which on a read path means an outage is silently reported as "no rows".';

function render(sites: readonly CallSite[]): string {
return sites
.map((c) => ` ${c.path}:${c.line}:${c.column} ${c.text}`)
.join('\n');
}

describe('isMissingTableError — every in-repo call names the object it read (#13440)', () => {
it('the scan is anchored to the real workspace root', () => {
expect(existsSync(SCANNED_TREE)).toBe(true);
expect(relative(REPO_ROOT, PACKAGE_ROOT).split(sep).join('/')).toBe('packages/types');
// Self-referential: a mis-anchored walk cannot reach the module under test.
const scanned = new Set(FILES.map((f) => relative(REPO_ROOT, f).split(sep).join('/')));
expect(scanned.has('packages/types/src/driver-error-classification.ts')).toBe(true);
});

// ── POSITIVE CONTROL ──────────────────────────────────────────────────────
// An empty violation set is the passing state, and a broken scanner produces
// the identical empty set. These two say the scanner is looking.

it('POSITIVE CONTROL: sees the exempt contract tests (~30 one-argument calls)', () => {
const inExemptFiles = ALL_CALLS.filter((c) => EXEMPT.test(c.path) && c.argumentCount < 2);
expect(
inExemptFiles.length,
'the defining contract tests exercise the one-argument published form ~30 times ' +
'(30 when this landed); finding none means the call matcher stopped matching, ' +
'not that the repo is clean',
).toBeGreaterThanOrEqual(20);
});

it('POSITIVE CONTROL: the walk reaches packages other than the defining one', () => {
const elsewhere = ALL_CALLS.filter(
(c) => !c.path.startsWith('packages/types/') && c.argumentCount >= 2,
);
const packages = new Set(elsewhere.map((c) => c.path.split('/').slice(0, 2).join('/')));
expect(
elsewhere.length,
'production call sites outside packages/types pass the read object (18 when this ' +
'landed); finding none means the directory walk never left the defining package, ' +
'which would make this gate vacuous',
).toBeGreaterThanOrEqual(15);
expect(packages.size).toBeGreaterThanOrEqual(3);
});

// ── THE EXTENSION BOUNDARY ────────────────────────────────────────────────
// The header explains why this gate reads `.ts` and nothing else. These two
// keep that paragraph from becoming decoration.

it('the scanned extension set is exactly `.ts`, matching the declared glob', () => {
// Pure predicate assertions — no I/O, so this adds nothing to the radius
// this package must declare. Widening any line here without widening
// `packages/**\/*.ts` in CROSS_PACKAGE_TEST_INPUTS is the #7802 shape:
// the scan would judge files turbo never re-runs it for.
expect(SOURCE_FILE('engine.ts')).toBe(true);
expect(SOURCE_FILE('engine.d.ts')).toBe(false);
expect(SOURCE_FILE('realtime-hooks.test.tsx')).toBe(false);
expect(SOURCE_FILE('thing.mts')).toBe(false);
expect(SOURCE_FILE('thing.cts')).toBe(false);
expect(FILES.every((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'))).toBe(true);
});

it('POSITIVE CONTROL: `.tsx` files really do exist under packages/, so excluding them is a decision', () => {
// Filename-only: this counts directory ENTRIES and never opens a `.tsx`
// file, so the exclusion cannot smuggle in a content dependence on files
// outside the declared glob. A floor, not a pin — adding `.tsx` files can
// never redden it, and finding zero would mean the header's measurement
// (8 when this landed) had quietly become a statement about nothing.
const countTsx = (dir: string): number => {
let n = 0;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) n += countTsx(join(dir, entry.name));
} else if (entry.isFile() && entry.name.endsWith('.tsx')) n += 1;
}
return n;
};
expect(countTsx(SCANNED_TREE)).toBeGreaterThanOrEqual(1);
});

it('no renamed import hides a call from the by-name matcher', () => {
expect(
RENAMED_IMPORTS,
`${PREDICATE} is matched by callee NAME, so a renamed binding would be invisible to ` +
'this gate. One now exists — teach the matcher the local name before this can pass:\n' +
RENAMED_IMPORTS.map((r) => ` ${r.path}:${r.line} as ${r.local}`).join('\n'),
).toEqual([]);
});

// ── THE GATE ──────────────────────────────────────────────────────────────

it('no in-repo call omits or discards the read object', () => {
const violations = ALL_CALLS.filter((c) => !EXEMPT.test(c.path) && offends(c));
expect(
violations,
`${violations.length} call site(s) of ${PREDICATE}() do not name the object being read:\n` +
`${render(violations)}\n\n${REMEDY}\n\n` +
'The only exemption is the defining package\'s own contract tests ' +
'(packages/types/src/driver-error-classification*.test.ts), which pin the published ' +
'one-argument form on purpose. If your call genuinely has no read object by ' +
'construction, that is a decision for the card, not a widening of this gate.',
).toEqual([]);
});
});
Loading
Loading