diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index bdfd738c..0c6e0bcd 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -82,15 +82,20 @@ * 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 a computed member — access or call, in any position — - * whose key is a text-only template literal (obj[`insert`](), - * ns[`companies`]) fails closed anywhere — template text never reaches - * the lexer's code output, so such a member is indistinguishable from a - * runtime-constructed one (a template key WITH interpolation is a - * non-literal computed member, above). Invoking a write/exec verb via - * `.apply`/`.call`/`.bind` likewise fails closed anywhere. Constructing - * the member at runtime is adjacent to eval and is expected to be caught - * in review. + * 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 key carrying expression dressing + * (`ns['companies' as const]`, `ns['companies'!]`), 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`, or through any + * `Reflect.*` call naming a verb, fails closed the same way. 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 @@ -115,6 +120,12 @@ * - 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). + * Files excluded from the scan — test files and out-of-src modules — are + * also 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; the 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) has not landed, so no @@ -247,6 +258,20 @@ const DB_FACTORY_IMPORTERS: string[] = [ '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']); @@ -506,7 +531,9 @@ const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`; * 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. + * 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*)*`; @@ -987,42 +1014,93 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { ) { violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' }); } - // 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 anywhere, 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', - }); - } - // 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. - // Fail-closed anywhere: no legitimate site invokes a builder verb this way - // (calibrated clean over the production tree). - 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', - }); + // 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 computed key carrying expression dressing + // (`ns['companies' as const]`, `ns['companies' satisfies 'companies']`, + // `ns['companies'!]`) keeps its static value while breaking every + // `['name']` matcher — the bracket alternatives require the closing + // quote to touch 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 `!(?!=)` 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')`. Any + // Reflect call whose argument text (up to the first `)`) names a verb + // fails closed — the tree has zero Reflect call sites (measured). + if ( + new RegExp( + `\\bReflect${DOT}[\\w$]+${CALL_OPEN}[^)\\n]*\\b(?:insert|update|delete|execute|query|unsafe|raw)\\b`, + ).test(code) + ) { + violations.push({ + file: rel, + prong: 'code-construction', + detail: 'Reflect verb indirection', + }); + } } // Dynamic import whose specifier is not a single string literal: the // import graph becomes unanalyzable. Checked per call site, so a literal @@ -2032,6 +2110,58 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E92 template-keyed receiver verb in capability-free file', src: `export class R { constructor(private pool: { execute(s: string): Promise }) {}\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 }) {}\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; await fn.call(db, 'DELETE FROM ' + t); }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ { @@ -2046,6 +2176,10 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'clean: interpolated template key is a non-literal computed member, not a text-only key', src: `export function g(o: Record void>, k: string) { o[\`\${k}\`](); }`, }, + { + name: 'clean: strict-inequality after a bracket member is not key dressing', + src: `export function h(o: Record) { return o['kind'] !== 'x'; }`, + }, ]; it('analyzer flags every known evasion form', () => {