2314 lines
103 KiB
TypeScript
2314 lines
103 KiB
TypeScript
/**
|
||
* Hierarchy writer-coverage assertion — contract 1
|
||
* (docs/requirements/hierarchy-schema.md) §6.3(b).
|
||
*
|
||
* A static CI assertion over all production sources (apps/, packages/,
|
||
* plugins/) with three prongs, each bound to a closed, explicitly enumerated
|
||
* allowlist:
|
||
*
|
||
* (i) Symbol prong — write references (insert/update/delete) to the
|
||
* class-table schema symbols occur only in allowlisted modules.
|
||
* Schema symbols are tracked through named imports (aliased or not),
|
||
* namespace imports, and re-export conduits. Conduits carry an
|
||
* EXPORT MAP (per-module named exports and namespace exports),
|
||
* computed to a fixpoint, so a rename at the export site
|
||
* (`export { companies as c } from`), an export of a locally bound
|
||
* alias (`import { companies }; export { companies as co }`), and a
|
||
* binding derived from a namespace (`export const co = ns.companies`)
|
||
* all propagate symbol identity to the consumer. Declaration exports
|
||
* tolerate a type annotation and prior declarators
|
||
* (`export const co: typeof companies = companies`,
|
||
* `export const dummy = 0, co = companies`). The DEFAULT-export
|
||
* slot is an export name like any other (`export { companies as
|
||
* default } from`, `export default companies`), so a bare default
|
||
* import of a conduit resolves against the map too; namespace-member
|
||
* write targets match the source's EXPORTED names — renames included
|
||
* (`M.co` after `export { companies as co } from`) — not just the
|
||
* original class symbols. Destructuring a NON-class property from a
|
||
* schema namespace over-approximates: the binding is treated as a
|
||
* schema namespace itself (fail-closed), and single-alias derivation
|
||
* from a namespace (`const t = ns.companies`) is folded in to a
|
||
* fixed depth.
|
||
* (ii) Literal prong — a class-table name inside a string or template
|
||
* span that also carries SQL context fails outside the allowlist.
|
||
* Spans are produced by a real lexer, so comments cannot hide code
|
||
* and strings cannot hide comments. Schema definitions and generated
|
||
* migrations are excluded from this prong only (per contract).
|
||
* (iii) Raw-execution prong — content-independent. A file is RAW-CAPABLE
|
||
* when it imports a database driver or the createDb/createPgliteDb
|
||
* factories — directly, via namespace, by destructuring a factory
|
||
* symbol out of a schema namespace, or from a capability CONDUIT.
|
||
* Conduits carry their own exported-name maps computed to a
|
||
* fixpoint, so a factory renamed at any hop (`export { createDb as
|
||
* mk } from`, `export default createDb`) still marks every module
|
||
* down the chain, and a tracked dynamic import of a conduit is an
|
||
* import edge like a static one. In a raw-capable file
|
||
* outside the allowlist and register, EVERY `.execute(`, `.query(`,
|
||
* and `.unsafe(` call fails, on any receiver, with any argument —
|
||
* there is no tagged-template exemption (§6.3(b): "regardless of what
|
||
* the SQL string contains or how it is constructed"). In files
|
||
* without detected capability (the DI residual), a conventional
|
||
* receiver backstop still fires on db/client-shaped receivers.
|
||
* `sql.raw` is tracked through import aliasing and namespaces.
|
||
*
|
||
* Runtime code-construction primitives (eval, new Function — spelled
|
||
* directly or as a literal computed member like `globalThis['eval']`) fail
|
||
* anywhere — allowlist and register included: constructed code defeats every
|
||
* static prong, so there is no enumerated disposition path for it.
|
||
* Unanalyzable import routes fail everywhere except their own enumerated
|
||
* disposition registers below: a dynamic import whose specifier is not a
|
||
* single string literal, and createRequire (tracked through import aliasing
|
||
* — `createRequire as x` — and destructuring from the module namespace,
|
||
* whether that namespace came from a static `import * as` or a tracked
|
||
* dynamic import; a
|
||
* route that hides the NAME entirely, e.g. re-exporting createRequire from a
|
||
* helper, falls under the value-flow residual). A LITERAL dynamic import is
|
||
* an import edge like any static import (contract rev 9): its bindings
|
||
* (destructured or namespace) feed prong (i) exactly as static import
|
||
* bindings do — and to keep that claim sound, a literal dynamic import of a
|
||
* schema/factory/driver source OUTSIDE the tracked
|
||
* `const X = await import('…')` binding shape (`.then` chains, deferred
|
||
* awaits, Promise.all) is itself a violation: bindings the analyzer cannot
|
||
* track are not allowed to exist for capability-bearing modules.
|
||
*
|
||
* KNOWN RESIDUALS (deliberate, reviewed trade-offs — not claims of closure):
|
||
* - In a file with NO detected raw capability (no driver/factory import on
|
||
* any tracked route), prong (iii) falls back to a conventional-receiver
|
||
* backstop (db/client/pool/tx/…). A dependency-injected handle bound to
|
||
* an unconventional name in such a file is an OPEN evasion path. The
|
||
* counterfactual was measured: any-receiver flagging in capability-free
|
||
* files false-positives on real non-SQL .execute()/.query() sites
|
||
* (runtime-provider-registry, connector-lease, the log CLI), so the
|
||
* residual is accepted and reviews of DI provider modules carry it.
|
||
* - Computed member access with a NON-literal name (obj[verb]()) is not
|
||
* statically resolvable; literal computed access (obj['insert']()) is
|
||
* flagged, and the statically-resolvable DISGUISES of a literal key fail
|
||
* closed — access or call, in any position: a text-only template-literal
|
||
* key (obj[`insert`](), ns[`companies`] — template text never reaches
|
||
* the lexer's code output), a quoted OR template key carrying expression
|
||
* dressing (`ns['companies' as const]`, `ns['companies'!]`,
|
||
* `ns[`companies` as const]` — the dressed rule's key class includes the
|
||
* backtick, so the lexed empty backtick pair matches), and a quoted key
|
||
* built from string escapes (`\u`/`\x`/octal). (A template key WITH
|
||
* interpolation is a non-literal computed member, above.) Invoking a
|
||
* write/exec verb via `.apply`/`.call`/`.bind` fails closed the same
|
||
* way, and ANY appearance of the `Reflect` identifier fails closed
|
||
* outright — verb detection through Reflect is not boundable (argument
|
||
* windows stop at newlines and nested parens, the method can be
|
||
* bracket-spelled, the object aliased), and the tree has zero
|
||
* occurrences outside test files. These
|
||
* code-shape rules match ordinary syntax over ordinary method names, so
|
||
* they carry their own enumerated disposition (CODE_SHAPE_REGISTER,
|
||
* empty today): a reviewed legitimate hit is registered, never resolved
|
||
* by weakening the shape. Constructing the member at runtime is adjacent
|
||
* to eval and is expected to be caught in review.
|
||
* - The DB_FACTORY_IMPORTERS enumeration counts the import edges
|
||
* hasSymbolImportEdge can see (named import, literal dynamic package
|
||
* import with symbol use, namespace member use). Destructuring a factory
|
||
* out of a schema namespace (`const { createDb } = dbns`) confers
|
||
* capability for prong (iii) but is NOT visible to the enumeration, so a
|
||
* file on that route joins the importer set only via review; prong (iii)
|
||
* still flags any execution in it. The enumeration is a measured set,
|
||
* not a soundness claim.
|
||
* - General VALUE FLOW is not modeled. The analyzer tracks import edges,
|
||
* re-export chains, and single-step alias/destructure derivations from a
|
||
* namespace binding (to depth 2) — not arbitrary assignment chains,
|
||
* function returns, or method extraction. Demonstrated escapes in this
|
||
* class: `const u = this.client.unsafe; u.call(this.client, s)` (the
|
||
* verb never appears as a member call), an alias chain three or more
|
||
* steps deep, a helper function that returns a schema symbol, and an
|
||
* export whose expression COMPUTES the value (a ternary, a call result)
|
||
* rather than naming a binding or member.
|
||
* Closing it requires data-flow analysis (a type-checker-backed rewrite,
|
||
* tracked for M4-1b consideration); the counterfactual — flagging every
|
||
* bare identifier call — false-positives on essentially all callback
|
||
* code. Reviews of modules touching db handles carry this residual.
|
||
* - The scan perimeter is the full <root>/<pkg> tree for the three roots
|
||
* (build output, tool caches, and dot-directories excluded), so
|
||
* production TS outside src/ — package configs, e2e helpers,
|
||
* packages/mosaic/framework/** — is scanned and conduit-visible
|
||
* (widened from src/-only in M4-1b-i; the widened set was measured free
|
||
* of every trigger token at the time). Files excluded from the scan —
|
||
* test files — are still invisible as import-graph CONDUITS: test files
|
||
* are emitted to dist, so a production module could launder a symbol or
|
||
* capability through a re-export in one. Importing a test module from
|
||
* production code is anomalous and review-visible; that blind spot is
|
||
* accepted as a residual, not closed.
|
||
*
|
||
* The writer allowlist names hierarchy command/repository modules ONLY. It is
|
||
* empty today: the hierarchy command family (M4-1b-ii) has not landed, so no
|
||
* production module may write the class tables. The infrastructure register
|
||
* holds legitimate non-hierarchy raw execution; registered modules are exempt
|
||
* from prong (iii) only — prongs (i) and (ii) apply to them with no
|
||
* exemption, and no registered module may appear on the writer allowlist.
|
||
*
|
||
* Register modules whose exports let a CALLER reach SQL execution carry their
|
||
* own closed importer enumerations (the laundering path §6.3(b) closes):
|
||
* the migration runner, migrate-tier, and the createDb/createPgliteDb
|
||
* factories. The remaining registered modules execute only fixed statements
|
||
* or collection-CRUD over the storage `(id, data)` shape — which cannot
|
||
* address class-table columns — and export no caller-supplied-SQL surface;
|
||
* that composition property is review-enforced (§5.1) on any change to them.
|
||
*
|
||
* A false positive is resolved in the same PR by adding the module to the one
|
||
* enumerated list its role permits — never by weakening the assertion.
|
||
*
|
||
* Test files (*.spec.*, *.test.*, __tests__/) are not scanned: they are not
|
||
* production mutation paths, and the contract's own §6 witnesses must write
|
||
* class tables directly to witness database constraints. The known evasion
|
||
* forms from the M4-1a review are kept below as permanent controls: the
|
||
* analyzer must flag every one of them, so a regression that reopens an
|
||
* escape fails this suite.
|
||
*/
|
||
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
|
||
import { dirname, join, relative, resolve, sep } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { describe, expect, it } from 'vitest';
|
||
|
||
const REPO_ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)), '..', '..', '..');
|
||
|
||
/** Drizzle schema symbols of the five class tables (packages/db/src/schema.ts). */
|
||
const CLASS_SYMBOLS = ['companies', 'estates', 'platformProjects', 'workspaces', 'hierarchyGrants'];
|
||
|
||
/** SQL table names of the five class tables. */
|
||
const CLASS_TABLES = [
|
||
'companies',
|
||
'estates',
|
||
'platform_projects',
|
||
'workspaces',
|
||
'hierarchy_grants',
|
||
];
|
||
|
||
/**
|
||
* Writer allowlist (§6.3b): hierarchy command/repository modules only.
|
||
* EMPTY until the hierarchy command family lands (M4-1b-ii; M4-1b-i ships
|
||
* only the audit/outbox machinery, which writes no class table). Adding a module
|
||
* here is a contract-conformance decision reviewed under §5.1 — the module
|
||
* must be part of the Gateway hierarchy command path, and it must not export
|
||
* a function that executes caller-supplied SQL.
|
||
*/
|
||
const WRITER_ALLOWLIST: string[] = [];
|
||
|
||
/**
|
||
* Infrastructure register: closed enumeration of legitimate non-hierarchy raw
|
||
* execution. Exempt from prong (iii) ONLY; prongs (i)/(ii) still apply, and
|
||
* none of these may ever join the writer allowlist.
|
||
*/
|
||
const INFRA_REGISTER: string[] = [
|
||
'packages/db/src/client.ts', // connection factory (imports postgres driver)
|
||
'packages/db/src/client-pglite.ts', // PGlite factory (imports the pglite driver)
|
||
'packages/db/src/migrate.ts', // migration runner (hash-ledger DDL execution)
|
||
'packages/db/src/backlog.ts', // backlog domain module: parameterized sql`` over backlog tables only
|
||
'packages/memory/src/insights.ts', // analytics raw query over memory tables
|
||
'packages/storage/src/tier-detection.ts', // driver import for tier probing (fixed statements)
|
||
'packages/storage/src/adapters/pglite.ts', // storage adapter (collection CRUD over (id, data))
|
||
'packages/storage/src/adapters/postgres.ts', // storage adapter (extension bootstrap)
|
||
'packages/storage/src/migrate-tier.ts', // storage tier migration
|
||
'packages/storage/src/cli.ts', // storage CLI health probe
|
||
'apps/gateway/src/admin/admin-health.controller.ts', // SELECT 1 health probe
|
||
];
|
||
|
||
/**
|
||
* Closed importer enumerations for registered modules whose exports execute
|
||
* SQL or hand out an executing handle. An import edge is a static value
|
||
* import naming the symbol (from the module path or the package barrel), or
|
||
* a literal dynamic import of the package in a file using the symbol.
|
||
* `import type` is erased and is not an edge; parameter injection (the
|
||
* gateway schema-check module) has no edge. Being enumerated confers nothing
|
||
* else: importers stay subject to every prong and gain no writer standing.
|
||
*/
|
||
const MIGRATION_RUNNER_SYMBOLS = ['runMigrations', 'runPgliteMigrations', 'getMigrationStatus'];
|
||
const MIGRATION_RUNNER_IMPORTERS: string[] = [
|
||
'apps/gateway/src/database/database.module.ts',
|
||
'packages/storage/src/adapters/postgres.ts',
|
||
'packages/mosaic/src/commands/fleet-backlog.ts',
|
||
'packages/mosaic/src/commands/gateway/verify.ts',
|
||
];
|
||
const MIGRATE_TIER_SYMBOLS = [
|
||
'runMigrateTier',
|
||
'checkTargetPreconditions',
|
||
'PostgresMigrationTarget',
|
||
'DrizzleMigrationSource',
|
||
];
|
||
const MIGRATE_TIER_IMPORTERS: string[] = [
|
||
'packages/storage/src/cli.ts', // storage CLI command surface
|
||
'packages/storage/src/index.ts', // package barrel re-export (public API)
|
||
];
|
||
/**
|
||
* Enumerated disposition for non-literal dynamic imports (§6.3b review F8):
|
||
* files here may use a computed import specifier; every other prong still
|
||
* applies to them in full. Each entry needs a justification.
|
||
*/
|
||
const DYNAMIC_IMPORT_REGISTER: string[] = [
|
||
'plugins/macp/src/index.ts', // loads the ACP runtime SDK from a configured sdkRoot; no db access
|
||
];
|
||
/**
|
||
* Enumerated disposition for createRequire (an unanalyzable CJS import
|
||
* route). Every entry uses it for package.json version reads or module
|
||
* resolution only (measured); every other prong still applies in full.
|
||
*/
|
||
const CREATE_REQUIRE_REGISTER: string[] = [
|
||
'packages/mosaic/src/cli.ts', // package.json version read
|
||
'packages/mosaic/src/commands/gateway/daemon.ts', // module path resolution
|
||
'packages/mosaic/src/commands/launch.ts', // package.json version read + resolution
|
||
'packages/mosaic/src/commands/lease-activation-probe.ts', // injectable module resolver default
|
||
'plugins/macp/src/index.ts', // OpenCode SDK resolution
|
||
];
|
||
/** Importers of the db-handle factories (measured set; a new importer must be reviewed in). */
|
||
const DB_FACTORY_SYMBOLS = ['createDb', 'createPgliteDb'];
|
||
const DB_FACTORY_IMPORTERS: string[] = [
|
||
'apps/gateway/src/database/database.module.ts',
|
||
'packages/brain/src/cli.ts',
|
||
'packages/log/src/cli.ts',
|
||
'packages/memory/src/adapters/pgvector.ts',
|
||
'packages/memory/src/cli.ts',
|
||
'packages/mosaic/src/commands/fleet-backlog.ts',
|
||
'packages/storage/src/adapters/postgres.ts',
|
||
'packages/storage/src/cli.ts',
|
||
'packages/storage/src/migrate-tier.ts',
|
||
];
|
||
/**
|
||
* Enumerated disposition for the fail-closed CODE-SHAPE rules (text-only
|
||
* template keys, dressed/escape-built quoted keys, verb
|
||
* `.apply`/`.call`/`.bind`, `Reflect.*` verb indirection). Those shapes use
|
||
* ordinary syntax over ordinary method names (`query`, `delete`), so a
|
||
* legitimate hit is possible — e.g. a non-SQL `.query.bind(this)` on a
|
||
* log-shaped service. Such a hit is registered here with a justification,
|
||
* reviewed under §5.1, and is never resolved by weakening the shape. Empty
|
||
* today: the production tree has zero occurrences of any of these shapes
|
||
* (calibrated by the full-tree test). Exemption covers the shape rules ONLY —
|
||
* every prong still applies in full. eval/new Function stays unconditional:
|
||
* constructed code has no disposition path.
|
||
*/
|
||
const CODE_SHAPE_REGISTER: string[] = [];
|
||
|
||
const SCAN_ROOTS = ['apps', 'packages', 'plugins'];
|
||
const EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
||
const DRIVER_SPECIFIERS = ['postgres', 'pg', '@electric-sql/pglite'];
|
||
|
||
function isTestPath(rel: string): boolean {
|
||
return (
|
||
/\.(spec|test)\.[cm]?tsx?$/.test(rel) ||
|
||
rel.split(sep).includes('__tests__') ||
|
||
rel.endsWith('.d.ts')
|
||
);
|
||
}
|
||
|
||
/** Directory names excluded from the walk: build output and tool caches only. */
|
||
const EXCLUDED_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', 'test-results']);
|
||
|
||
function collectSources(): string[] {
|
||
const files: string[] = [];
|
||
for (const root of SCAN_ROOTS) {
|
||
const rootDir = join(REPO_ROOT, root);
|
||
if (!existsSync(rootDir)) continue;
|
||
for (const pkg of readdirSync(rootDir)) {
|
||
const pkgDir = join(rootDir, pkg);
|
||
if (!statSync(pkgDir).isDirectory()) continue;
|
||
const walk = (dir: string): void => {
|
||
for (const entry of readdirSync(dir)) {
|
||
const full = join(dir, entry);
|
||
const st = statSync(full);
|
||
if (st.isDirectory()) {
|
||
if (EXCLUDED_DIRS.has(entry) || entry.startsWith('.')) continue;
|
||
walk(full);
|
||
} else if (EXTENSIONS.has(full.slice(full.lastIndexOf('.')))) {
|
||
const rel = relative(REPO_ROOT, full).split(sep).join('/');
|
||
if (!isTestPath(rel)) files.push(rel);
|
||
}
|
||
}
|
||
};
|
||
walk(pkgDir);
|
||
}
|
||
}
|
||
return files.sort();
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Lexer: single pass producing (a) comment-free source with strings intact
|
||
// (for import/export and call-site regexes) and (b) the string/template text
|
||
// spans (for the literal prong). String-aware, so `//` inside a string is not
|
||
// a comment and a quote inside a comment does not open a string. Template
|
||
// expressions re-enter code mode (nesting supported); regex literals are
|
||
// recognized with the standard prev-token heuristic so their contents cannot
|
||
// open a phantom string.
|
||
// ---------------------------------------------------------------------------
|
||
interface Lexed {
|
||
code: string;
|
||
spans: string[];
|
||
}
|
||
|
||
function lexSource(src: string): Lexed {
|
||
let code = '';
|
||
const spans: string[] = [];
|
||
let i = 0;
|
||
const n = src.length;
|
||
// Template nesting: each entry is the accumulated text of one template.
|
||
const tplStack: string[] = [];
|
||
// Brace depth inside the current ${ } expression, one entry per nesting level.
|
||
const exprDepth: number[] = [];
|
||
let lastSig = ''; // last significant code char (regex-vs-division heuristic)
|
||
let lastWord = ''; // last identifier/keyword emitted to code
|
||
|
||
const emit = (ch: string): void => {
|
||
code += ch;
|
||
if (!/\s/.test(ch)) {
|
||
lastSig = ch;
|
||
if (/[A-Za-z0-9_$]/.test(ch)) lastWord += ch;
|
||
else lastWord = '';
|
||
}
|
||
};
|
||
|
||
const regexCanStart = (): boolean => {
|
||
if (lastSig === '' || '([{,;=:!&|?+-*%^<>~'.includes(lastSig)) return true;
|
||
return ['return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'do', 'else'].includes(
|
||
lastWord,
|
||
);
|
||
};
|
||
|
||
while (i < n) {
|
||
const ch = src[i]!;
|
||
const next = i + 1 < n ? src[i + 1]! : '';
|
||
|
||
if (tplStack.length > 0 && exprDepth.length < tplStack.length) {
|
||
// Inside template literal text.
|
||
const top = tplStack.length - 1;
|
||
if (ch === '\\') {
|
||
tplStack[top] += src.slice(i, i + 2);
|
||
i += 2;
|
||
continue;
|
||
}
|
||
if (ch === '`') {
|
||
spans.push(tplStack.pop()!);
|
||
emit('`');
|
||
i += 1;
|
||
continue;
|
||
}
|
||
if (ch === '$' && next === '{') {
|
||
exprDepth.push(0);
|
||
emit('$');
|
||
emit('{');
|
||
i += 2;
|
||
continue;
|
||
}
|
||
tplStack[top] += ch;
|
||
i += 1;
|
||
continue;
|
||
}
|
||
|
||
// Code mode (possibly inside a ${ } expression).
|
||
if (ch === '/' && next === '/') {
|
||
while (i < n && src[i] !== '\n') i += 1;
|
||
continue;
|
||
}
|
||
if (ch === '/' && next === '*') {
|
||
i += 2;
|
||
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) i += 1;
|
||
i += 2;
|
||
emit(' ');
|
||
continue;
|
||
}
|
||
if (ch === "'" || ch === '"') {
|
||
let span = '';
|
||
i += 1;
|
||
while (i < n && src[i] !== ch) {
|
||
if (src[i] === '\\') {
|
||
span += src.slice(i, i + 2);
|
||
i += 2;
|
||
} else {
|
||
span += src[i];
|
||
i += 1;
|
||
}
|
||
}
|
||
i += 1;
|
||
spans.push(span);
|
||
// Keep quoted strings in code output so import specifiers stay parseable.
|
||
emit(ch);
|
||
code += span;
|
||
emit(ch);
|
||
continue;
|
||
}
|
||
if (ch === '`') {
|
||
tplStack.push('');
|
||
emit('`');
|
||
i += 1;
|
||
continue;
|
||
}
|
||
if (ch === '/' && regexCanStart()) {
|
||
// Regex literal: consume without interpreting quotes/backticks inside.
|
||
i += 1;
|
||
let inClass = false;
|
||
while (i < n) {
|
||
const rc = src[i]!;
|
||
if (rc === '\\') {
|
||
i += 2;
|
||
continue;
|
||
}
|
||
if (rc === '[') inClass = true;
|
||
else if (rc === ']') inClass = false;
|
||
else if (rc === '/' && !inClass) break;
|
||
else if (rc === '\n') break; // not a regex after all; bail safely
|
||
i += 1;
|
||
}
|
||
i += 1;
|
||
while (i < n && /[a-z]/.test(src[i]!)) i += 1; // flags
|
||
emit('/');
|
||
continue;
|
||
}
|
||
if (exprDepth.length > 0) {
|
||
const top = exprDepth.length - 1;
|
||
if (ch === '{') exprDepth[top] = exprDepth[top]! + 1;
|
||
if (ch === '}') {
|
||
if (exprDepth[top] === 0) {
|
||
exprDepth.pop();
|
||
emit('}');
|
||
i += 1;
|
||
continue;
|
||
}
|
||
exprDepth[top] = exprDepth[top]! - 1;
|
||
}
|
||
}
|
||
emit(ch);
|
||
i += 1;
|
||
}
|
||
while (tplStack.length > 0) spans.push(tplStack.pop()!);
|
||
return { code, spans };
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Import graph helpers
|
||
// ---------------------------------------------------------------------------
|
||
function resolveSpecifier(fromRel: string, spec: string, fileSet: Set<string>): string | null {
|
||
if (!spec.startsWith('.')) return null;
|
||
const base = join(dirname(fromRel), spec).split(sep).join('/');
|
||
const noJs = base.replace(/\.(js|mjs|cjs)$/, '');
|
||
for (const cand of [
|
||
base,
|
||
noJs,
|
||
`${noJs}.ts`,
|
||
`${noJs}.tsx`,
|
||
`${noJs}.mts`,
|
||
`${noJs}.cts`,
|
||
`${noJs}/index.ts`,
|
||
]) {
|
||
if (fileSet.has(cand)) return cand;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const IMPORT_RE =
|
||
/import\s*(type\s+)?(?:(\w+)\s*,\s*)?(?:\{([^}]*)\}|\*\s*as\s+(\w+)|(\w+))?\s*from\s*['"]([^'"]+)['"]/g;
|
||
const EXPORT_FROM_RE =
|
||
/export\s*(type\s+)?(?:\{([^}]*)\}|\*(?:\s*as\s+\w+)?)\s*from\s*['"]([^'"]+)['"]/g;
|
||
|
||
// Member-access shapes, tolerant of optional chaining and non-null
|
||
// assertions (ns?.companies, ns!.companies, ns?.['companies']) — a `?.` or
|
||
// `!` between receiver and member must not break a match.
|
||
const DOT = `\\s*(?:\\?\\.|!\\s*\\.|\\.)\\s*`;
|
||
const BRACKET_OPEN = `\\s*(?:\\?\\.|!)?\\s*\\[`;
|
||
const MEMBER_SEG = `(?:${DOT}\\w+|${BRACKET_OPEN}\\s*['"\`]\\w+['"\`]\\s*\\])`;
|
||
/** Terminal member access matching one of `alt`'s names, dot or bracket form. */
|
||
const memberTail = (alt: string): string =>
|
||
`(?:${DOT}(?:${alt})\\b|${BRACKET_OPEN}\\s*['"\`](?:${alt})['"\`]\\s*\\])`;
|
||
/**
|
||
* Reduce an exported expression to its core: strip parenthesization,
|
||
* trailing type assertions (`as …`, `satisfies …`), and trailing non-null
|
||
* assertions (`(companies)`, `companies as unknown as object`,
|
||
* `companies satisfies object`, `companies!`, `(companies as any)!`) so
|
||
* `export default <expr>` passes see the binding under the dressing.
|
||
*/
|
||
function stripExprDressing(raw: string): string {
|
||
let expr = raw.trim();
|
||
for (let prev = ''; prev !== expr; ) {
|
||
prev = expr;
|
||
expr = expr
|
||
.replace(/^\(\s*/, '')
|
||
.replace(/\s*\)$/, '')
|
||
.replace(/\s+as\s+[^()]+$/, '')
|
||
.replace(/\s+satisfies\s+[^()]+$/, '')
|
||
.replace(/\s*!+$/, '')
|
||
.trim();
|
||
}
|
||
return expr;
|
||
}
|
||
|
||
/** Call-open shape tolerant of the optional-call form: `f(…)` or `f?.(…)`. */
|
||
const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`;
|
||
|
||
/**
|
||
* Declaration-head shapes: a declarator's initializer may sit behind a type
|
||
* annotation (`export const co: typeof companies = companies`) or behind
|
||
* prior declarators (`export const dummy = 0, co = companies`). TYPE_ANN
|
||
* admits `=>` inside the type text but stops at a bare `=` (the
|
||
* initializer); DECL_LIST skips prior declarators whose initializers are
|
||
* comma-free. Both are approximations of the declarator grammar — exotic
|
||
* prior initializers (an array or call containing a comma) fall to the
|
||
* value-flow residual. Both are also SINGLE-LINE shapes: a multiline type
|
||
* annotation (prettier keeps one only past the print width) falls to the
|
||
* value-flow residual too.
|
||
*/
|
||
const TYPE_ANN = `(?:\\s*:\\s*(?:[^=;\\n]|=>)*?)?`;
|
||
const DECL_LIST = `(?:[\\w$]+${TYPE_ANN}\\s*=\\s*[^,;\\n]*,\\s*)*`;
|
||
|
||
interface FileFacts {
|
||
rel: string;
|
||
code: string;
|
||
spans: string[];
|
||
}
|
||
|
||
interface Violation {
|
||
file: string;
|
||
prong: string;
|
||
detail: string;
|
||
}
|
||
|
||
/**
|
||
* Schema exports of one module in the conduit graph: `named` are exported
|
||
* identifiers bound to a class-table symbol (under WHATEVER exported name —
|
||
* renames propagate); `ns` are exported identifiers that are themselves
|
||
* namespaces over a schema source (`export * as x from …`).
|
||
*/
|
||
interface SchemaExports {
|
||
named: Set<string>;
|
||
ns: Set<string>;
|
||
}
|
||
type SchemaConduits = Map<string, SchemaExports>;
|
||
|
||
/** Exported class-symbol names reachable through `spec` from `rel` (null = not a schema source). */
|
||
function schemaExportsOf(
|
||
rel: string,
|
||
spec: string,
|
||
conduits: SchemaConduits,
|
||
fileSet: Set<string>,
|
||
): SchemaExports | null {
|
||
if (spec === '@mosaicstack/db') return { named: new Set(CLASS_SYMBOLS), ns: new Set() };
|
||
const r = resolveSpecifier(rel, spec, fileSet);
|
||
return r !== null ? (conduits.get(r) ?? null) : null;
|
||
}
|
||
|
||
/**
|
||
* Compute the fixpoint map of "schema sources": module → the exported names
|
||
* under which a class-table symbol (or a schema namespace) is reachable from
|
||
* it. Seeds: the schema module and the db package barrel (the bare
|
||
* '@mosaicstack/db' specifier is handled in schemaExportsOf). Renames are
|
||
* propagated: `export { companies as c } from …` exports `c`, an
|
||
* `export { x as y }` of a local class binding exports `y`, and
|
||
* `export const y = ns.companies` over a schema namespace exports `y`.
|
||
*/
|
||
function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): SchemaConduits {
|
||
const conduits: SchemaConduits = new Map([
|
||
['packages/db/src/schema.ts', { named: new Set(CLASS_SYMBOLS), ns: new Set<string>() }],
|
||
['packages/db/src/index.ts', { named: new Set(CLASS_SYMBOLS), ns: new Set<string>() }],
|
||
]);
|
||
let changed = true;
|
||
while (changed) {
|
||
changed = false;
|
||
for (const f of files) {
|
||
const mine: SchemaExports = conduits.get(f.rel) ?? {
|
||
named: new Set<string>(),
|
||
ns: new Set<string>(),
|
||
};
|
||
const before = mine.named.size + mine.ns.size;
|
||
for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
|
||
if (m[1]) continue; // export type — erased
|
||
const src = schemaExportsOf(f.rel, m[3]!, conduits, fileSet);
|
||
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||
const starAs = /export\s*\*\s*as\s+(\w+)/.exec(m[0]);
|
||
if (starAs) {
|
||
mine.ns.add(starAs[1]!); // export * as x from schema source
|
||
} else if (m[2] === undefined) {
|
||
for (const n of src.named) mine.named.add(n); // export * from …
|
||
for (const n of src.ns) mine.ns.add(n);
|
||
} else {
|
||
for (const part of m[2].split(',')) {
|
||
const seg = part.trim();
|
||
if (!seg || seg.startsWith('type ')) continue;
|
||
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||
const original = asMatch ? asMatch[1]! : seg;
|
||
const exported = asMatch ? asMatch[2]! : seg;
|
||
if (src.named.has(original)) mine.named.add(exported);
|
||
if (src.ns.has(original)) mine.ns.add(exported);
|
||
}
|
||
}
|
||
}
|
||
// Exports of local bindings: `export { x as y }` where x is a local
|
||
// class alias (or namespace), and `export const y = ns.<classSym>`.
|
||
const aliases = classAliases(f, conduits, fileSet);
|
||
if (aliases.named.length > 0 || aliases.namespaces.length > 0) {
|
||
for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) {
|
||
for (const part of m[1]!.split(',')) {
|
||
const seg = part.trim();
|
||
if (!seg || seg.startsWith('type ')) continue;
|
||
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||
const local = asMatch ? asMatch[1]! : seg;
|
||
const exported = asMatch ? asMatch[2]! : seg;
|
||
if (aliases.named.includes(local)) mine.named.add(exported);
|
||
if (aliases.namespaces.includes(local)) mine.ns.add(exported);
|
||
}
|
||
}
|
||
for (const ns of aliases.namespaces) {
|
||
for (const m of f.code.matchAll(
|
||
new RegExp(
|
||
`export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`,
|
||
'g',
|
||
),
|
||
)) {
|
||
mine.named.add(m[1]!);
|
||
}
|
||
}
|
||
for (const local of aliases.named) {
|
||
for (const m of f.code.matchAll(
|
||
new RegExp(
|
||
`export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${local}\\b`,
|
||
'g',
|
||
),
|
||
)) {
|
||
mine.named.add(m[1]!);
|
||
}
|
||
}
|
||
// The default slot is an export name like any other, whatever the
|
||
// expression dressing: bare local, parenthesized, type-asserted
|
||
// (`as`/`satisfies`), non-null-asserted (`companies!`), or a
|
||
// namespace member chain in dot or bracket form, semicolon or not.
|
||
for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) {
|
||
const expr = stripExprDressing(m[1]!);
|
||
if (/^\w+$/.test(expr)) {
|
||
if (aliases.named.includes(expr)) mine.named.add('default');
|
||
if (aliases.namespaces.includes(expr)) mine.ns.add('default');
|
||
} else {
|
||
for (const ns of aliases.namespaces) {
|
||
if (
|
||
new RegExp(`^${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}$`).test(expr)
|
||
) {
|
||
mine.named.add('default');
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (mine.named.size + mine.ns.size > before) {
|
||
conduits.set(f.rel, mine);
|
||
changed = true;
|
||
}
|
||
}
|
||
}
|
||
return conduits;
|
||
}
|
||
|
||
interface ClassAliases {
|
||
named: string[]; // local identifiers bound to class-table symbols
|
||
namespaces: string[]; // namespace identifiers over a schema source
|
||
// Member names under which a class symbol is reachable on SOME imported
|
||
// schema source (class symbols plus every renamed conduit export the file
|
||
// imports) — the alternation for namespace-member write targets.
|
||
memberSyms: Set<string>;
|
||
}
|
||
|
||
/**
|
||
* Parse an import named-binding list ("a, b as c") against the SOURCE's
|
||
* schema exports, adding locals bound to class symbols / schema namespaces.
|
||
*/
|
||
function importBindings(
|
||
namedList: string,
|
||
src: SchemaExports,
|
||
named: Set<string>,
|
||
namespaces: Set<string>,
|
||
): void {
|
||
for (const part of namedList.split(',')) {
|
||
const seg = part.trim();
|
||
if (!seg || seg.startsWith('type ')) continue;
|
||
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||
const original = asMatch ? asMatch[1]! : seg;
|
||
const local = asMatch ? asMatch[2]! : seg;
|
||
if (src.named.has(original)) named.add(local);
|
||
if (src.ns.has(original)) namespaces.add(local);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parse a destructuring pattern ("a, b: c, d = x") over a schema source.
|
||
* A class-symbol property binds a named alias; ANY other destructured
|
||
* property is over-approximated as a schema namespace (it may be a nested
|
||
* namespace such as `const { schema } = ns` — fail-closed).
|
||
*/
|
||
function destructureBindings(
|
||
pattern: string,
|
||
src: SchemaExports,
|
||
named: Set<string>,
|
||
namespaces: Set<string>,
|
||
): void {
|
||
for (const part of pattern.split(',')) {
|
||
const seg = part.trim();
|
||
if (!seg) continue;
|
||
const m = /^(\w+)\s*(?::\s*(\w+))?\s*(?:=[\s\S]*)?$/.exec(seg);
|
||
if (!m) continue;
|
||
const local = m[2] ?? m[1]!;
|
||
if (src.named.has(m[1]!)) named.add(local);
|
||
else namespaces.add(local);
|
||
}
|
||
}
|
||
|
||
function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<string>): ClassAliases {
|
||
const named = new Set<string>();
|
||
const namespaces = new Set<string>();
|
||
const memberSyms = new Set<string>(CLASS_SYMBOLS);
|
||
for (const m of f.code.matchAll(IMPORT_RE)) {
|
||
const [, typeOnly, defaultWith, namedList, nsName, defaultBare, spec] = m;
|
||
if (typeOnly) continue;
|
||
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||
for (const n of src.named) memberSyms.add(n);
|
||
if (nsName) namespaces.add(nsName);
|
||
if (namedList) importBindings(namedList, src, named, namespaces);
|
||
// A default import binds whatever the source exports as `default`.
|
||
const dflt = defaultWith ?? defaultBare;
|
||
if (dflt) {
|
||
if (src.named.has('default')) named.add(dflt);
|
||
if (src.ns.has('default')) namespaces.add(dflt);
|
||
}
|
||
}
|
||
// Literal dynamic imports of a schema source are import edges like any
|
||
// other (contract rev 9): both binding shapes feed prong (i).
|
||
for (const m of f.code.matchAll(
|
||
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
|
||
)) {
|
||
const [, pattern, nsName, , spec] = m;
|
||
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||
for (const n of src.named) memberSyms.add(n);
|
||
if (nsName) namespaces.add(nsName);
|
||
if (pattern) destructureBindings(pattern, src, named, namespaces);
|
||
}
|
||
// Aliases derived FROM a schema namespace, to a bounded depth (2 passes):
|
||
// const { companies } = ns; → named alias
|
||
// const { schema } = ns; → nested namespace (fail-closed)
|
||
// const t = ns.companies; → named alias
|
||
// const s2 = ns.schema; → namespace alias
|
||
const nsSrc: SchemaExports = { named: memberSyms, ns: new Set() };
|
||
for (let pass = 0; pass < 2; pass += 1) {
|
||
for (const ns of [...namespaces]) {
|
||
for (const m of f.code.matchAll(
|
||
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*(?:await\\s+)?${ns}\\b`, 'g'),
|
||
)) {
|
||
destructureBindings(m[1]!, nsSrc, named, namespaces);
|
||
}
|
||
for (const m of f.code.matchAll(
|
||
new RegExp(
|
||
`(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
|
||
'g',
|
||
),
|
||
)) {
|
||
if (memberSyms.has((m[2] ?? m[3])!)) named.add(m[1]!);
|
||
else namespaces.add(m[1]!);
|
||
}
|
||
}
|
||
}
|
||
return { named: [...named], namespaces: [...namespaces], memberSyms };
|
||
}
|
||
|
||
/**
|
||
* Capability conduits: modules that re-export raw-execution capability, so a
|
||
* consumer can obtain it without a literal driver/factory specifier.
|
||
* `driver` — re-exports (star, named, or default) from a driver package or
|
||
* another driver conduit; importing ANYTHING from one confers driver
|
||
* capability (the conduit module itself is additionally flagged by prong
|
||
* (iii)'s literal check, so these routes fail at both ends).
|
||
* `factory` — carries a NAME MAP (module → the exported names under which a
|
||
* factory is reachable), computed to a fixpoint: `export … from` propagates
|
||
* the SOURCE's exported factory names through renames (star and star-as
|
||
* copy them all), and a local binding of a factory (named import, namespace
|
||
* member extraction, or tracked dynamic-import destructure) exported under
|
||
* any name — braces, declaration, or `export default` — adds that name.
|
||
* Importing ANYTHING from a factory conduit (statically or via a tracked
|
||
* dynamic import) confers factory capability — the conduit may rename the
|
||
* symbol at any hop, so there is no consumer-side name gate.
|
||
*/
|
||
function computeCapabilityConduits(
|
||
files: FileFacts[],
|
||
fileSet: Set<string>,
|
||
): { driver: Set<string>; factory: Set<string> } {
|
||
const driver = new Set<string>();
|
||
// Factory conduits carry a NAME MAP (module → exported names under which a
|
||
// factory is reachable) so renames propagate hop by hop, exactly like the
|
||
// schema export map — a literal-name gate at any hop would launder.
|
||
const factoryNames = new Map<string, Set<string>>();
|
||
const isDriverSpec = (rel: string, spec: string): boolean => {
|
||
if (DRIVER_SPECIFIERS.includes(spec)) return true;
|
||
const r = resolveSpecifier(rel, spec, fileSet);
|
||
return r !== null && driver.has(r);
|
||
};
|
||
const factoryNamesOf = (rel: string, spec: string): Set<string> | null => {
|
||
if (spec === '@mosaicstack/db') return new Set(DB_FACTORY_SYMBOLS);
|
||
const r = resolveSpecifier(rel, spec, fileSet);
|
||
return r !== null ? (factoryNames.get(r) ?? null) : null;
|
||
};
|
||
let changed = true;
|
||
while (changed) {
|
||
changed = false;
|
||
for (const f of files) {
|
||
for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
|
||
if (m[1]) continue; // export type — erased
|
||
const spec = m[3]!;
|
||
if (!driver.has(f.rel) && isDriverSpec(f.rel, spec)) {
|
||
driver.add(f.rel);
|
||
changed = true;
|
||
}
|
||
}
|
||
// `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's
|
||
// named branch above; `export x from` is not valid syntax — covered.
|
||
const mine = factoryNames.get(f.rel) ?? new Set<string>();
|
||
const before = mine.size;
|
||
for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
|
||
if (m[1]) continue;
|
||
const src = factoryNamesOf(f.rel, m[3]!);
|
||
if (src === null || src.size === 0) continue;
|
||
if (m[2] === undefined) {
|
||
// `export *` / `export * as x` — over-approximated to the source's
|
||
// names (membership is what confers capability on consumers).
|
||
for (const n of src) mine.add(n);
|
||
} else {
|
||
for (const part of m[2].split(',')) {
|
||
const seg = part.trim();
|
||
if (!seg || seg.startsWith('type ')) continue;
|
||
const am = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||
if (src.has(am ? am[1]! : seg)) mine.add(am ? am[2]! : seg);
|
||
}
|
||
}
|
||
}
|
||
// Locals bound to a factory (named import — aliased or not — namespace
|
||
// member extraction, or tracked dynamic-import destructure), then
|
||
// exported under ANY name and by ANY form, braces or default included
|
||
// (`export const mk = mod.createDb`, `export default createDb`).
|
||
const locals = new Set<string>();
|
||
const nss = new Map<string, Set<string>>();
|
||
for (const im of f.code.matchAll(IMPORT_RE)) {
|
||
const [, typeOnly, , namedList, nsName, , spec] = im;
|
||
if (typeOnly) continue;
|
||
const src = factoryNamesOf(f.rel, spec!);
|
||
if (src === null || src.size === 0) continue;
|
||
if (nsName) nss.set(nsName, src);
|
||
if (namedList) {
|
||
for (const part of namedList.split(',')) {
|
||
const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(part.trim());
|
||
if (am && src.has(am[1]!)) locals.add(am[2] ?? am[1]!);
|
||
}
|
||
}
|
||
}
|
||
for (const dm of f.code.matchAll(
|
||
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
|
||
)) {
|
||
const [, pattern, nsName, , spec] = dm;
|
||
const src = factoryNamesOf(f.rel, spec!);
|
||
if (src === null || src.size === 0) continue;
|
||
if (nsName) nss.set(nsName, src);
|
||
if (pattern) {
|
||
for (const part of pattern.split(',')) {
|
||
const pm = /^(\w+)\s*(?::\s*(\w+))?/.exec(part.trim());
|
||
if (pm && src.has(pm[1]!)) locals.add(pm[2] ?? pm[1]!);
|
||
}
|
||
}
|
||
}
|
||
for (const [ns, src] of nss) {
|
||
for (const am of f.code.matchAll(
|
||
new RegExp(
|
||
`(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${ns}(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])`,
|
||
'g',
|
||
),
|
||
)) {
|
||
if (src.has((am[2] ?? am[3])!)) locals.add(am[1]!);
|
||
}
|
||
}
|
||
if (locals.size > 0) {
|
||
for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) {
|
||
for (const part of m[1]!.split(',')) {
|
||
const seg = part.trim();
|
||
if (!seg || seg.startsWith('type ')) continue;
|
||
const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(seg);
|
||
if (am && locals.has(am[1]!)) mine.add(am[2] ?? am[1]!);
|
||
}
|
||
}
|
||
for (const local of locals) {
|
||
if (
|
||
new RegExp(`export\\s+(?:const|let|var|function)\\s+${DECL_LIST}${local}\\b`).test(
|
||
f.code,
|
||
)
|
||
) {
|
||
mine.add(local);
|
||
}
|
||
// A derived binding exported under a NEW name re-exports the
|
||
// capability under that name: `export const mk2 = (mk);`
|
||
for (const dm of f.code.matchAll(
|
||
new RegExp(
|
||
`export\\s+(?:const|let|var)\\s+${DECL_LIST}(\\w+)${TYPE_ANN}\\s*=\\s*\\(*\\s*${local}\\b`,
|
||
'g',
|
||
),
|
||
)) {
|
||
mine.add(dm[1]!);
|
||
}
|
||
}
|
||
}
|
||
// The default slot, under any expression dressing — a local factory
|
||
// binding (`export default (createDb)`) or a namespace factory member
|
||
// (`export default mod.createDb`, `export default mod['createDb']`) —
|
||
// mirrors the schema side's default-export pass.
|
||
for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) {
|
||
const expr = stripExprDressing(m[1]!);
|
||
if (/^\w+$/.test(expr)) {
|
||
if (locals.has(expr)) mine.add('default');
|
||
} else {
|
||
for (const [ns, src] of nss) {
|
||
const t = new RegExp(
|
||
`^${ns}(?:${DOT}(\\w+)\\b|${BRACKET_OPEN}\\s*['"\`](\\w+)['"\`]\\s*\\])$`,
|
||
).exec(expr);
|
||
if (t && src.has((t[1] ?? t[2])!)) {
|
||
mine.add('default');
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (mine.size > before) {
|
||
factoryNames.set(f.rel, mine);
|
||
changed = true;
|
||
}
|
||
}
|
||
}
|
||
return { driver, factory: new Set(factoryNames.keys()) };
|
||
}
|
||
|
||
/** Value-import edge naming one of `symbols` (named import from anywhere, or literal dynamic package import while using the symbol). */
|
||
function hasSymbolImportEdge(code: string, symbols: string[], packageName: string): boolean {
|
||
const pkg = packageName.replace('/', '\\/');
|
||
const staticEdge = new RegExp(
|
||
`import\\s*(?!type\\b)(?:\\w+\\s*,\\s*)?\\{[^}]*\\b(${symbols.join('|')})\\b[^}]*\\}\\s*from\\s*['"][^'"]+['"]`,
|
||
).test(code);
|
||
const usesSymbol = new RegExp(`\\b(${symbols.join('|')})\\b`).test(code);
|
||
const dynamicEdge = usesSymbol && new RegExp(`import\\s*\\(\\s*['"]${pkg}['"]\\s*\\)`).test(code);
|
||
// Namespace form: `import * as ns from '<pkg>'` + `ns.<symbol>` usage
|
||
// (dot or bracket member, optional-chain/non-null tolerant).
|
||
let nsEdge = false;
|
||
for (const m of code.matchAll(
|
||
new RegExp(`import\\s*\\*\\s*as\\s+(\\w+)\\s*from\\s*['"]${pkg}['"]`, 'g'),
|
||
)) {
|
||
if (new RegExp(`\\b${m[1]}${memberTail(symbols.join('|'))}`).test(code)) nsEdge = true;
|
||
}
|
||
return staticEdge || dynamicEdge || nsEdge;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// The analyzer — pure over (rel, source) so the evasion controls below can
|
||
// feed synthetic files through the exact production logic.
|
||
// ---------------------------------------------------------------------------
|
||
interface AnalysisCtx {
|
||
fileSet: Set<string>;
|
||
conduits: SchemaConduits; // schema-symbol sources with exported names (prong i)
|
||
driverConduits: Set<string>; // driver-capability re-exporters (prong iii)
|
||
factoryConduits: Set<string>; // factory-capability re-exporters (prong iii)
|
||
}
|
||
|
||
function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
||
const { fileSet, conduits, driverConduits, factoryConduits } = ctx;
|
||
const violations: Violation[] = [];
|
||
const rel = f.rel;
|
||
const code = f.code;
|
||
const inAllowlist = WRITER_ALLOWLIST.includes(rel);
|
||
const inRegister = INFRA_REGISTER.includes(rel);
|
||
const isSchemaDefinition = rel === 'packages/db/src/schema.ts';
|
||
|
||
// Runtime code construction: fails anywhere. Covers direct calls
|
||
// (optional-call form included), and literal computed access
|
||
// (window['eval'], globalThis['Function']).
|
||
if (
|
||
new RegExp(
|
||
`\\beval${CALL_OPEN}|\\bnew\\s+Function\\s*\\(|\\[\\s*['"](eval|Function)['"]\\s*\\]`,
|
||
).test(code)
|
||
) {
|
||
violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' });
|
||
}
|
||
// Fail-closed CODE-SHAPE rules: statically-resolvable disguises of a
|
||
// literal member key or verb invocation. They match ordinary syntax, so a
|
||
// reviewed legitimate hit is dispositioned through CODE_SHAPE_REGISTER —
|
||
// the shapes themselves are never weakened.
|
||
if (!CODE_SHAPE_REGISTER.includes(rel)) {
|
||
// A computed member — access or call — whose key is a text-only template
|
||
// literal is invisible to every member and verb matcher (the lexer routes
|
||
// template text to spans, so the key survives in code as two ADJACENT
|
||
// backticks — the discriminator against backticks inside quoted-string
|
||
// prose, whose text stays in code, and against interpolated keys, whose
|
||
// `${…}` expression stays in code between the backticks). Runtime code
|
||
// construction's sibling: fail-closed in any position (write target,
|
||
// export expression, receiver, call). A template key WITH interpolation
|
||
// is a non-literal computed member (documented residual above).
|
||
if (new RegExp(`\\[\\s*\`\`\\s*\\]`).test(code)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'code-construction',
|
||
detail: 'template-literal computed member',
|
||
});
|
||
}
|
||
// A quoted OR template computed key carrying expression dressing
|
||
// (`ns['companies' as const]`, `ns['companies' satisfies 'companies']`,
|
||
// `ns['companies'!]`, and the template forms `ns[\`companies\` as
|
||
// const]`, `ns[\`companies\`!]`) keeps its static value while breaking
|
||
// every `['name']` matcher — the bracket alternatives require the
|
||
// closing quote to touch the `]`, and a dressed TEMPLATE key also breaks
|
||
// the template rule above (dressing intervenes before the `]`).
|
||
// Tolerance inside the matchers cannot span the class (the dressing's
|
||
// type text may itself contain a `]`, e.g. `as Foo['x']`), so the SHAPE
|
||
// fails closed: a quote-close followed by `!`/`as`/`satisfies` inside a
|
||
// bracket. The key class includes the backtick: a dressed template key
|
||
// survives in lexed code as the empty adjacent-backtick pair, which the
|
||
// empty-key case of this regex matches (an interpolated key keeps its
|
||
// `\${…}` between the backticks and stays clean unless dressed — a
|
||
// dressed interpolated key fires too, an over-match dispositioned like
|
||
// any other shape hit). The `!(?!=)` guard keeps ordinary comparisons
|
||
// (`o['k'] !== x` — dressing AFTER the bracket) clean.
|
||
if (
|
||
new RegExp(`\\[\\s*(['"\`])(?:(?!\\1)[^\\n])*\\1\\s*(?:!(?!=)|as\\s|satisfies\\s)`).test(code)
|
||
) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'code-construction',
|
||
detail: 'dressed computed-member key',
|
||
});
|
||
}
|
||
// A quoted key built from string escapes (a `\` + `u`/`x`/octal-digit
|
||
// sequence whose decoded text is a plain identifier, e.g. a key spelling
|
||
// `companies` with its first letter unicode-escaped) stays invisible to
|
||
// every `\w+`-keyed matcher. The key IS statically
|
||
// resolvable, so it is not under the non-literal residual: any bracketed
|
||
// quoted key containing an identifier-capable escape fails closed.
|
||
if (new RegExp(`\\[\\s*(['"])[^'"\\n]*\\\\[ux0-7][^'"\\n]*\\1\\s*\\]`).test(code)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'code-construction',
|
||
detail: 'escape-built computed-member key',
|
||
});
|
||
}
|
||
// Invoking a write/exec verb through Function.prototype indirection
|
||
// (`db.insert.apply(db, [companies])`, `d.execute.call(d, s)`,
|
||
// `db.insert.bind(db)`) hides the argument shape from every verb matcher
|
||
// while both member names stay statically visible — unlike the
|
||
// method-EXTRACTION residual, where the verb never appears as a member.
|
||
if (
|
||
new RegExp(
|
||
`(?:${DOT}(?:insert|update|delete|execute|query|unsafe|raw)\\b|` +
|
||
`\\[\\s*['"](?:insert|update|delete|execute|query|unsafe|raw)['"]\\s*\\])` +
|
||
`${DOT}(?:apply|call|bind)${CALL_OPEN}`,
|
||
).test(code)
|
||
) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'code-construction',
|
||
detail: 'verb apply/call/bind indirection',
|
||
});
|
||
}
|
||
// `Reflect` reaches the same members without member syntax:
|
||
// `Reflect.apply(db.insert, db, [companies])`, `Reflect.get(db,
|
||
// 'insert')`, `Reflect.getOwnPropertyDescriptor(db, 'execute')`. Verb
|
||
// detection through Reflect is not boundable: any argument window stops
|
||
// at a newline (prettier breaks a >100-col call one-arg-per-line, moving
|
||
// the verb past it) or at the first `)` (a nested-paren first argument
|
||
// closes it early), the method can be bracket-spelled
|
||
// (`Reflect['apply']`), and the object can be aliased
|
||
// (`const R = Reflect`). So the IDENTIFIER is the shape: any `Reflect`
|
||
// token in lexed code fails closed — member access, aliasing, or
|
||
// argument passing alike. The tree has zero occurrences outside test
|
||
// files (measured); a reviewed legitimate use is register-dispositioned.
|
||
// (`\b` keeps compound identifiers like `ReflectHelper` clean.)
|
||
if (new RegExp(`\\bReflect\\b`).test(code)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'code-construction',
|
||
detail: 'Reflect indirection',
|
||
});
|
||
}
|
||
}
|
||
// Dynamic import whose specifier is not a single string literal: the
|
||
// import graph becomes unanalyzable. Checked per call site, so a literal
|
||
// first fragment ('x' + y) does not slip past. A LITERAL import of a
|
||
// schema/factory source must additionally sit in the tracked binding shape
|
||
// (const X = await import(…)) — any other consumption of its promise
|
||
// (.then, deferred await, array wrapping) hides the binding from prong (i),
|
||
// so it fails closed here.
|
||
if (!DYNAMIC_IMPORT_REGISTER.includes(rel)) {
|
||
for (const m of code.matchAll(/\bimport\s*\(/g)) {
|
||
const idx = m.index ?? 0;
|
||
const tail = code.slice(idx + m[0].length);
|
||
const lit = /^\s*(['"])((?:[^'"\\]|\\.)*?)\1\s*[,)]/.exec(tail);
|
||
if (!lit) {
|
||
violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' });
|
||
continue;
|
||
}
|
||
const spec = lit[2]!;
|
||
const schemaSrc = schemaExportsOf(rel, spec, conduits, fileSet);
|
||
const factoryLike =
|
||
spec === '@mosaicstack/db' ||
|
||
(() => {
|
||
const r = resolveSpecifier(rel, spec, fileSet);
|
||
return r !== null && (factoryConduits.has(r) || driverConduits.has(r));
|
||
})() ||
|
||
DRIVER_SPECIFIERS.includes(spec);
|
||
if (schemaSrc !== null || factoryLike) {
|
||
const before = code.slice(Math.max(0, idx - 200), idx);
|
||
if (!/(?:const|let|var)\s*(?:\{[^}]*\}|\w+)\s*=\s*await\s*$/.test(before)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'dynamic-import',
|
||
detail: `literal import('${spec}') outside the tracked binding shape`,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// createRequire: an unanalyzable CJS import route. Tracked by name AND
|
||
// through import aliases (import { createRequire as x } / destructured
|
||
// from a dynamic module import).
|
||
if (!CREATE_REQUIRE_REGISTER.includes(rel)) {
|
||
const crNames = new Set<string>();
|
||
if (new RegExp(`\\bcreateRequire${CALL_OPEN}`).test(code)) crNames.add('createRequire');
|
||
for (const m of code.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"](?:node:)?module['"]/g)) {
|
||
const alias = /\bcreateRequire\s+as\s+(\w+)/.exec(m[1]!);
|
||
if (alias) crNames.add(alias[1]!);
|
||
}
|
||
for (const m of code.matchAll(
|
||
/(?:const|let|var)\s*\{([^}]*)\}\s*=\s*await\s+import\s*\(\s*['"](?:node:)?module['"]\s*\)/g,
|
||
)) {
|
||
const alias = /\bcreateRequire\s*:\s*(\w+)/.exec(m[1]!);
|
||
if (alias) crNames.add(alias[1]!);
|
||
}
|
||
// Destructuring from a STATIC or dynamically-bound module namespace:
|
||
// import * as M from 'node:module'; const { createRequire: x } = M;
|
||
const modNs = new Set<string>();
|
||
for (const m of code.matchAll(/import\s*\*\s*as\s+(\w+)\s*from\s*['"](?:node:)?module['"]/g)) {
|
||
modNs.add(m[1]!);
|
||
}
|
||
for (const m of code.matchAll(
|
||
/(?:const|let|var)\s+(\w+)\s*=\s*await\s+import\s*\(\s*['"](?:node:)?module['"]\s*\)/g,
|
||
)) {
|
||
modNs.add(m[1]!);
|
||
}
|
||
for (const ns of modNs) {
|
||
for (const m of code.matchAll(
|
||
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'),
|
||
)) {
|
||
const alias = /\bcreateRequire\s*(?::\s*(\w+))?/.exec(m[1]!);
|
||
if (alias) crNames.add(alias[1] ?? 'createRequire');
|
||
}
|
||
}
|
||
for (const n of crNames) {
|
||
if (n === 'createRequire' || new RegExp(`\\b${n}${CALL_OPEN}`).test(code)) {
|
||
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' });
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
const aliases = classAliases(f, conduits, fileSet);
|
||
|
||
// Prong (i): schema-symbol writes — alias-, namespace-, and conduit-aware.
|
||
if (!inAllowlist) {
|
||
// Word boundaries live inside each alternative: the bracket form ends in
|
||
// `]` (non-word), where a trailing `\b` could never match.
|
||
const targets: string[] = [...aliases.named].map((a) => `${a}\\b`);
|
||
// Namespace members match the source's EXPORTED names (renames included),
|
||
// not just the original class symbols.
|
||
const symAlt = [...aliases.memberSyms].join('|');
|
||
for (const ns of aliases.namespaces) {
|
||
// Allow intermediate property segments (ns.schema.companies — nested
|
||
// namespace re-exports), literal computed access (ns['companies']),
|
||
// and optional-chain/non-null markers (ns?.companies, ns!.companies).
|
||
targets.push(`${ns}${MEMBER_SEG}*${memberTail(symAlt)}`);
|
||
}
|
||
if (targets.length > 0) {
|
||
// The argument prefix tolerates parenthesization, spread, and array
|
||
// wrapping: .insert((companies)), .insert(...[companies]).
|
||
const writeRe = new RegExp(
|
||
`\\.\\s*(insert|update|delete)${CALL_OPEN}\\s*(?:(?:\\(|\\[|\\.\\.\\.)\\s*)*(${targets.join('|')})`,
|
||
'g',
|
||
);
|
||
for (const m of code.matchAll(writeRe)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'i-symbol',
|
||
detail: `.${m[1]}(${m[2]}) outside the writer allowlist`,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Computed member access with a literal verb name (obj['insert'](…))
|
||
// bypasses dot-based call detection; the tree has no legitimate use of the
|
||
// form (measured), so it fails outright. Write verbs fail outside the
|
||
// allowlist; execution verbs fail outside allowlist ∪ register.
|
||
if (!inAllowlist) {
|
||
const writeBracket = new RegExp(
|
||
`\\[\\s*['"](insert|update|delete)['"]\\s*\\]${CALL_OPEN}`,
|
||
'g',
|
||
);
|
||
for (const m of code.matchAll(writeBracket)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'i-symbol',
|
||
detail: `['${m[1]}']() computed-member write call`,
|
||
});
|
||
}
|
||
if (!inRegister) {
|
||
for (const m of code.matchAll(
|
||
new RegExp(`\\[\\s*['"](execute|query|unsafe)['"]\\s*\\]${CALL_OPEN}`, 'g'),
|
||
)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'iii-raw-execution',
|
||
detail: `['${m[1]}']() computed-member execution call`,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Prong (ii): class-table names in string/template spans with SQL context.
|
||
// The SQL keyword must be ADJACENT to the table name — co-residence
|
||
// anywhere in one span over-matches prose ("pnpm workspaces" plus an
|
||
// unrelated "from" in an embedded doc string is not SQL). Adjacency
|
||
// tolerates schema qualification (public.hierarchy_grants), bare
|
||
// qualifier words (TABLE, ONLY, IF EXISTS), interposed block comments,
|
||
// and quoting (including escaped quotes in span text).
|
||
if (!inAllowlist && !isSchemaDefinition) {
|
||
const kw =
|
||
'(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy|on|lock(?:\\s+table)?)';
|
||
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+';
|
||
// Bare qualifier words may sit between keyword and table name:
|
||
// GRANT … ON TABLE t, DELETE FROM ONLY t, DROP TABLE IF EXISTS t.
|
||
const qual = `(?:(?:table|only|if\\s+exists)${gap})*`;
|
||
const q = `(?:\\\\?["'\`])?`;
|
||
const sqlAdjacentRe = new RegExp(
|
||
`\\b${kw}${gap}${qual}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`,
|
||
'i',
|
||
);
|
||
for (const span of f.spans) {
|
||
if (sqlAdjacentRe.test(span)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'ii-literal',
|
||
detail: `class-table name in SQL context: ${span.slice(0, 80)}`,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Prong (iii): content-independent raw execution.
|
||
if (!inAllowlist && !inRegister) {
|
||
const literalDriver = new RegExp(
|
||
`(from\\s*|import\\s*\\(\\s*)['"](${DRIVER_SPECIFIERS.map((s) => s.replace('/', '\\/')).join('|')})['"]`,
|
||
).test(code);
|
||
// Any value import from a driver conduit confers driver capability.
|
||
let conduitDriver = false;
|
||
let factoryConduitImport = false;
|
||
for (const m of f.code.matchAll(IMPORT_RE)) {
|
||
if (m[1]) continue; // import type — erased
|
||
const r = resolveSpecifier(rel, m[6]!, fileSet);
|
||
if (r !== null && driverConduits.has(r)) conduitDriver = true;
|
||
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
||
}
|
||
// A TRACKED dynamic import of a conduit is an import edge like any
|
||
// static one (contract rev 9) — for capability too, not just prong (i).
|
||
for (const m of f.code.matchAll(/=\s*await\s+import\s*\(\s*(['"])([^'"]+)\1\s*\)/g)) {
|
||
const r = resolveSpecifier(rel, m[2]!, fileSet);
|
||
if (r !== null && driverConduits.has(r)) conduitDriver = true;
|
||
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
||
}
|
||
const driverImport = literalDriver || conduitDriver;
|
||
// Factory capability: a named/namespace edge to the factory symbols, OR
|
||
// ANY value import from a factory conduit — the conduit may re-export
|
||
// the factory under a different name (export { createDb as default }),
|
||
// so the consumer-side name gate is dropped for conduit imports.
|
||
const factoryImport =
|
||
factoryConduitImport ||
|
||
hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') ||
|
||
(aliases.namespaces.length > 0 &&
|
||
new RegExp(
|
||
`\\b(?:${aliases.namespaces.join('|')})${MEMBER_SEG}*${memberTail('createDb|createPgliteDb')}`,
|
||
).test(code)) ||
|
||
// Destructuring a factory symbol OUT of a schema namespace confers
|
||
// capability whatever the local rename: const { createDb: mk } = dbns.
|
||
(aliases.namespaces.length > 0 &&
|
||
new RegExp(
|
||
`(?:const|let|var)\\s*\\{[^}]*\\b(?:createDb|createPgliteDb)\\b[^}]*\\}\\s*=\\s*(?:await\\s+)?(?:${aliases.namespaces.join('|')})\\b`,
|
||
).test(code));
|
||
if (driverImport) {
|
||
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' });
|
||
// A driver client exposes query/execute/unsafe as raw primitives:
|
||
// flag them all on any receiver, any argument, spaced, optional-call
|
||
// (`?.(`) or not.
|
||
for (const m of code.matchAll(new RegExp(`\\.\\s*(execute|query|unsafe)${CALL_OPEN}`, 'g'))) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'iii-raw-execution',
|
||
detail: `.${m[1]}() in a driver-importing module`,
|
||
});
|
||
}
|
||
} else if (factoryImport) {
|
||
// A drizzle handle's raw primitive is .execute (its .query namespace is
|
||
// the relational builder, and unrelated .query() methods are common),
|
||
// so factory capability flags execute/unsafe on any receiver and query
|
||
// only on the db/client-shaped receiver backstop below.
|
||
for (const m of code.matchAll(new RegExp(`\\.\\s*(execute|unsafe)${CALL_OPEN}`, 'g'))) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'iii-raw-execution',
|
||
detail: `.${m[1]}() in a factory-importing module`,
|
||
});
|
||
}
|
||
}
|
||
if (!driverImport) {
|
||
// DI residual backstop: db/client-shaped receivers fire regardless of
|
||
// detected capability (a handle can arrive by injection).
|
||
// The receiver may be a dotted name OR a literal bracketed member with
|
||
// a conventional name — this['db'].query(…) — and the final member
|
||
// access tolerates ?. and ! markers (this.db?.query(…)).
|
||
for (const m of code.matchAll(
|
||
new RegExp(
|
||
`(?:\\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\\[\\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\\s*\\])${DOT}(execute|query|unsafe)${CALL_OPEN}`,
|
||
'g',
|
||
),
|
||
)) {
|
||
violations.push({
|
||
file: rel,
|
||
prong: 'iii-raw-execution',
|
||
detail: `.${m[1]}() on a db-shaped receiver`,
|
||
});
|
||
}
|
||
}
|
||
// sql.raw through aliases and namespaces (drizzle-orm and the db barrel).
|
||
const sqlAliases = new Set<string>();
|
||
for (const m of code.matchAll(IMPORT_RE)) {
|
||
const [, typeOnly, , namedList, nsName, , spec] = m;
|
||
if (typeOnly) continue;
|
||
if (!/^drizzle-orm|^@mosaicstack\/db$/.test(spec!)) continue;
|
||
if (namedList) {
|
||
for (const part of namedList.split(',')) {
|
||
const seg = part.trim();
|
||
const asMatch = /^sql\s+as\s+(\w+)$/.exec(seg);
|
||
if (seg === 'sql') sqlAliases.add('sql');
|
||
else if (asMatch) sqlAliases.add(asMatch[1]!);
|
||
}
|
||
}
|
||
if (nsName) sqlAliases.add(`${nsName}\\s*\\.\\s*sql`);
|
||
}
|
||
if (sqlAliases.size > 0) {
|
||
const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw${CALL_OPEN}`);
|
||
if (rawRe.test(code)) {
|
||
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' });
|
||
}
|
||
}
|
||
}
|
||
return violations;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tests
|
||
// ---------------------------------------------------------------------------
|
||
describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
|
||
const sourceRels = collectSources();
|
||
const fileSet = new Set(sourceRels);
|
||
const files: FileFacts[] = sourceRels.map((rel) => {
|
||
const { code, spans } = lexSource(readFileSync(join(REPO_ROOT, rel), 'utf8'));
|
||
return { rel, code, spans };
|
||
});
|
||
const conduits = computeSchemaConduits(files, fileSet);
|
||
const capConduits = computeCapabilityConduits(files, fileSet);
|
||
const ctx: AnalysisCtx = {
|
||
fileSet,
|
||
conduits,
|
||
driverConduits: capConduits.driver,
|
||
factoryConduits: capConduits.factory,
|
||
};
|
||
|
||
it('scans a non-empty production source set including plugins', () => {
|
||
expect(files.length).toBeGreaterThan(100);
|
||
expect(sourceRels.some((r) => r.startsWith('plugins/'))).toBe(true);
|
||
});
|
||
|
||
it('enumerated modules exist on disk (no stale allowlist/register entries)', () => {
|
||
for (const p of [
|
||
...WRITER_ALLOWLIST,
|
||
...INFRA_REGISTER,
|
||
...MIGRATION_RUNNER_IMPORTERS,
|
||
...MIGRATE_TIER_IMPORTERS,
|
||
...DB_FACTORY_IMPORTERS,
|
||
...DYNAMIC_IMPORT_REGISTER,
|
||
...CREATE_REQUIRE_REGISTER,
|
||
]) {
|
||
expect(existsSync(join(REPO_ROOT, p)), `enumerated module missing: ${p}`).toBe(true);
|
||
}
|
||
});
|
||
|
||
it('no registered module appears on the writer allowlist', () => {
|
||
for (const p of INFRA_REGISTER) {
|
||
expect(WRITER_ALLOWLIST, `register/allowlist overlap: ${p}`).not.toContain(p);
|
||
}
|
||
});
|
||
|
||
it('three-prong writer coverage holds over the production tree', () => {
|
||
const violations = files.flatMap((f) => analyzeFile(f, ctx));
|
||
expect(
|
||
violations,
|
||
violations.map((v) => `[prong ${v.prong}] ${v.file}: ${v.detail}`).join('\n'),
|
||
).toEqual([]);
|
||
});
|
||
|
||
it('migration-runner import edges are exactly the closed importer enumeration', () => {
|
||
const offenders: string[] = [];
|
||
for (const f of files) {
|
||
if (f.rel.startsWith('packages/db/src/')) continue; // the runner's own package
|
||
if (!new RegExp(`\\b(${MIGRATION_RUNNER_SYMBOLS.join('|')})\\b`).test(f.code)) continue;
|
||
if (
|
||
hasSymbolImportEdge(f.code, MIGRATION_RUNNER_SYMBOLS, '@mosaicstack/db') &&
|
||
!MIGRATION_RUNNER_IMPORTERS.includes(f.rel)
|
||
) {
|
||
offenders.push(f.rel);
|
||
}
|
||
}
|
||
expect(offenders, `unenumerated migration-runner importers:\n${offenders.join('\n')}`).toEqual(
|
||
[],
|
||
);
|
||
});
|
||
|
||
it('migrate-tier import edges are exactly the closed importer enumeration', () => {
|
||
const offenders: string[] = [];
|
||
for (const f of files) {
|
||
if (f.rel === 'packages/storage/src/migrate-tier.ts') continue;
|
||
const pathEdge =
|
||
/from\s*['"][^'"]*migrate-tier(\.js)?['"]/.test(f.code) ||
|
||
/import\s*\(\s*['"][^'"]*migrate-tier(\.js)?['"]\s*\)/.test(f.code);
|
||
const symbolEdge =
|
||
new RegExp(`\\b(${MIGRATE_TIER_SYMBOLS.join('|')})\\b`).test(f.code) &&
|
||
hasSymbolImportEdge(f.code, MIGRATE_TIER_SYMBOLS, '@mosaicstack/storage');
|
||
if ((pathEdge || symbolEdge) && !MIGRATE_TIER_IMPORTERS.includes(f.rel)) {
|
||
offenders.push(f.rel);
|
||
}
|
||
}
|
||
expect(offenders, `unenumerated migrate-tier importers:\n${offenders.join('\n')}`).toEqual([]);
|
||
});
|
||
|
||
it('db-factory (createDb/createPgliteDb) import edges are exactly the closed importer enumeration', () => {
|
||
const offenders: string[] = [];
|
||
for (const f of files) {
|
||
if (f.rel.startsWith('packages/db/src/')) continue; // the factories' own package
|
||
if (!new RegExp(`\\b(${DB_FACTORY_SYMBOLS.join('|')})\\b`).test(f.code)) continue;
|
||
if (
|
||
hasSymbolImportEdge(f.code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') &&
|
||
!DB_FACTORY_IMPORTERS.includes(f.rel)
|
||
) {
|
||
offenders.push(f.rel);
|
||
}
|
||
}
|
||
expect(offenders, `unenumerated db-factory importers:\n${offenders.join('\n')}`).toEqual([]);
|
||
});
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Permanent evasion controls: every known escape from the M4-1a review must
|
||
// be flagged by the analyzer, and the two legitimate controls must pass.
|
||
// Synthetic files run through the exact production analyzer.
|
||
// -------------------------------------------------------------------------
|
||
const EVASIONS: Array<{
|
||
name: string;
|
||
src: string;
|
||
extras?: Array<{ rel: string; src: string }>;
|
||
}> = [
|
||
{
|
||
name: 'E1 namespace import write',
|
||
src: `import * as dbSchema from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(dbSchema.companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E2 re-export laundering (conduit consumer)',
|
||
src: `import { companies } from './evasion-barrel.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E3 renamed createDb handle',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport async function f(t: string) { const d = createDb('u'); await d.execute('DELETE FROM ' + t); }`,
|
||
},
|
||
{
|
||
name: 'E4 aliased sql.raw',
|
||
src: `import { sql as q } from 'drizzle-orm';\nimport { db } from './x.js';\nexport async function f(t: string) { await db.execute(q.raw('TRUNCATE ' + t)); }`,
|
||
},
|
||
{
|
||
name: 'E5 interpolated tagged template on db receiver',
|
||
src: `import { sql } from 'drizzle-orm';\nimport { db } from './x.js';\nexport async function f() { const tbl = 'hierarchy_grants'; await db.execute(sql\`DELETE FROM \${tbl}\`); }`,
|
||
},
|
||
{
|
||
name: 'E6 driver client via import',
|
||
src: `import postgres from 'postgres';\nexport async function f(t: string) { const c = postgres('u'); await c.unsafe('TRUNCATE ' + t); }`,
|
||
},
|
||
{
|
||
name: 'E7 DI-shaped driver client query',
|
||
src: `export class R { constructor(private client: { query(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.client.query('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E8 string containing // does not hide code',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { const x = 'oops//'; await db.insert(companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'C-literal SQL string with class table',
|
||
src: `export const q = 'DELETE FROM hierarchy_grants WHERE role = $1';`,
|
||
},
|
||
{
|
||
name: 'C-eval',
|
||
src: `export function f(s: string) { return eval(s); }`,
|
||
},
|
||
// --- round-2 review shapes (R1–R5) ---
|
||
{
|
||
name: 'E9 namespace destructuring',
|
||
src: `import * as dbSchema from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { companies } = dbSchema;\nexport async function f() { await db.insert(companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E10 nested namespace re-export',
|
||
src: `import * as ns from './evasion-mid.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns.schema.companies).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid.ts',
|
||
src: `export * as schema from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E11 literal dynamic import, destructured binding',
|
||
src: `import { db } from './x.js';\nexport async function f() { const { companies } = await import('@mosaicstack/db'); await db.insert(companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E12 literal dynamic import, namespace binding',
|
||
src: `import { db } from './x.js';\nexport async function f() { const m = await import('@mosaicstack/db'); await db.insert(m.companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E13 createRequire outside its register',
|
||
src: `import { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\nexport const pg = require('postgres');`,
|
||
},
|
||
{
|
||
name: 'E14 factory capability laundered through re-export',
|
||
src: `import * as dbns from './evasion-mid2.js';\nexport async function f(t: string) { await dbns.createDb('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid2.ts',
|
||
src: `export { createDb } from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E15 driver default laundered through re-export',
|
||
src: `import pg from './evasion-mid3.js';\nexport async function f(t: string) { const c = pg('u'); await c.unsafe('TRUNCATE ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid3.ts',
|
||
src: `export { default } from 'postgres';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E16 concatenated dynamic-import specifier',
|
||
src: `export async function f() { const m = await import('@mosaicstack/' + 'db'); return m; }`,
|
||
},
|
||
{
|
||
name: 'E17 computed-member write call',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db['insert'](companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E18 spaced member access write',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db . insert (companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E19 computed-member execution call',
|
||
src: `export class R { constructor(private client: { query(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.client['query']('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E20 schema-qualified table in SQL span',
|
||
src: `export const q = 'DELETE FROM public.hierarchy_grants WHERE role = $1';`,
|
||
},
|
||
{
|
||
name: 'E21 comment interposed in SQL span',
|
||
src: `export const q = 'DELETE FROM /* audit */ hierarchy_grants WHERE role = $1';`,
|
||
},
|
||
{
|
||
name: 'E22 COPY statement in SQL span',
|
||
src: `export const q = 'COPY hierarchy_grants FROM STDIN';`,
|
||
},
|
||
{
|
||
name: 'E23 escaped-backtick-quoted table in SQL span',
|
||
src: 'export const q = `DELETE FROM \\`hierarchy_grants\\` WHERE role = 1`;',
|
||
},
|
||
// --- round-3 review shapes (G1–G6) ---
|
||
{
|
||
name: 'E24 export-site rename through conduit',
|
||
src: `import { c } from './evasion-mid4.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid4.ts',
|
||
src: `export { companies as c } from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E25 export of locally bound alias, renamed',
|
||
src: `import { co } from './evasion-mid5.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid5.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport { companies as co };`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E26 alias-export helper over dynamic namespace',
|
||
src: `import { co } from './evasion-mid6.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid6.ts',
|
||
src: `const mod = await import('@mosaicstack/db');\nexport const co = mod.companies;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E27 single-const alias from namespace',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst t = ns.companies;\nexport async function f() { await db.insert(t).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E28 non-class destructure from namespace (fail-closed)',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { schema } = ns;\nexport async function f() { await db.insert(schema.companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E29 destructure with default value',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { companies: co = null } = ns;\nexport async function f() { await db.insert(co).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E30 factory renamed to default through conduit',
|
||
src: `import createDbNow from './evasion-mid7.js';\nexport async function f(t: string) { await createDbNow('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid7.ts',
|
||
src: `export { createDb as default } from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E31 factory alias-export helper over dynamic namespace',
|
||
src: `import { mk } from './evasion-mid8.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid8.ts',
|
||
src: `const mod = await import('@mosaicstack/db');\nexport const mk = mod.createDb;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E32 createRequire import alias',
|
||
src: `import { createRequire as mkReq } from 'node:module';\nconst req = mkReq(import.meta.url);\nexport const pg = req('postgres');`,
|
||
},
|
||
{
|
||
name: 'E33 then-form dynamic schema import',
|
||
src: `import { db } from './x.js';\nexport function f() { return import('@mosaicstack/db').then((m) => db.insert(m.companies).values({})); }`,
|
||
},
|
||
{
|
||
name: 'E34 quoted schema qualifier in SQL span',
|
||
src: `export const q = 'DELETE FROM "public".hierarchy_grants WHERE role = $1';`,
|
||
},
|
||
{
|
||
name: 'E35 SQL line comment interposed in SQL span',
|
||
src: 'export const q = `DELETE FROM -- audit\nhierarchy_grants`;',
|
||
},
|
||
{
|
||
name: 'E36 spaced member access on conventional receiver',
|
||
src: `export class R { constructor(private db: { query(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.db . query ('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E37 bracket-form schema argument',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies']).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E38 parenthesized schema argument',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert((companies)).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E39 computed-member eval',
|
||
src: `export function f(s: string) { return (globalThis as never)['eval'](s); }`,
|
||
},
|
||
{
|
||
name: 'E40 schema symbol renamed to default through conduit',
|
||
src: `import c from './evasion-mid9.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid9.ts',
|
||
src: `export { companies as default } from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E41 export default of locally bound schema symbol',
|
||
src: `import c from './evasion-mid10.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid10.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport default companies;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E42 export default of dynamic namespace member',
|
||
src: `import c from './evasion-mid11.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid11.ts',
|
||
src: `const mod = await import('@mosaicstack/db');\nexport default mod.companies;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E43 namespace member under renamed export',
|
||
src: `import * as M from './evasion-mid12.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(M.co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid12.ts',
|
||
src: `export { companies as co } from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E44 dynamic namespace member under renamed export',
|
||
src: `import { db } from './x.js';\nconst M = await import('./evasion-mid13.js');\nexport async function f() { await db.insert(M.co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid13.ts',
|
||
src: `export { companies as co } from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E45 two-hop factory export-from rename',
|
||
src: `import { mk } from './evasion-mid15.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid14.ts',
|
||
src: `export { createDb as mk } from '@mosaicstack/db';`,
|
||
},
|
||
{
|
||
rel: 'packages/db/src/evasion-mid15.ts',
|
||
src: `export { mk } from './evasion-mid14.js';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E46 export default of locally bound factory',
|
||
src: `import mk from './evasion-mid16.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid16.ts',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport default createDb;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E47 tracked dynamic import of a factory conduit',
|
||
src: `const F = await import('./evasion-mid17.js');\nexport async function f(t: string) { await F.mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid17.ts',
|
||
src: `export { createDb as mk } from '@mosaicstack/db';`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E48 factory destructure-rename from schema namespace',
|
||
src: `import * as dbns from '@mosaicstack/db';\nconst { createDb: mk } = dbns;\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
},
|
||
{
|
||
name: 'E49 createRequire destructured from static module namespace',
|
||
src: `import * as M from 'node:module';\nconst { createRequire: mkReq } = M;\nconst req = mkReq(import.meta.url);\nexport const pg = req('postgres');`,
|
||
},
|
||
{
|
||
name: 'E50 spread-argument schema write',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(...[companies]).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E51 GRANT ON class table in SQL span',
|
||
src: `export const q = 'GRANT SELECT ON hierarchy_grants TO auditor';`,
|
||
},
|
||
{
|
||
name: 'E52 LOCK TABLE class table in SQL span',
|
||
src: `export const q = 'LOCK TABLE hierarchy_grants IN ACCESS EXCLUSIVE MODE';`,
|
||
},
|
||
{
|
||
name: 'E53 bracketed conventional receiver',
|
||
src: `export class R { async f(t: string) { await this['db'].query('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E54 parenthesized default export of schema symbol',
|
||
src: `import c from './evasion-mid18.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid18.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport default (companies);`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E55 bracket-member default export over dynamic namespace',
|
||
src: `import c from './evasion-mid19.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid19.ts',
|
||
src: `const mod = await import('@mosaicstack/db');\nexport default mod['companies'];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E56 type-asserted default export of schema symbol',
|
||
src: `import c from './evasion-mid20.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid20.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport default companies as unknown as object;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E57 export-const bracket member of schema namespace',
|
||
src: `import { co } from './evasion-mid21.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid21.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport const co = ns['companies'];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E58 default export of dynamic namespace factory member',
|
||
src: `import mk from './evasion-mid22.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid22.ts',
|
||
src: `const mod = await import('@mosaicstack/db');\nexport default mod.createDb;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E59 default export of static namespace factory member',
|
||
src: `import mk from './evasion-mid23.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid23.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport default ns.createDb;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E60 export-const bracket factory member',
|
||
src: `import { mk } from './evasion-mid24.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid24.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport const mk = ns['createDb'];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E61 parenthesized default export of factory local',
|
||
src: `import mk from './evasion-mid25.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid25.ts',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport default (createDb);`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E62 GRANT ON TABLE class table in SQL span',
|
||
src: `export const q = 'GRANT SELECT ON TABLE hierarchy_grants TO auditor';`,
|
||
},
|
||
{
|
||
name: 'E63 DROP TABLE IF EXISTS class table in SQL span',
|
||
src: `export const q = 'DROP TABLE IF EXISTS hierarchy_grants';`,
|
||
},
|
||
{
|
||
name: 'E64 DELETE FROM ONLY class table in SQL span',
|
||
src: `export const q = 'DELETE FROM ONLY hierarchy_grants WHERE id = $1';`,
|
||
},
|
||
{
|
||
name: 'E65 optional-chained namespace write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns?.companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E66 non-null-asserted namespace write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns!.companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E67 optional-chained DI receiver',
|
||
src: `export class R { constructor(private db?: { query(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.db?.query('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E68 optional-chained bracketed DI receiver',
|
||
src: `export class R { async f(t: string) { await this['db']?.query('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E69 non-null-asserted default export of schema symbol',
|
||
src: `import c from './evasion-mid26.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid26.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport default companies!;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E70 satisfies-dressed default export of schema symbol',
|
||
src: `import c from './evasion-mid27.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid27.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport default companies satisfies object;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E71 paren-plus-assertion-plus-non-null default export',
|
||
src: `import c from './evasion-mid28.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid28.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport default (companies as unknown)!;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E72 non-null-asserted ns-member default export (dynamic ns)',
|
||
src: `import c from './evasion-mid29.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid29.ts',
|
||
src: `const mod = await import('@mosaicstack/db');\nexport default mod.companies!;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E73 parenthesized ns-member export-const of schema symbol',
|
||
src: `import { co } from './evasion-mid30.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid30.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport const co = (ns.companies);`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E74 non-null-asserted default export of factory local',
|
||
src: `import mk from './evasion-mid31.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid31.ts',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport default createDb!;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E75 non-null-asserted ns-member factory default export',
|
||
src: `import mk from './evasion-mid32.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid32.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport default ns.createDb!;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E76 satisfies-dressed default export of factory local',
|
||
src: `import mk from './evasion-mid33.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid33.ts',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport default createDb satisfies typeof createDb;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E77 parenthesized ns-member factory derivation re-exported',
|
||
src: `import mk from './evasion-mid34.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid34.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nconst mk = (ns.createDb);\nexport default mk;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E78 optional-call write verb',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert?.(companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E79 optional-call DI receiver chain',
|
||
src: `export class R { constructor(private pool?: { execute(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.pool?.execute?.('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E80 optional-call execution in factory-capable file',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport async function f(t: string) { const d = createDb('u'); await d.execute?.('DELETE FROM ' + t); }`,
|
||
},
|
||
{
|
||
name: 'E81 template-literal computed member call',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\`](companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E82 template-literal computed member as write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns[\`companies\`]).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E83 template-literal computed member in schema default export',
|
||
src: `import c from './evasion-mid35.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid35.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport default ns[\`companies\`];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E84 template-literal computed member in factory extraction',
|
||
src: `import { mk } from './evasion-mid36.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid36.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport const mk = ns[\`createDb\`];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E85 type-annotated re-export of schema binding',
|
||
src: `import { co } from './evasion-mid37.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid37.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport const co: typeof companies = companies;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E86 second-declarator re-export of schema binding',
|
||
src: `import { co } from './evasion-mid38.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid38.ts',
|
||
src: `import { companies } from '@mosaicstack/db';\nexport const dummy = 0,\n co = companies;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E87 type-annotated re-export of factory binding',
|
||
src: `import { mk } from './evasion-mid39.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid39.ts',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport const mk: typeof createDb = createDb;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E88 second-declarator re-export of factory binding',
|
||
src: `import { mk } from './evasion-mid40.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid40.ts',
|
||
src: `import { createDb } from '@mosaicstack/db';\nexport const d0 = 0,\n mk = createDb;`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E89 apply-invoked write verb',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.apply(db, [companies]).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E90 call-invoked write verb',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert.call(db, companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E91 apply-invoked bracket verb in capability-free file',
|
||
src: `export class R { constructor(private pool: { execute(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.pool['execute'].apply(this.pool, ['TRUNCATE ' + t]); } }`,
|
||
},
|
||
{
|
||
name: 'E92 template-keyed receiver verb in capability-free file',
|
||
src: `export class R { constructor(private pool: { execute(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.pool[\`execute\`].apply(this.pool, ['TRUNCATE ' + t]); } }`,
|
||
},
|
||
{
|
||
name: 'E93 as-dressed computed key as write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies' as const]).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E94 satisfies-dressed computed key as write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies' satisfies 'companies']).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E95 non-null-dressed computed key as write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies'!]).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E96 escape-built computed key as write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['\\u0063ompanies']).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E97 dressed computed key in schema conduit export',
|
||
src: `import { co } from './evasion-mid41.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid41.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport const co = ns['companies' as const];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E98 dressed computed key in schema default export',
|
||
src: `import c from './evasion-mid42.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid42.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport default ns['companies' as const];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E99 dressed computed key in single-file factory extraction',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport async function f(t: string) { const mk = ns['createDb' as const]; const d = mk('u'); await d.execute('DELETE FROM ' + t); }`,
|
||
},
|
||
{
|
||
name: 'E100 Reflect.apply of a write verb',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await Reflect.apply(db.insert, db, [companies]); }`,
|
||
},
|
||
{
|
||
name: 'E101 Reflect.apply of an execution verb in capability-free file',
|
||
src: `export class R { constructor(private pool: { execute(s: string): Promise<unknown> }) {}\n async f(t: string) { await Reflect.apply(this.pool.execute, this.pool, ['TRUNCATE ' + t]); } }`,
|
||
},
|
||
{
|
||
name: 'E102 Reflect.get extraction of an execution verb',
|
||
src: `import { db } from './x.js';\nexport async function f(t: string) { const fn = Reflect.get(db, 'execute') as (s: string) => Promise<unknown>; await fn.call(db, 'DELETE FROM ' + t); }`,
|
||
},
|
||
{
|
||
name: 'E103 as-dressed template key as write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns[\`companies\` as const]).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E104 as-dressed template key as verb call',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\` as const](companies).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E105 non-null-dressed template key as write target',
|
||
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns[\`companies\`!]).values({}); }`,
|
||
},
|
||
{
|
||
name: 'E106 dressed template receiver verb in capability-free file',
|
||
src: `export class R { constructor(private pool: { query(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.pool[\`query\` as const]('TRUNCATE ' + t); } }`,
|
||
},
|
||
{
|
||
name: 'E107 dressed template key in schema conduit export',
|
||
src: `import { co } from './evasion-mid43.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||
extras: [
|
||
{
|
||
rel: 'packages/db/src/evasion-mid43.ts',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport const co = ns[\`companies\` as const];`,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'E108 dressed template key in single-file factory extraction',
|
||
src: `import * as ns from '@mosaicstack/db';\nexport async function f(t: string) { const mk = ns[\`createDb\` as const]; const d = mk('u'); await d.execute('DELETE FROM ' + t); }`,
|
||
},
|
||
{
|
||
name: 'E109 multiline Reflect.apply in prettier-broken shape',
|
||
src: `export class R { constructor(private pool: { execute(s: string): Promise<unknown> }) {}\n async f(t: string) {\n await Reflect.apply(\n this.pool.execute,\n this.pool,\n ['TRUNCATE ' + t],\n );\n } }`,
|
||
},
|
||
{
|
||
name: 'E110 bracket-spelled Reflect method',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await Reflect['apply'](db.insert, db, [companies]); }`,
|
||
},
|
||
{
|
||
name: 'E111 Reflect.apply with nested-paren first argument',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await Reflect.apply((fn: () => unknown) => fn(), db.insert, [db, [companies]]); }`,
|
||
},
|
||
{
|
||
name: 'E112 aliased Reflect',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { const R = Reflect; await R.apply(db.insert, db, [companies]); }`,
|
||
},
|
||
];
|
||
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
|
||
{
|
||
name: 'clean: schema read via select',
|
||
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { return db.select().from(companies); }`,
|
||
},
|
||
{
|
||
name: 'clean: comment mentioning a table is not SQL',
|
||
src: `// syncs hierarchy_grants downstream\nexport const n = 1;`,
|
||
},
|
||
{
|
||
name: 'clean: interpolated template key is a non-literal computed member, not a text-only key',
|
||
src: `export function g(o: Record<string, () => void>, k: string) { o[\`\${k}\`](); }`,
|
||
},
|
||
{
|
||
name: 'clean: strict-inequality after a bracket member is not key dressing',
|
||
src: `export function h(o: Record<string, string>) { return o['kind'] !== 'x'; }`,
|
||
},
|
||
{
|
||
name: 'clean: undressed interpolated template key stays a non-literal member under the backtick key class',
|
||
src: `export function j(o: Record<string, string>, k: string) { return o[\`\${k}\`]; }`,
|
||
},
|
||
{
|
||
name: 'clean: compound identifier containing Reflect is not the Reflect object',
|
||
src: `export class ReflectHelper { reflectStyle = 1; }\n// Reflect in a comment is prose, not code`,
|
||
},
|
||
];
|
||
|
||
it('analyzer flags every known evasion form', () => {
|
||
// A synthetic conduit so E2 exercises the fixpoint through a real re-export.
|
||
const barrel: FileFacts = {
|
||
rel: 'packages/db/src/evasion-barrel.ts',
|
||
...lexSource(`export { companies } from '@mosaicstack/db';`),
|
||
};
|
||
for (const e of EVASIONS) {
|
||
const rel = 'packages/db/src/evasion-sample.ts';
|
||
const synthetic: FileFacts = { rel, ...lexSource(e.src) };
|
||
const extraFacts: FileFacts[] = (e.extras ?? []).map((x) => ({
|
||
rel: x.rel,
|
||
...lexSource(x.src),
|
||
}));
|
||
const synthFiles = [...files, barrel, ...extraFacts, synthetic];
|
||
const synthSet = new Set([...fileSet, rel, barrel.rel, ...extraFacts.map((x) => x.rel)]);
|
||
const synthCap = computeCapabilityConduits(synthFiles, synthSet);
|
||
const synthCtx: AnalysisCtx = {
|
||
fileSet: synthSet,
|
||
conduits: computeSchemaConduits(synthFiles, synthSet),
|
||
driverConduits: synthCap.driver,
|
||
factoryConduits: synthCap.factory,
|
||
};
|
||
// An evasion is caught when ANY file in the chain is flagged: some
|
||
// laundering routes fail closed at the HELPER (origin), not the
|
||
// consumer — e.g. a template-keyed conduit export — which keeps the
|
||
// chain out of the tree just as effectively.
|
||
const v = [synthetic, ...extraFacts].flatMap((ff) => analyzeFile(ff, synthCtx));
|
||
expect(v.length, `evasion not caught: ${e.name}`).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
it('analyzer passes legitimate non-writer code (no false positives on controls)', () => {
|
||
for (const c of CLEAN_CONTROLS) {
|
||
const rel = 'packages/db/src/clean-sample.ts';
|
||
const synthetic: FileFacts = { rel, ...lexSource(c.src) };
|
||
const synthFiles = [...files, synthetic];
|
||
const synthSet = new Set([...fileSet, rel]);
|
||
const synthCap = computeCapabilityConduits(synthFiles, synthSet);
|
||
const synthCtx: AnalysisCtx = {
|
||
fileSet: synthSet,
|
||
conduits: computeSchemaConduits(synthFiles, synthSet),
|
||
driverConduits: synthCap.driver,
|
||
factoryConduits: synthCap.factory,
|
||
};
|
||
const v = analyzeFile(synthetic, synthCtx);
|
||
expect(
|
||
v,
|
||
`false positive on ${c.name}: ${v.map((x) => `${x.prong}:${x.detail}`).join('; ')}`,
|
||
).toEqual([]);
|
||
}
|
||
});
|
||
});
|