feat(db): hierarchy record class schema + witnesses (contract 1, M4-1a) #1459

Merged
fred merged 10 commits from feat/m4-1a-hierarchy-schema into next 2026-08-28 00:42:36 +00:00
Showing only changes of commit 7dedc8d3c0 - Show all commits
+347 -46
View File
@@ -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 <root>/<pkg>/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<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 (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<string>): 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<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 namespaces = new Set<string>();
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<string>,
): { driver: Set<string>; factory: Set<string> } {
const driver = new Set<string>();
const factory = new 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 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 '<pkg>'` + `ns.<symbol>` 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<string>, fileSet: Set<string>): Violation[] {
interface AnalysisCtx {
fileSet: Set<string>;
conduits: Set<string>; // schema-symbol sources (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;
@@ -491,9 +627,20 @@ function analyzeFile(f: FileFacts, conduits: Set<string>, fileSet: Set<string>):
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<string>, fileSet: Set<string>):
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<string>, fileSet: Set<string>):
}
}
// 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<string>, fileSet: Set<string>):
// 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 (R1R5) ---
{
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`;',
},
];
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('; ')}`,