fix(db): propagate symbol identity through conduits in writer-coverage assertion
ci/woodpecker/pr/ci Pipeline was successful

Round-3 review remediation (G1-G6):
- export-map schema conduits: renames propagate through export-from,
  export-of-local, and export-const-from-namespace routes (G1)
- fail-closed namespace destructure over-approximation + depth-2 alias
  derivation (const t = ns.companies, const { schema } = ns) (G1)
- factory capability: consumer-side name gate dropped for conduit imports;
  export-of-local factory bindings make a module a factory conduit (G2)
- createRequire tracked through import aliases and dynamic destructure (G3)
- literal dynamic import of a schema/factory/driver source outside the
  tracked const-await binding shape is a violation (.then/deferred/
  Promise.all) (G4)
- prong (ii): quoted schema qualifier and SQL line-comment gaps (G5)
- computed eval, spaced execution verbs, bracket-form and parenthesized
  write arguments, spaced sql.raw (G6)
- value-flow residual documented in KNOWN RESIDUALS with counterfactuals
- 16 new permanent controls (E24-E39)
This commit is contained in:
fred
2026-08-27 17:04:40 -05:00
parent 7dedc8d3c0
commit f44db58b0d
+418 -97
View File
@@ -9,9 +9,17 @@
* (i) Symbol prong — write references (insert/update/delete) to the * (i) Symbol prong — write references (insert/update/delete) to the
* class-table schema symbols occur only in allowlisted modules. * class-table schema symbols occur only in allowlisted modules.
* Schema symbols are tracked through named imports (aliased or not), * Schema symbols are tracked through named imports (aliased or not),
* namespace imports, and re-export conduits: any scanned module that * namespace imports, and re-export conduits. Conduits carry an
* re-exports the schema (or another conduit) is itself treated as a * EXPORT MAP (per-module named exports and namespace exports),
* schema source, computed to a fixpoint. * 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. 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 * (ii) Literal prong — a class-table name inside a string or template
* span that also carries SQL context fails outside the allowlist. * span that also carries SQL context fails outside the allowlist.
* Spans are produced by a real lexer, so comments cannot hide code * Spans are produced by a real lexer, so comments cannot hide code
@@ -28,14 +36,23 @@
* receiver backstop still fires on db/client-shaped receivers. * receiver backstop still fires on db/client-shaped receivers.
* `sql.raw` is tracked through import aliasing and namespaces. * `sql.raw` is tracked through import aliasing and namespaces.
* *
* Runtime code-construction primitives (eval, new Function) fail anywhere — * Runtime code-construction primitives (eval, new Function — spelled
* allowlist and register included: constructed code defeats every static * directly or as a literal computed member like `globalThis['eval']`) fail
* prong, so there is no enumerated disposition path for it. Unanalyzable * anywhere — allowlist and register included: constructed code defeats every
* import routes (a dynamic import whose specifier is not a single string * static prong, so there is no enumerated disposition path for it.
* literal, and createRequire) fail everywhere except their own enumerated * Unanalyzable import routes fail everywhere except their own enumerated
* disposition registers below. A LITERAL dynamic import is an import edge * disposition registers below: a dynamic import whose specifier is not a
* like any static import (contract rev 9): its bindings (destructured or * single string literal, and createRequire (tracked through import aliasing
* namespace) feed prong (i) exactly as static import bindings do. * — `createRequire as x` — and destructuring from the module namespace; 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): * KNOWN RESIDUALS (deliberate, reviewed trade-offs — not claims of closure):
* - In a file with NO detected raw capability (no driver/factory import on * - In a file with NO detected raw capability (no driver/factory import on
@@ -50,6 +67,17 @@
* statically resolvable; literal computed access (obj['insert']()) is * statically resolvable; literal computed access (obj['insert']()) is
* flagged. Constructing the verb at runtime is adjacent to eval and is * flagged. Constructing the verb at runtime is adjacent to eval and is
* expected to be caught in review. * expected to be caught in review.
* - 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, and a helper function that returns a schema symbol.
* 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 <root>/<pkg>/src for the three roots; production * - The scan perimeter is <root>/<pkg>/src for the three roots; production
* TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not * TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not
* scanned (verified free of db/driver/execute references at review time). * scanned (verified free of db/driver/execute references at review time).
@@ -415,46 +443,109 @@ interface Violation {
} }
/** /**
* Compute the fixpoint set of "schema sources": module paths from which a * Schema exports of one module in the conduit graph: `named` are exported
* class-table symbol can be imported. Seeds: the schema module and the db * identifiers bound to a class-table symbol (under WHATEVER exported name —
* package barrel (plus the bare '@mosaicstack/db' specifier, handled * renames propagate); `ns` are exported identifiers that are themselves
* separately). Any scanned module that re-exports from a schema source * namespaces over a schema source (`export * as x from …`).
* (star, or a named list carrying a class symbol) — or imports class symbols
* and re-exports those local names — joins the set.
*/ */
function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Set<string> { interface SchemaExports {
const conduits = new Set<string>(['packages/db/src/schema.ts', 'packages/db/src/index.ts']); named: Set<string>;
const isSchemaSpec = (rel: string, spec: string): boolean => { ns: Set<string>;
if (spec === '@mosaicstack/db') return true; }
const resolved = resolveSpecifier(rel, spec, fileSet); type SchemaConduits = Map<string, SchemaExports>;
return resolved !== null && conduits.has(resolved);
}; /** 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; let changed = true;
while (changed) { while (changed) {
changed = false; changed = false;
for (const f of files) { for (const f of files) {
if (conduits.has(f.rel)) continue; const mine: SchemaExports = conduits.get(f.rel) ?? {
let isConduit = false; 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)) { for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
if (m[1]) continue; // export type — erased if (m[1]) continue; // export type — erased
if (!isSchemaSpec(f.rel, m[3]!)) continue; const src = schemaExportsOf(f.rel, m[3]!, conduits, fileSet);
if (m[2] === undefined) { if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
isConduit = true; // export * from schema source const starAs = /export\s*\*\s*as\s+(\w+)/.exec(m[0]);
} else if (CLASS_SYMBOLS.some((s) => new RegExp(`\\b${s}\\b`).test(m[2]!))) { if (starAs) {
isConduit = true; 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);
}
} }
} }
if (!isConduit) { // Exports of local bindings: `export { x as y }` where x is a local
const aliases = classAliases(f, conduits, fileSet); // class alias (or namespace), and `export const y = ns.<classSym>`.
if (aliases.named.length > 0) { const aliases = classAliases(f, conduits, fileSet);
const exported = [...f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)] if (aliases.named.length > 0 || aliases.namespaces.length > 0) {
.map((m) => m[1]!) for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) {
.join(','); for (const part of m[1]!.split(',')) {
if (aliases.named.some((a) => new RegExp(`\\b${a}\\b`).test(exported))) isConduit = true; 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+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`,
'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+(\\w+)\\s*=\\s*${local}\\b`, 'g'),
)) {
mine.named.add(m[1]!);
}
} }
} }
if (isConduit) { if (mine.named.size + mine.ns.size > before) {
conduits.add(f.rel); conduits.set(f.rel, mine);
changed = true; changed = true;
} }
} }
@@ -467,48 +558,60 @@ interface ClassAliases {
namespaces: string[]; // namespace identifiers over a schema source namespaces: string[]; // namespace identifiers over a schema source
} }
/** Parse an import named-binding list ("a, b as c") into locals bound to class symbols. */ /**
function importBindings(namedList: string, named: Set<string>): void { * 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(',')) { for (const part of namedList.split(',')) {
const seg = part.trim(); const seg = part.trim();
if (!seg || seg.startsWith('type ')) continue; if (!seg || seg.startsWith('type ')) continue;
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg); const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
const original = asMatch ? asMatch[1]! : seg; const original = asMatch ? asMatch[1]! : seg;
const local = asMatch ? asMatch[2]! : seg; const local = asMatch ? asMatch[2]! : seg;
if (CLASS_SYMBOLS.includes(original)) named.add(local); if (src.named.has(original)) named.add(local);
if (src.ns.has(original)) namespaces.add(local);
} }
} }
/** Parse a destructuring pattern ("a, b: c") into locals bound to class symbols. */ /**
function destructureBindings(pattern: string, named: Set<string>): void { * 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(',')) { for (const part of pattern.split(',')) {
const seg = part.trim(); const seg = part.trim();
if (!seg) continue; if (!seg) continue;
const m = /^(\w+)\s*(?::\s*(\w+))?$/.exec(seg); const m = /^(\w+)\s*(?::\s*(\w+))?\s*(?:=[\s\S]*)?$/.exec(seg);
if (m && CLASS_SYMBOLS.includes(m[1]!)) named.add(m[2] ?? m[1]!); if (!m) continue;
const local = m[2] ?? m[1]!;
if (src.named.has(m[1]!)) named.add(local);
else namespaces.add(local);
} }
} }
function isSchemaSpecifier( function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<string>): ClassAliases {
rel: string,
spec: string,
conduits: Set<string>,
fileSet: Set<string>,
): boolean {
if (spec === '@mosaicstack/db') return true;
const r = resolveSpecifier(rel, spec, fileSet);
return r !== null && conduits.has(r);
}
function classAliases(f: FileFacts, conduits: Set<string>, fileSet: Set<string>): ClassAliases {
const named = new Set<string>(); const named = new Set<string>();
const namespaces = new Set<string>(); const namespaces = new Set<string>();
for (const m of f.code.matchAll(IMPORT_RE)) { for (const m of f.code.matchAll(IMPORT_RE)) {
const [, typeOnly, , namedList, nsName, , spec] = m; const [, typeOnly, , namedList, nsName, , spec] = m;
if (typeOnly) continue; if (typeOnly) continue;
if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
if (nsName) namespaces.add(nsName); if (nsName) namespaces.add(nsName);
if (namedList) importBindings(namedList, named); if (namedList) importBindings(namedList, src, named, namespaces);
} }
// Literal dynamic imports of a schema source are import edges like any // Literal dynamic imports of a schema source are import edges like any
// other (contract rev 9): both binding shapes feed prong (i). // other (contract rev 9): both binding shapes feed prong (i).
@@ -516,17 +619,33 @@ function classAliases(f: FileFacts, conduits: Set<string>, fileSet: Set<string>)
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g, /(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
)) { )) {
const [, pattern, nsName, , spec] = m; const [, pattern, nsName, , spec] = m;
if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
if (nsName) namespaces.add(nsName); if (nsName) namespaces.add(nsName);
if (pattern) destructureBindings(pattern, named); if (pattern) destructureBindings(pattern, src, named, namespaces);
} }
// Destructuring from a schema namespace binds class symbols to locals: // Aliases derived FROM a schema namespace, to a bounded depth (2 passes):
// `import * as s from '@mosaicstack/db'; const { companies } = s;` // const { companies } = ns; → named alias
for (const ns of [...namespaces]) { // const { schema } = ns; → nested namespace (fail-closed)
for (const m of f.code.matchAll( // const t = ns.companies; → named alias
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'), // const s2 = ns.schema; → namespace alias
)) { const nsSrc: SchemaExports = { named: new Set(CLASS_SYMBOLS), ns: new Set() };
destructureBindings(m[1]!, named); 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+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(\\w+)\\b`,
'g',
),
)) {
if (CLASS_SYMBOLS.includes(m[2]!)) named.add(m[1]!);
else namespaces.add(m[1]!);
}
} }
} }
return { named: [...named], namespaces: [...namespaces] }; return { named: [...named], namespaces: [...namespaces] };
@@ -540,8 +659,10 @@ function classAliases(f: FileFacts, conduits: Set<string>, fileSet: Set<string>)
* capability (the conduit module itself is additionally flagged by prong * capability (the conduit module itself is additionally flagged by prong
* (iii)'s literal check, so these routes fail at both ends). * (iii)'s literal check, so these routes fail at both ends).
* `factory` — re-exports createDb/createPgliteDb from the db package or * `factory` — re-exports createDb/createPgliteDb from the db package or
* another factory conduit; importing from one while referencing a factory * another factory conduit (via `export from`, or by exporting a local
* symbol confers factory capability. * binding of the factory under any name); importing ANYTHING from one
* confers factory capability — the conduit may rename the symbol, so there
* is no consumer-side name gate.
*/ */
function computeCapabilityConduits( function computeCapabilityConduits(
files: FileFacts[], files: FileFacts[],
@@ -580,6 +701,58 @@ function computeCapabilityConduits(
} }
// `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's // `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's
// named branch above; `export x from` is not valid syntax — covered. // named branch above; `export x from` is not valid syntax — covered.
// Export-of-local factory bindings: a module that BINDS the factory
// (named import, namespace member, or tracked dynamic import) and
// exports that binding under any name is a factory conduit even with
// no `export … from` clause (`export const mk = mod.createDb`).
if (!factory.has(f.rel)) {
const locals = new Set<string>();
const nss = new Set<string>();
for (const im of f.code.matchAll(IMPORT_RE)) {
const [, typeOnly, , namedList, nsName, , spec] = im;
if (typeOnly || !isFactorySpec(f.rel, spec!)) continue;
if (nsName) nss.add(nsName);
if (namedList) {
for (const part of namedList.split(',')) {
const am = /^(createDb|createPgliteDb)(?:\s+as\s+(\w+))?$/.exec(part.trim());
if (am) 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;
if (!isFactorySpec(f.rel, spec!)) continue;
if (nsName) nss.add(nsName);
if (pattern) {
for (const part of pattern.split(',')) {
const pm = /^(createDb|createPgliteDb)\s*(?::\s*(\w+))?/.exec(part.trim());
if (pm) locals.add(pm[2] ?? pm[1]!);
}
}
}
for (const ns of nss) {
for (const am of f.code.matchAll(
new RegExp(
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(?:createDb|createPgliteDb)\\b`,
'g',
),
)) {
locals.add(am[1]!);
}
}
for (const local of locals) {
if (
new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code) ||
new RegExp(`export\\s*\\{[^}]*\\b${local}\\b[^}]*\\}`).test(f.code)
) {
factory.add(f.rel);
changed = true;
break;
}
}
}
} }
} }
return { driver, factory }; return { driver, factory };
@@ -609,7 +782,7 @@ function hasSymbolImportEdge(code: string, symbols: string[], packageName: strin
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
interface AnalysisCtx { interface AnalysisCtx {
fileSet: Set<string>; fileSet: Set<string>;
conduits: Set<string>; // schema-symbol sources (prong i) conduits: SchemaConduits; // schema-symbol sources with exported names (prong i)
driverConduits: Set<string>; // driver-capability re-exporters (prong iii) driverConduits: Set<string>; // driver-capability re-exporters (prong iii)
factoryConduits: Set<string>; // factory-capability re-exporters (prong iii) factoryConduits: Set<string>; // factory-capability re-exporters (prong iii)
} }
@@ -623,39 +796,90 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
const inRegister = INFRA_REGISTER.includes(rel); const inRegister = INFRA_REGISTER.includes(rel);
const isSchemaDefinition = rel === 'packages/db/src/schema.ts'; const isSchemaDefinition = rel === 'packages/db/src/schema.ts';
// Runtime code construction: fails anywhere. // Runtime code construction: fails anywhere. Covers direct calls and
if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(code)) { // literal computed access (window['eval'], globalThis['Function']).
if (/\beval\s*\(|\bnew\s+Function\s*\(|\[\s*['"](eval|Function)['"]\s*\]/.test(code)) {
violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' });
} }
// Dynamic import whose specifier is not a single string literal: the // Dynamic import whose specifier is not a single string literal: the
// import graph becomes unanalyzable. Checked per call site, so a literal // import graph becomes unanalyzable. Checked per call site, so a literal
// first fragment ('x' + y) does not slip past. // 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)) { if (!DYNAMIC_IMPORT_REGISTER.includes(rel)) {
for (const m of code.matchAll(/\bimport\s*\(/g)) { for (const m of code.matchAll(/\bimport\s*\(/g)) {
const tail = code.slice((m.index ?? 0) + m[0].length); const idx = m.index ?? 0;
if (!/^\s*(['"])(?:[^'"\\]|\\.)*?\1\s*[,)]/.test(tail)) { 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()' }); 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. // createRequire: an unanalyzable CJS import route. Tracked by name AND
if (!CREATE_REQUIRE_REGISTER.includes(rel) && /\bcreateRequire\s*\(/.test(code)) { // through import aliases (import { createRequire as x } / destructured
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); // from a dynamic module import).
if (!CREATE_REQUIRE_REGISTER.includes(rel)) {
const crNames = new Set<string>();
if (/\bcreateRequire\s*\(/.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]!);
}
for (const n of crNames) {
if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) {
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' });
break;
}
}
} }
const aliases = classAliases(f, conduits, fileSet); const aliases = classAliases(f, conduits, fileSet);
// Prong (i): schema-symbol writes — alias-, namespace-, and conduit-aware. // Prong (i): schema-symbol writes — alias-, namespace-, and conduit-aware.
if (!inAllowlist) { if (!inAllowlist) {
const targets: string[] = [...aliases.named]; // 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`);
const symAlt = CLASS_SYMBOLS.join('|');
for (const ns of aliases.namespaces) { for (const ns of aliases.namespaces) {
// Allow intermediate property segments: ns.schema.companies (nested // Allow intermediate property segments (ns.schema.companies nested
// namespace re-exports) as well as ns.companies. // namespace re-exports) and literal computed access (ns['companies']).
for (const s of CLASS_SYMBOLS) targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*${s}`); targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${symAlt})\\b`);
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`);
} }
if (targets.length > 0) { if (targets.length > 0) {
// `\(\s*\(*` tolerates argument parenthesization: .insert((companies)).
const writeRe = new RegExp( const writeRe = new RegExp(
`\\.\\s*(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`, `\\.\\s*(insert|update|delete)\\s*\\(\\s*\\(*\\s*(${targets.join('|')})`,
'g', 'g',
); );
for (const m of code.matchAll(writeRe)) { for (const m of code.matchAll(writeRe)) {
@@ -701,10 +925,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
if (!inAllowlist && !isSchemaDefinition) { if (!inAllowlist && !isSchemaDefinition) {
const kw = const kw =
'(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)'; '(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)';
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/)+'; const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+';
const q = `(?:\\\\?["'\`])?`; const q = `(?:\\\\?["'\`])?`;
const sqlAdjacentRe = new RegExp( const sqlAdjacentRe = new RegExp(
`\\b${kw}${gap}${q}(?:\\w+\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`, `\\b${kw}${gap}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`,
'i', 'i',
); );
for (const span of f.spans) { for (const span of f.spans) {
@@ -733,20 +957,22 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true; if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
} }
const driverImport = literalDriver || conduitDriver; const driverImport = literalDriver || conduitDriver;
const usesFactorySymbol = /\b(createDb|createPgliteDb)\b/.test(code); // 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 = const factoryImport =
factoryConduitImport ||
hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') || hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') ||
(factoryConduitImport && usesFactorySymbol) ||
(aliases.namespaces.length > 0 && (aliases.namespaces.length > 0 &&
usesFactorySymbol &&
new RegExp( new RegExp(
`\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`, `\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`,
).test(code)); ).test(code));
if (driverImport) { if (driverImport) {
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' }); violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' });
// A driver client exposes query/execute/unsafe as raw primitives: // A driver client exposes query/execute/unsafe as raw primitives:
// flag them all on any receiver, any argument. // flag them all on any receiver, any argument, spaced or not.
for (const m of code.matchAll(/\.(execute|query|unsafe)\s*\(/g)) { for (const m of code.matchAll(/\.\s*(execute|query|unsafe)\s*\(/g)) {
violations.push({ violations.push({
file: rel, file: rel,
prong: 'iii-raw-execution', prong: 'iii-raw-execution',
@@ -758,7 +984,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
// the relational builder, and unrelated .query() methods are common), // the relational builder, and unrelated .query() methods are common),
// so factory capability flags execute/unsafe on any receiver and query // so factory capability flags execute/unsafe on any receiver and query
// only on the db/client-shaped receiver backstop below. // only on the db/client-shaped receiver backstop below.
for (const m of code.matchAll(/\.(execute|unsafe)\s*\(/g)) { for (const m of code.matchAll(/\.\s*(execute|unsafe)\s*\(/g)) {
violations.push({ violations.push({
file: rel, file: rel,
prong: 'iii-raw-execution', prong: 'iii-raw-execution',
@@ -770,7 +996,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
// DI residual backstop: db/client-shaped receivers fire regardless of // DI residual backstop: db/client-shaped receivers fire regardless of
// detected capability (a handle can arrive by injection). // detected capability (a handle can arrive by injection).
for (const m of code.matchAll( for (const m of code.matchAll(
/\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\.(execute|query|unsafe)\s*\(/g, /\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\s*\.\s*(execute|query|unsafe)\s*\(/g,
)) { )) {
violations.push({ violations.push({
file: rel, file: rel,
@@ -793,10 +1019,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
else if (asMatch) sqlAliases.add(asMatch[1]!); else if (asMatch) sqlAliases.add(asMatch[1]!);
} }
} }
if (nsName) sqlAliases.add(`${nsName}\\.sql`); if (nsName) sqlAliases.add(`${nsName}\\s*\\.\\s*sql`);
} }
if (sqlAliases.size > 0) { if (sqlAliases.size > 0) {
const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\.raw\\s*\\(`); const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw\\s*\\(`);
if (rawRe.test(code)) { if (rawRe.test(code)) {
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' }); violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' });
} }
@@ -1035,6 +1261,101 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
name: 'E23 escaped-backtick-quoted table in SQL span', name: 'E23 escaped-backtick-quoted table in SQL span',
src: 'export const q = `DELETE FROM \\`hierarchy_grants\\` WHERE role = 1`;', src: 'export const q = `DELETE FROM \\`hierarchy_grants\\` WHERE role = 1`;',
}, },
// --- round-3 review shapes (G1G6) ---
{
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); }`,
},
]; ];
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
{ {