From 7dedc8d3c0eaa392ca64212118ce0f995e6f60cc Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 16:38:48 -0500 Subject: [PATCH] fix(db): close round-2 review evasions in writer-coverage assertion Per the second M4-1a detached review (REQUEST_CHANGES, R1-R8): - R1: prong (i) now tracks namespace destructuring, nested namespace re-exports (ns.schema.companies), and literal dynamic-import bindings (destructured and namespace) per contract rev 9. - R2: capability-conduit fixpoint (computeCapabilityConduits) closes driver/factory laundering through export-from modules. - R3: dynamic-import check is per call site with a full-literal tail match, so concatenated specifiers no longer pass. - R4: computed-member calls with literal verb names (obj['insert'], obj['query']) and spaced member access are flagged. - R5: prong (ii) adjacency tolerates schema qualification, interposed block comments, COPY, and escaped quotes. - R6/R8: header documents KNOWN RESIDUALS (DI receiver rename in capability-free files, computed non-literal member access, scan perimeter) with the measured counterfactuals. - createRequire fails outside a 5-module measured register (R1 route). - 15 new permanent evasion controls (E9-E23) with helper-file conduits run through the production analyzer. Calibration: tree-wide prong test green with zero new exclusions; tsc, eslint, and the full package suite pass. --- .../db/src/hierarchy-writer-coverage.test.ts | 393 ++++++++++++++++-- 1 file changed, 347 insertions(+), 46 deletions(-) diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index af973c62..ec1271df 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -28,11 +28,31 @@ * 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) and non-literal - * dynamic imports fail anywhere — allowlist and register included. This is - * deliberately stricter than the contract's minimum: an unanalyzable import - * or constructed code defeats every static prong, so there is no enumerated - * disposition path for them. + * Runtime code-construction primitives (eval, new Function) fail anywhere — + * allowlist and register included: constructed code defeats every static + * prong, so there is no enumerated disposition path for it. Unanalyzable + * import routes (a dynamic import whose specifier is not a single string + * literal, and createRequire) fail everywhere except their own enumerated + * disposition registers below. 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. + * + * 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. Constructing the verb at runtime is adjacent to eval and is + * expected to be caught in review. + * - The scan perimeter is //src for the three roots; production + * TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not + * scanned (verified free of db/driver/execute references at review time). * * The writer allowlist names hierarchy command/repository modules ONLY. It is * empty today: the hierarchy command family (M4-1b) has not landed, so no @@ -140,6 +160,18 @@ const MIGRATE_TIER_IMPORTERS: string[] = [ 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[] = [ @@ -435,51 +467,155 @@ interface ClassAliases { 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): 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 (CLASS_SYMBOLS.includes(original)) named.add(local); + } +} + +/** Parse a destructuring pattern ("a, b: c") into locals bound to class symbols. */ +function destructureBindings(pattern: string, named: Set): void { + for (const part of pattern.split(',')) { + const seg = part.trim(); + if (!seg) continue; + const m = /^(\w+)\s*(?::\s*(\w+))?$/.exec(seg); + if (m && CLASS_SYMBOLS.includes(m[1]!)) named.add(m[2] ?? m[1]!); + } +} + +function isSchemaSpecifier( + rel: string, + spec: string, + conduits: Set, + fileSet: Set, +): 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, fileSet: Set): ClassAliases { const named = new Set(); const namespaces = new Set(); for (const m of f.code.matchAll(IMPORT_RE)) { const [, typeOnly, , namedList, nsName, , spec] = m; if (typeOnly) continue; - const fromSchema = - spec === '@mosaicstack/db' || - (() => { - const r = resolveSpecifier(f.rel, spec!, fileSet); - return r !== null && conduits.has(r); - })(); - if (!fromSchema) continue; + if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; if (nsName) namespaces.add(nsName); - if (namedList) { - 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 (CLASS_SYMBOLS.includes(original)) named.add(local); - } + if (namedList) importBindings(namedList, named); + } + // 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; + if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue; + if (nsName) namespaces.add(nsName); + if (pattern) destructureBindings(pattern, named); + } + // Destructuring from a schema namespace binds class symbols to locals: + // `import * as s from '@mosaicstack/db'; const { companies } = s;` + for (const ns of [...namespaces]) { + for (const m of f.code.matchAll( + new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'), + )) { + destructureBindings(m[1]!, named); } } return { named: [...named], namespaces: [...namespaces] }; } +/** + * 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` — re-exports createDb/createPgliteDb from the db package or + * another factory conduit; importing from one while referencing a factory + * symbol confers factory capability. + */ +function computeCapabilityConduits( + files: FileFacts[], + fileSet: Set, +): { driver: Set; factory: Set } { + const driver = new Set(); + const factory = new Set(); + 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 isFactorySpec = (rel: string, spec: string): boolean => { + if (spec === '@mosaicstack/db') return true; + const r = resolveSpecifier(rel, spec, fileSet); + return r !== null && factory.has(r); + }; + 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; + } + if (!factory.has(f.rel) && isFactorySpec(f.rel, spec)) { + const named = m[2]; + if (named === undefined || /\b(createDb|createPgliteDb)\b/.test(named)) { + factory.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. + } + } + return { driver, factory }; +} + /** 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*['"]${packageName.replace('/', '\\/')}['"]\\s*\\)`).test(code); - return staticEdge || dynamicEdge; + const dynamicEdge = usesSymbol && new RegExp(`import\\s*\\(\\s*['"]${pkg}['"]\\s*\\)`).test(code); + // Namespace form: `import * as ns from ''` + `ns.` usage. + 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]}\\s*\\.\\s*(${symbols.join('|')})\\b`).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. // --------------------------------------------------------------------------- -function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): Violation[] { +interface AnalysisCtx { + fileSet: Set; + conduits: Set; // schema-symbol sources (prong i) + driverConduits: Set; // driver-capability re-exporters (prong iii) + factoryConduits: Set; // 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; @@ -491,9 +627,20 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(code)) { violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); } - // Non-literal dynamic import: makes the import graph unanalyzable. - if (!DYNAMIC_IMPORT_REGISTER.includes(rel) && /\bimport\s*\(\s*(?!['"])/.test(code)) { - violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); + // 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. + if (!DYNAMIC_IMPORT_REGISTER.includes(rel)) { + for (const m of code.matchAll(/\bimport\s*\(/g)) { + const tail = code.slice((m.index ?? 0) + m[0].length); + if (!/^\s*(['"])(?:[^'"\\]|\\.)*?\1\s*[,)]/.test(tail)) { + violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' }); + } + } + } + // createRequire: an unanalyzable CJS import route. + if (!CREATE_REQUIRE_REGISTER.includes(rel) && /\bcreateRequire\s*\(/.test(code)) { + violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); } const aliases = classAliases(f, conduits, fileSet); @@ -502,11 +649,13 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): if (!inAllowlist) { const targets: string[] = [...aliases.named]; for (const ns of aliases.namespaces) { - for (const s of CLASS_SYMBOLS) targets.push(`${ns}\\.${s}`); + // Allow intermediate property segments: ns.schema.companies (nested + // namespace re-exports) as well as ns.companies. + for (const s of CLASS_SYMBOLS) targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*${s}`); } if (targets.length > 0) { const writeRe = new RegExp( - `\\.(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`, + `\\.\\s*(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`, 'g', ); for (const m of code.matchAll(writeRe)) { @@ -519,13 +668,43 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): } } + // 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 = /\[\s*['"](insert|update|delete)['"]\s*\]\s*\(/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(/\[\s*['"](execute|query|unsafe)['"]\s*\]\s*\(/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 (optionally quoted): - // co-residence anywhere in one span over-matches prose (English "from" plus - // "pnpm workspaces" in an embedded doc string is not SQL). + // 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), 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)'; + const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/)+'; + const q = `(?:\\\\?["'\`])?`; const sqlAdjacentRe = new RegExp( - `\\b(insert\\s+into|update|delete\\s+from|from|join|truncate(\\s+table)?|alter\\s+table|drop\\s+table|references|into)\\s+["'\`]?(${CLASS_TABLES.join('|')})\\b`, + `\\b${kw}${gap}${q}(?:\\w+\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`, 'i', ); for (const span of f.spans) { @@ -541,15 +720,28 @@ function analyzeFile(f: FileFacts, conduits: Set, fileSet: Set): // Prong (iii): content-independent raw execution. if (!inAllowlist && !inRegister) { - const driverImport = new RegExp( + 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; + } + const driverImport = literalDriver || conduitDriver; + const usesFactorySymbol = /\b(createDb|createPgliteDb)\b/.test(code); const factoryImport = hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') || + (factoryConduitImport && usesFactorySymbol) || (aliases.namespaces.length > 0 && - new RegExp(`\\b(${aliases.namespaces.join('|')})\\.(createDb|createPgliteDb)\\b`).test( - code, - )); + usesFactorySymbol && + new RegExp( + `\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\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: @@ -624,6 +816,13 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { 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); @@ -638,6 +837,7 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { ...MIGRATE_TIER_IMPORTERS, ...DB_FACTORY_IMPORTERS, ...DYNAMIC_IMPORT_REGISTER, + ...CREATE_REQUIRE_REGISTER, ]) { expect(existsSync(join(REPO_ROOT, p)), `enumerated module missing: ${p}`).toBe(true); } @@ -650,7 +850,7 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { }); it('three-prong writer coverage holds over the production tree', () => { - const violations = files.flatMap((f) => analyzeFile(f, conduits, fileSet)); + const violations = files.flatMap((f) => analyzeFile(f, ctx)); expect( violations, violations.map((v) => `[prong ${v.prong}] ${v.file}: ${v.detail}`).join('\n'), @@ -711,7 +911,11 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { // 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 }> = [ + 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({}); }`, @@ -752,6 +956,85 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { 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 }) {}\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`;', + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { @@ -773,9 +1056,20 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { for (const e of EVASIONS) { const rel = 'packages/db/src/evasion-sample.ts'; const synthetic: FileFacts = { rel, ...lexSource(e.src) }; - const synthSet = new Set([...fileSet, rel, barrel.rel]); - const synthConduits = computeSchemaConduits([...files, barrel, synthetic], synthSet); - const v = analyzeFile(synthetic, synthConduits, synthSet); + 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, + }; + const v = analyzeFile(synthetic, synthCtx); expect(v.length, `evasion not caught: ${e.name}`).toBeGreaterThan(0); } }); @@ -784,9 +1078,16 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { 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 synthConduits = computeSchemaConduits([...files, synthetic], synthSet); - const v = analyzeFile(synthetic, synthConduits, synthSet); + 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('; ')}`,