From 42d3b9dce4eff1b79e97cc2ae2a25b7653196143 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 10:17:16 +0000 Subject: [PATCH 1/3] fix(devx): give the driver-conformance census a dialect axis The census scored `driver x case-set` and reported 45 covered cells before and after a change whose entire content was "this conformance suite went from executing on one dialect to executing on three". ADR-0053 D-A3 declares the matrix with a third axis -- `driver {SQLite, Postgres at minimum}` -- enforced only from inside a suite, by opt-in routing through live-dialect-matrix.testkit and by OS_EXPECT_LIVE_DIALECT_MATRIX=1. Both fire only for a file that already opted in, so a suite hard-coding `client: 'better-sqlite3'` was invisible to every gate in the repo while counting as a covered cell. Measured cost, not hypothetical: sql-driver-aggregation-conformance.test.ts was green on main and on every PR; on live PG 16.13 sum/avg/min/max over a boolean column threw SQLSTATE 42883. DIALECTED is the new invariant. It asserts that a conformance suite SAYS which dialects it runs on -- not that the answer is good enough, which is a different decision. The population is defined by the presence of a DECLARATION, never by the absence of a client literal, so moving the config into a helper cannot respell a suite out of the gate's reach. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o --- scripts/check-driver-conformance.mjs | 734 ++++++++++++++++++++++++++- 1 file changed, 717 insertions(+), 17 deletions(-) diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index 0f8d16bb29..172821edab 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -499,6 +499,130 @@ const CASE_SETS = [ const LEDGER = []; +// ── The DIALECT axis (ADR-0053 D-A3) ──────────────────────────────────────── +// +// Everything above scores `driver x case-set`. That is two axes of a matrix the +// ADR declares with three: D-A3 states the matrix is +// `driver {SQLite, Postgres at minimum}` x case-set. The third axis was enforced +// only from INSIDE a suite — by routing through +// `driver-sql/src/live-dialect-matrix.testkit.ts`, and by +// `OS_EXPECT_LIVE_DIALECT_MATRIX=1` turning an unprovisioned cell into a named +// red. Both are OPT-IN: they fire only for a file that already opted in, so a +// suite that hard-codes `client: 'better-sqlite3'` was invisible to every gate +// in the repo, counted here as a covered cell, with nothing anywhere saying it +// measured one dialect of three. +// +// ## The cost is measured, not hypothetical (#11456) +// +// `sql-driver-aggregation-conformance.test.ts` hard-coded SQLite. On `main` and +// on every PR it was green. Converted to the matrix and measured on live +// PG 16.13, `sum`/`avg`/`min`/`max` over a boolean column threw SQLSTATE 42883. +// The suite's name said conformance; its coverage said SQLite. This gate could +// not tell the difference, and its headline number said 45 either way. +// +// ## What this axis asserts, and what it deliberately does NOT +// +// It asserts that a conformance suite SAYS which dialects it runs on. It does +// not decide whether that answer is good enough — "deliberately single-dialect" +// and "accidentally single-dialect" were spelled identically before this, and +// making them distinguishable is the whole of the fix. +// `sql-driver-11321-sqlite-audit-default-canonical.test.ts` is about SQLite BY +// NAME; a gate that read a hard-coded client as a defect would be wrong about +// it, and about most of its neighbours. Measured on this tree: 105 files under +// `driver-sql/src` carry a literal `client: 'better-sqlite3'`. Those are not +// 105 defects and this gate does not treat them as any. It scores only the +// files that make a cell of THIS census covered — the suites whose coverage +// this script's own headline is asserting. +// +// Promoting "every dialect-scored cell has at least one matrix-routed suite" +// from a printed measurement to an invariant is the follow-up the numbers below +// size, not something to infer here: today exactly one cell would fail it, and +// converting that suite is a change to what the SUITE asserts, not to what the +// census can see. +// +// ## Declared, not detected — so it cannot be respelled around +// +// The population is defined by the PRESENCE OF A DECLARATION, never by the +// absence of a client literal. A detector keyed on `client: '...'` sees only the +// spellings it knows: move the config into a local helper, or behind a +// `beforeAll`, and the literal is gone while the coverage is exactly as narrow. +// So a suite passes by NAMING a stance symbol from its driver's dialect-matrix +// testkit, and nothing else counts. + +/** + * Naming one of these means the suite iterates the whole cell list — every + * dialect the driver speaks, with the unprovisioned ones reported rather than + * omitted. This is the D-A3 shape. + */ +const DIALECT_MATRIX_SYMBOLS = ['DIALECT_CELLS', 'LIVE_DIALECT_CELLS']; + +/** + * Naming one of these — and none of the above — means the suite is deliberately + * about NAMED cells: `dialectCell('sqlite')`, `PG_CELL`, `MYSQL_CELL`, or + * `declareDialectCell(MYSQL_CELL, …)`. + * + * `declareDialectCell` / `declareUnprovisionedCell` sit HERE rather than with + * the matrix symbols, and the precedence matters: they are per-cell helpers, so + * `declareDialectCell(PG_CELL, …)` is one deliberate cell and not a matrix. A + * file that iterates the list imports `DIALECT_CELLS` too, which is what + * promotes it — measured on this tree, all five matrix-routed suites do. + */ +const DIALECT_CELL_SYMBOLS = ['dialectCell', 'PG_CELL', 'MYSQL_CELL', 'declareDialectCell', 'declareUnprovisionedCell']; + +/** The export that identifies a package's dialect-matrix testkit, found on disk. */ +const DIALECT_TESTKIT_EXPORT = 'DIALECT_CELLS'; + +// ── The dialect ledger ────────────────────────────────────────────────────── +// +// One entry per conformance suite that declares no stance. Same discipline as +// LEDGER above: every entry is MEASURED against `main`, and clearing one means +// giving the suite a stance and deleting the row in the same PR. RECONCILED runs +// in both directions, so a row for a suite that now declares — or that no longer +// covers a cell — is an error rather than residue. +// +// SEEDED, not empty, and the seeding is the first measurement of this axis +// rather than newly written looseness: these two rows are what the population +// WAS on the day the axis was added. Both are deliberately left for a follow-up +// card, because giving either one a stance is a claim about intent this gate's +// author cannot verify and the maintainer can: +// +// comparand-type FILTER_COMPARAND_TYPE_CASES is described in CASE_SETS as +// "the six accepted types compile EVERYWHERE". It is measured +// on one dialect, and it is the only dialect-scored cell with +// no matrix-routed suite at all. Marking it `dialectCell( +// 'sqlite')` would write down the opposite of its own +// case-set's claim. +// icontains FILTER_TEXT_CASES is the case-set whose answer is KNOWN to +// diverge by dialect — #6518 made case sensitivity per-dialect +// (`GLOB` on SQLite, `LIKE` on Postgres, `LIKE` over +// `CAST(… AS BINARY)` on MySQL). A sqlite-only suite over it +// is the shape most likely to be accidental, so "deliberate" +// is exactly the word that must not be guessed. The cell +// itself is matrix-covered by +// `sql-driver-text-case-conformance.test.ts`; this is a second +// suite over the same case-set. + +const DIALECT_LEDGER = [ + { + driver: 'driver-sql', + file: 'packages/drivers/driver-sql/src/sql-driver-comparand-type-conformance.test.ts', + why: 'Hard-codes the SQLite cell. Its case-set claims the accepted types "compile everywhere", ' + + 'and this is the only dialect-scored cell with no matrix-routed suite — so whether it should ' + + 'be marked single-cell or converted to the matrix is a decision, not a marking.', + issue: '#12014', + }, + { + driver: 'driver-sql', + file: 'packages/drivers/driver-sql/src/sql-driver-icontains-and-retired-operators.test.ts', + why: 'Hard-codes the SQLite cell over FILTER_TEXT_CASES, whose answer diverges by dialect ' + + '(#6518: GLOB / LIKE / CAST AS BINARY). The cell is matrix-covered by ' + + 'sql-driver-text-case-conformance.test.ts, so this is a second suite whose narrowness may ' + + 'be deliberate — that is the claim that must be made rather than assumed.', + issue: '#12014', + }, +]; + + // ── The ratchet-remedy authority convention (#8435) ───────────────────────── // // The rule above is the authority rule, and until now it was written only here @@ -535,6 +659,27 @@ const RATCHET_EXPANSION_OFFER = new RegExp( `add a measured DEBT/EXEMPT entry to the ledger in\\s+${LEDGER_REL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, ); +/** + * The same detector for the DIALECT ledger's offer. A second ledger is a second + * way to buy green, so it carries the same authority label — and gets its own + * detector rather than a widened one, so a rewording of either offer fails the + * self-test that names IT rather than silently disarming the other. + */ +const DIALECT_RATCHET_EXPANSION_OFFER = new RegExp( + `add a measured entry to the dialect ledger in\\s+${LEDGER_REL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, +); + +/** + * As {@link ratchetRemedyCarriesAuthority}, for the dialect ledger's offer. + * + * @param {string} message + * @returns {boolean} + */ +function dialectRemedyCarriesAuthority(message) { + if (!DIALECT_RATCHET_EXPANSION_OFFER.test(message)) return true; + return message.includes(RATCHET_AUTHORITY_MARKER); +} + /** * The convention: a message that hands the author the ledger-expanding path must * say in the same breath that the path is not theirs. A message offering no such @@ -568,6 +713,32 @@ function consumedMessage(driver, caseSet) { ); } +/** + * DIALECTED's text, named and pure for the same reason {@link consumedMessage} + * is: a message built inline is a message no assertion can reach. + * + * @param {string} driver + * @param {string} relFile repo-relative path of the undeclared suite + * @param {string[]} markers the case-set markers this suite makes covered + * @param {{specifier: string, cellIds: string[]}} kit the driver's dialect testkit + * @returns {string} + */ +function dialectedMessage(driver, relFile, markers, kit) { + return ( + `DIALECTED: ${driver}'s ${relFile} is what makes ${markers.join(', ')} a covered cell, but it ` + + `never says which of this driver's dialects (${kit.cellIds.join(', ')}) it runs on. ADR-0053 ` + + 'D-A3 declares the matrix as driver x {SQLite, Postgres at minimum}; a suite that names no ' + + `cell is counted here as covered while measuring an unknown fraction of that. Import a stance ` + + `from '${kit.specifier}' and use it: ${DIALECT_MATRIX_SYMBOLS[0]} to run every cell, or ` + + `${DIALECT_CELL_SYMBOLS[0]}('') / PG_CELL / MYSQL_CELL to say it is deliberately about ` + + 'named cells. That is the fix, and the only one of the two you can take on your own. ' + + `${RATCHET_AUTHORITY_MARKER}, NOT a co-equal option: add a measured entry to the dialect ` + + `ledger in ${LEDGER_REL} saying why the suite's dialect coverage cannot be stated. A ledger ` + + 'entry is a MEASURED, tracked exception the maintainer has agreed to, never the cheaper half ' + + 'of "say what this suite runs on" — do not take this path to get CI green.' + ); +} + // ── Discovery ─────────────────────────────────────────────────────────────── @@ -762,27 +933,206 @@ function walkTsInto(dir, out) { } /** - * Does this driver package drive `marker`? + * Is `symbol` imported from `specifier` AND referenced outside that import? + * + * The two-part shape is the one {@link consumes} has always used — an unused + * import is not coverage, and it is the shape a half-finished suite leaves + * behind. Extracted so the dialect axis asserts a stance the same way this + * script asserts a case-set, rather than inventing a second idiom next to it. * - * Requires BOTH an import naming it from `@objectstack/spec/data` and a - * reference outside that import — an unused import is not coverage, and it is - * the shape a half-finished suite leaves behind. + * @param {string} src file text — pass it through {@link stripCommentsTs} when a + * comment mentioning the symbol must not count. (`consumes` deliberately does + * not, so its verdicts stay byte-identical to what they were.) + */ +function drivenFrom(src, symbol, specifier) { + if (!src.includes(symbol)) return false; + const quoted = specifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const imported = new RegExp( + `import[\\s\\S]*?\\b${symbol}\\b[\\s\\S]*?from\\s+['"]${quoted}['"]`, + ).test(src); + if (!imported) return false; + const withoutImports = src.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?/g, ''); + return new RegExp(`\\b${symbol}\\b`).test(withoutImports); +} + +/** + * EVERY file under this driver's `src/` that drives `marker`, in walk order. + * + * `consumes` answers "is this cell covered" and needs only the first. The + * dialect axis needs them ALL: a cell can be covered by two suites with + * different dialect stances, and scoring only the first would let an + * undeclared one hide behind a matrix-routed sibling — which is exactly the + * arrangement FILTER_TEXT_CASES is in on this tree. + */ +function coveringFiles(driverDir, marker) { + const hits = []; + for (const file of walkTs(join(driverDir, 'src'))) { + if (drivenFrom(readFileSync(file, 'utf8'), marker, '@objectstack/spec/data')) hits.push(file); + } + return hits; +} + +/** + * Does this driver package drive `marker`? The covering file, or `null`. + * + * Delegates to {@link coveringFiles} so there is one detector rather than two + * that can drift; the walk order is unchanged, so the file this returns is the + * file it always returned. */ function consumes(driverDir, marker) { + return coveringFiles(driverDir, marker)[0] ?? null; +} + +// ── The dialect axis: detectors ───────────────────────────────────────────── + +/** Punctuation after which a `/` starts a regex literal rather than a division. */ +const REGEX_PREV_PUNCT = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '~', '^', '<', '>']); +/** ...and the keywords after which the same is true (`return /re/.test(x)`). */ +const REGEX_PREV_KEYWORD = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'case', 'do', 'else', 'yield', 'await', 'throw']); + +/** + * `src` with its comments removed, strings and regex literals left intact. + * + * ## Why this is load-bearing rather than tidiness + * + * The dialect stance is read off the source text, so a MENTION must not count as + * a declaration — and on this tree the mentions are everywhere, because the + * files that were FIXED describe the fix in prose: + * + * sql-driver-aggregation-conformance.test.ts the #11456 conversion. Its head + * note says "It used to construct `client: 'better-sqlite3'` as a literal", + * so a detector grepping for the literal flags the one file that was + * repaired. + * sql-driver.ts mentions `FILTER_LOGIC_CASES` and `client: 'postgres'` in + * comments only. Measured: 5 of this driver's 8 covering files change their + * client-literal answer between raw and stripped text. + * + * Strings stay because a stripper that ate them would have to be right about + * `'https://…'`; regex literals stay because a `/` mistaken for a comment start + * would swallow the rest of a line — and a swallowed line is a MISSING stance, + * i.e. a false red. Both directions are pinned in the self-test. + */ +function stripCommentsTs(src) { + let out = ''; + let i = 0; + const n = src.length; + let prevPunct = ''; + let prevWord = ''; + let prevWasWord = false; + const regexAllowed = () => (prevWasWord ? REGEX_PREV_KEYWORD.has(prevWord) : REGEX_PREV_PUNCT.has(prevPunct)); + while (i < n) { + const c = src[i]; + const d = src[i + 1]; + if (c === '/' && d === '/') { + while (i < n && src[i] !== '\n') i++; + continue; + } + if (c === '/' && d === '*') { + i += 2; + // Newlines are kept so line numbers survive for anything that reports them. + while (i < n && !(src[i] === '*' && src[i + 1] === '/')) { + if (src[i] === '\n') out += '\n'; + i++; + } + i += 2; + out += ' '; + prevPunct = ' '; + prevWasWord = false; + continue; + } + if (c === '"' || c === "'" || c === '`') { + out += c; + i++; + while (i < n) { + const s = src[i]; + if (s === '\\') { out += s + (src[i + 1] ?? ''); i += 2; continue; } + out += s; + i++; + if (s === c) break; + } + prevPunct = c; + prevWasWord = false; + continue; + } + if (c === '/' && regexAllowed()) { + out += c; + i++; + let inClass = false; + while (i < n) { + const s = src[i]; + if (s === '\\') { out += s + (src[i + 1] ?? ''); i += 2; continue; } + out += s; + i++; + if (s === '\n') break; // unterminated: it was a division after all + if (s === '[') inClass = true; + else if (s === ']') inClass = false; + else if (s === '/' && !inClass) break; + } + prevPunct = '/'; + prevWasWord = false; + continue; + } + out += c; + if (/[A-Za-z0-9_$]/.test(c)) { + prevWord = prevWasWord ? prevWord + c : c; + prevWasWord = true; + } else if (!/\s/.test(c)) { + prevPunct = c; + prevWasWord = false; + } + i++; + } + return out; +} + +/** + * This driver's dialect-matrix testkit, or `null` when the package has none. + * + * Discovered from disk by its `DIALECT_CELLS` export — never a hardcoded package + * name, for the reason both other axes are read off disk: a list written here is + * a list that goes stale the day a second driver grows a dialect matrix, and it + * goes stale SILENTLY (one fewer scored package, still green). + * + * A package with no such testkit is single-backend — `driver-memory`, + * `driver-mongodb`, `driver-sqlite-wasm` and `driver-turso` all are — and has no + * dialect axis to score. That is reported as a count rather than skipped in + * silence. + * + * @returns {{file: string, specifier: string, cellIds: string[]} | null} + */ +function discoverDialectTestkit(driverDir) { for (const file of walkTs(join(driverDir, 'src'))) { const src = readFileSync(file, 'utf8'); - if (!src.includes(marker)) continue; - const imported = new RegExp( - `import[\\s\\S]*?\\b${marker}\\b[\\s\\S]*?from\\s+['"]@objectstack/spec/data['"]`, - ).test(src); - if (!imported) continue; - // Count references outside the import statement(s). - const withoutImports = src.replace(/import[\s\S]*?from\s+['"][^'"]+['"];?/g, ''); - if (new RegExp(`\\b${marker}\\b`).test(withoutImports)) return file; + if (!new RegExp(`^export const ${DIALECT_TESTKIT_EXPORT}\\b`, 'm').test(src)) continue; + const base = file.slice(driverDir.length + 1).replace(/\\/g, '/').replace(/^src\//, ''); + // The ids come from the array BODY, not the whole file: `id:` is a common + // enough key that a file-wide scan would collect unrelated ones and report a + // dialect list the driver does not speak. Bounded by the array's own + // terminator, so a second `id:`-bearing export below it cannot leak in. + const body = stripCommentsTs(src).split(new RegExp(`^export const ${DIALECT_TESTKIT_EXPORT}\\b`, 'm'))[1] ?? ''; + const cellIds = [...(body.split(/^\]/m)[0] ?? '').matchAll(/\bid:\s*'([a-z0-9_-]+)'/g)].map((m) => m[1]); + return { file, specifier: `./${base.replace(/\.ts$/, '.js')}`, cellIds }; } return null; } +/** + * What does this suite SAY it runs on? `matrix`, `cell`, or `undeclared`. + * + * Read from comment-stripped text, and by the same import-plus-reference rule + * `consumes` uses — a stance mentioned in a comment, or imported and never used, + * is not a stance. + * + * @param {string} src file text (raw; stripped here) + * @param {string} specifier the testkit's module specifier for this package + */ +function dialectStance(src, specifier) { + const clean = stripCommentsTs(src); + if (DIALECT_MATRIX_SYMBOLS.some((s) => drivenFrom(clean, s, specifier))) return 'matrix'; + if (DIALECT_CELL_SYMBOLS.some((s) => drivenFrom(clean, s, specifier))) return 'cell'; + return 'undeclared'; +} + // ── The run ───────────────────────────────────────────────────────────────── function audit() { @@ -821,10 +1171,19 @@ function audit() { // CONSUMED + collect the matrix. const ledgerHit = new Set(); + /** driver -> (covering file -> the markers it covers), for the dialect axis. */ + const coveringByDriver = new Map(); for (const driver of drivers) { const dir = join(DRIVERS_DIR, driver); for (const c of CASE_SETS) { - const where = consumes(dir, c.marker); + const covering = coveringFiles(dir, c.marker); + const where = covering[0] ?? null; + for (const f of covering) { + if (!coveringByDriver.has(driver)) coveringByDriver.set(driver, new Map()); + const perFile = coveringByDriver.get(driver); + if (!perFile.has(f)) perFile.set(f, []); + perFile.get(f).push(c.marker); + } const entry = LEDGER.find((l) => l.driver === driver && l.marker === c.marker); if (entry) ledgerHit.add(`${driver}::${c.marker}`); @@ -854,7 +1213,87 @@ function audit() { } } - return { drivers, rows, errors }; + // DIALECTED — the third axis. Runs last because it scores the covering files + // the CONSUMED pass just collected. + const dialect = dialectAudit(drivers, coveringByDriver, errors); + + return { drivers, rows, errors, dialect }; +} + +/** + * DIALECTED: every conformance suite in a dialect-capable driver declares which + * dialects it runs on, or carries a measured dialect-ledger entry. + * + * Judged PER FILE rather than per cell, deliberately. A cell can be covered by + * two suites, and scoring the cell would let an undeclared suite hide behind a + * matrix-routed sibling — the arrangement FILTER_TEXT_CASES is in on this tree, + * and the one that lets the population accrete while the census stays green. + * + * @param {string[]} drivers + * @param {Map>} coveringByDriver + * @param {string[]} errors appended to in place, as the other invariants do + * @param {{driversDir?: string, root?: string, ledger?: typeof DIALECT_LEDGER}} [over] + * parameterised for the same reason `discoverDrivers` is: so the self-test + * drives THIS function over a synthetic tree rather than a re-implementation + * of it, which is the only kind of self-test a refactor cannot neuter. + */ +function dialectAudit(drivers, coveringByDriver, errors, over = {}) { + const driversDir = over.driversDir ?? DRIVERS_DIR; + const root = over.root ?? ROOT; + const ledger = over.ledger ?? DIALECT_LEDGER; + const scored = []; + const singleBackend = []; + const notExecutable = []; + const kits = new Map(); + const ledgerHit = new Set(); + + for (const driver of drivers) { + const kit = discoverDialectTestkit(join(driversDir, driver)); + if (!kit) { + singleBackend.push(driver); + continue; + } + kits.set(driver, kit); + for (const [file, markers] of coveringByDriver.get(driver) ?? new Map()) { + const rel = file.slice(root.length + 1); + // Only a test file executes, so only a test file has a dialect to declare. + // Counted and named in the report rather than filtered away in silence. + if (!/\.test\.tsx?$/.test(file)) { + notExecutable.push(rel); + continue; + } + const stance = dialectStance(readFileSync(file, 'utf8'), kit.specifier); + const entry = ledger.find((l) => l.driver === driver && l.file === rel); + if (entry) ledgerHit.add(rel); + if (stance === 'undeclared' && !entry) { + errors.push(dialectedMessage(driver, rel, markers, kit)); + } else if (stance !== 'undeclared' && entry) { + errors.push( + `RECONCILED: ${rel} now declares a dialect stance (${stance}), but the dialect ledger ` + + 'still carries an entry for it. Delete the entry.', + ); + } + scored.push({ driver, file: rel, markers, stance: entry && stance === 'undeclared' ? 'ledger' : stance }); + } + } + + // RECONCILED, the reverse direction — a dialect-ledger row must still point at + // a conformance suite in a dialect-capable driver. + for (const entry of ledger) { + if (!kits.has(entry.driver)) { + errors.push( + `RECONCILED: dialect-ledger entry for ${entry.driver}, which is not a driver package with ` + + 'a dialect matrix.', + ); + } else if (!ledgerHit.has(entry.file)) { + errors.push( + `RECONCILED: dialect-ledger entry for ${entry.file}, which no longer covers a case-set ` + + 'cell (moved, renamed, deleted, or its case-set import is gone). Delete the entry.', + ); + } + } + + return { scored, singleBackend, kits, notExecutable }; } function reportDeadRoots(err) { @@ -880,7 +1319,7 @@ function report() { process.exit(1); return; } - const { drivers, rows, errors } = audited; + const { drivers, rows, errors, dialect } = audited; const covered = rows.filter((r) => r.state === 'covered').length; const debt = rows.filter((r) => r.state === 'debt').length; @@ -901,6 +1340,40 @@ function report() { } console.log(''); + // ── The dialect axis ────────────────────────────────────────────────────── + // Printed BEFORE the error block so a red run still shows the measurement: + // the number this axis exists to make visible is most wanted on the run where + // something is wrong. + const scoredCells = new Set(); + const matrixCells = new Set(); + for (const s of dialect.scored) { + for (const m of s.markers) { + scoredCells.add(`${s.driver}::${m}`); + if (s.stance === 'matrix') matrixCells.add(`${s.driver}::${m}`); + } + } + const singleBackendCells = rows.filter( + (r) => r.state === 'covered' && dialect.singleBackend.includes(r.driver), + ).length; + + for (const [driver, kit] of dialect.kits) { + console.log(`dialect axis (ADR-0053 D-A3) — ${driver} speaks {${kit.cellIds.join(', ')}}\n`); + const suites = dialect.scored.filter((s) => s.driver === driver); + const w = Math.max(...suites.map((s) => s.file.split('/').pop().length), 5); + for (const s of suites.sort((a, b) => a.file.localeCompare(b.file))) { + const glyph = { matrix: 'matrix', cell: 'cell ', ledger: 'LEDGER', undeclared: 'UNDECLARED' }[s.stance]; + console.log( + ` ${glyph} ${s.file.split('/').pop().padEnd(w)} ${s.markers.map((m) => m.replace(/_CASES$/, '')).join(', ')}`, + ); + } + console.log(''); + } + for (const rel of dialect.notExecutable) { + console.log(` note ${rel} covers a cell but is not a test file — nothing executes, so it`); + console.log(' carries no dialect stance and is not scored on this axis.'); + } + if (dialect.notExecutable.length) console.log(''); + if (errors.length) { for (const e of errors) console.error(` x ${e}`); console.error(`\ncheck-driver-conformance: ${errors.length} problem(s).\n`); @@ -917,9 +1390,29 @@ function report() { } if (LEDGER.length) console.log(''); + for (const entry of DIALECT_LEDGER) { + console.log(` DIALECT-DEBT ${entry.driver} × ${entry.file.split('/').pop()}`); + console.log(` ${entry.why}`); + if (entry.issue) console.log(` tracked: ${entry.issue}`); + } + if (DIALECT_LEDGER.length) console.log(''); + console.log( `check-driver-conformance: OK — ${covered} covered cell(s), ${debt} in the DEBT ledger, ` - + `${exempt} exempt.\n`, + + `${exempt} exempt.`, + ); + // The second line is the one #12014 is about: the first says 45 whether a + // suite ran on one dialect or three, and said 45 before and after #11456 + // converted one from the former to the latter. + console.log( + `check-driver-conformance: dialect axis — ${dialect.scored.length} conformance suite(s) across ` + + `${dialect.kits.size} dialect-capable driver(s): ` + + `${dialect.scored.filter((s) => s.stance === 'matrix').length} run the matrix, ` + + `${dialect.scored.filter((s) => s.stance === 'cell').length} declare named cell(s), ` + + `${dialect.scored.filter((s) => s.stance === 'ledger').length} in the DIALECT ledger. ` + + `${matrixCells.size} of ${scoredCells.size} dialect-scored cell(s) have a matrix-routed ` + + `suite. ${singleBackendCells} covered cell(s) belong to ${dialect.singleBackend.length} ` + + 'single-backend driver(s), which have no dialect axis.\n', ); } @@ -1166,6 +1659,208 @@ function selfTest() { expect('the declared form is NOT a scan-root value', !ROOT_DIR_WATCH_HINTS.some((h) => h === driversRel || h === DRIVERS_DIR)); + // ── The DIALECT axis (#12014) ────────────────────────────────────────────── + // + // The axis is a source-text reading, so every way a reading can be wrong gets + // a named assertion: the stripper (a mention must not count), the stance + // classifier (both directions, plus the precedence), the testkit discovery, + // and the invariant itself red-then-green over a synthetic tree. + + // -- stripCommentsTs: comments go, code stays. -- + // The false-GREEN direction: a comment must not be able to declare anything. + expect('a line comment is removed', !stripCommentsTs('const a = 1; // DIALECT_CELLS\n').includes('DIALECT_CELLS')); + expect('a block comment is removed', !stripCommentsTs('/* DIALECT_CELLS */ const a = 1;\n').includes('DIALECT_CELLS')); + // The false-RED direction: eating code would hide a real stance. Each of these + // is a shape a naive `//`-scanner gets wrong, and getting it wrong here costs a + // suite that DOES declare its stance a red it cannot act on. + expect('a `//` inside a string is not a comment', + stripCommentsTs("const u = 'https://example.test/x'; const KEEP = 1;\n").includes('KEEP')); + expect('a `//` inside a regex literal is not a comment', + stripCommentsTs('const r = /a\\/\\/b/; const KEEP = 1;\n').includes('KEEP')); + expect('a regex after `return` is a regex, not a division (its quotes must not open a string)', + stripCommentsTs('function f(s) { return /[\'"]/.test(s); }\nconst KEEP = 1;\n').includes('KEEP')); + expect('a real division is still a division, and the comment after it still goes', + (() => { + const out = stripCommentsTs('const q = a / b; // DIALECT_CELLS\nconst KEEP = 1;\n'); + return out.includes('KEEP') && !out.includes('DIALECT_CELLS'); + })()); + + // -- dialectStance: what a suite SAYS it runs on. -- + const KIT_SPEC = './kit.testkit.js'; + expect('iterating the cell list is the matrix stance', + dialectStance("import { DIALECT_CELLS } from './kit.testkit.js';\nfor (const c of DIALECT_CELLS) {}\n", KIT_SPEC) === 'matrix'); + expect('naming one cell is the cell stance', + dialectStance("import { dialectCell } from './kit.testkit.js';\nconst c = dialectCell('sqlite');\n", KIT_SPEC) === 'cell'); + // The precedence, which is the whole reason the two symbol lists are split: + // `declareDialectCell` is a PER-CELL helper, so using it on one named cell is + // one deliberate cell and must not be read as a matrix. + expect('declareDialectCell on a named cell is one cell, NOT a matrix', + dialectStance("import { declareDialectCell, PG_CELL } from './kit.testkit.js';\ndeclareDialectCell(PG_CELL, 'm', () => {});\n", KIT_SPEC) === 'cell'); + expect('a hard-coded client declares nothing', + dialectStance("const d = new SqlDriver({ client: 'better-sqlite3' });\n", KIT_SPEC) === 'undeclared'); + expect('an imported but unused stance is not a stance (the `consumes` rule, applied here)', + dialectStance("import { DIALECT_CELLS } from './kit.testkit.js';\nconst x = 1;\n", KIT_SPEC) === 'undeclared'); + + { + // THE non-vacuity assertion for the stripper. This fixture is the real shape + // on this tree: `sql-driver-aggregation-conformance.test.ts` describes its + // own #11456 conversion in prose, so a detector reading raw text finds a + // whole import statement inside a comment. Without stripping this reads as + // `matrix` — the file would be scored by its head note rather than its code. + const proseOnly = + '/*\n * Converted by #11456. It now does\n * import { DIALECT_CELLS } from ' + + "'./kit.testkit.js';\n * and iterates DIALECT_CELLS for every dialect.\n */\n" + + "const driver = new SqlDriver({ client: 'better-sqlite3' });\n"; + // if/else for the #8435 reason: a fixture that stopped being recognisable as + // a stance in RAW text could no longer test the stripper at all, and that + // failure must name itself rather than masquerading as a passing check. + if (!DIALECT_MATRIX_SYMBOLS.some((s) => drivenFrom(proseOnly, s, KIT_SPEC))) { + expect('#12014 — the prose-only fixture is no longer a stance even UNSTRIPPED, so it cannot ' + + 'test the stripper. Re-spell it so raw text reads as `matrix`', false); + } else { + expect('#12014 — a stance that appears ONLY in a comment is not a stance (proves the ' + + 'stripper is load-bearing, not decoration)', + dialectStance(proseOnly, KIT_SPEC) === 'undeclared'); + } + } + + // -- discoverDialectTestkit + dialectAudit, over a synthetic tree. -- + const tmpDialect = join(ROOT, 'node_modules', '.check-driver-conformance-selftest-dialect'); + try { + const dsrc = (d) => join(tmpDialect, d, 'src'); + mkdirSync(dsrc('driver-x'), { recursive: true }); + writeFileSync(join(tmpDialect, 'driver-x', 'package.json'), '{}\n'); + writeFileSync( + join(dsrc('driver-x'), 'kit.testkit.ts'), + "export const DIALECT_CELLS = [\n { id: 'sqlite' },\n { id: 'pg' },\n] as const;\n", + ); + const undeclaredFile = join(dsrc('driver-x'), 'a.test.ts'); + const declaredFile = join(dsrc('driver-x'), 'b.test.ts'); + writeFileSync(undeclaredFile, "const d = new SqlDriver({ client: 'better-sqlite3' });\n"); + writeFileSync(declaredFile, "import { DIALECT_CELLS } from './kit.testkit.js';\nfor (const c of DIALECT_CELLS) {}\n"); + mkdirSync(dsrc('driver-y'), { recursive: true }); // no testkit: single-backend + writeFileSync(join(tmpDialect, 'driver-y', 'package.json'), '{}\n'); + writeFileSync(join(dsrc('driver-y'), 'c.test.ts'), 'const x = 1;\n'); + + const kit = discoverDialectTestkit(join(tmpDialect, 'driver-x')); + expect('a package with a DIALECT_CELLS export is dialect-capable', kit !== null); + expect('the specifier is derived from the file on disk, not spelled here', kit?.specifier === './kit.testkit.js'); + expect('the cell ids are read off the testkit', kit?.cellIds.join(',') === 'sqlite,pg'); + expect('a package without one is not', discoverDialectTestkit(join(tmpDialect, 'driver-y')) === null); + + const coveringOf = (files) => new Map([ + ['driver-x', new Map(files)], + ['driver-y', new Map([[join(dsrc('driver-y'), 'c.test.ts'), ['PAGINATION_CASES']]])], + ]); + const drive = (files, ledger) => { + const errs = []; + const out = dialectAudit(['driver-x', 'driver-y'], coveringOf(files), errs, { + driversDir: tmpDialect, root: tmpDialect, ledger, + }); + return { errs, out }; + }; + + // RED: an undeclared conformance suite, with an empty ledger. + const red = drive([[undeclaredFile, ['PAGINATION_CASES']]], []); + expect('an undeclared conformance suite is an error', red.errs.length === 1); + expect('the error names the suite', /a\.test\.ts/.test(red.errs[0] ?? '')); + expect('the error names the dialects it could have declared', /sqlite, pg/.test(red.errs[0] ?? '')); + + // GREEN: the same suite, declared. Same tree, same call — so the red above + // was caused by the missing stance and nothing else. + const green = drive([[declaredFile, ['PAGINATION_CASES']]], []); + expect('declaring a stance clears it', green.errs.length === 0); + expect('and the stance is recorded as matrix', green.out.scored[0]?.stance === 'matrix'); + + // GREEN by ledger — and REPORTED as ledgered rather than as covered-and-fine. + const ledgered = drive([[undeclaredFile, ['PAGINATION_CASES']]], + [{ driver: 'driver-x', file: 'driver-x/src/a.test.ts', why: 'measured', issue: '#0' }]); + expect('a ledger entry accounts for an undeclared suite', ledgered.errs.length === 0); + expect('and it is REPORTED as ledgered, never as declared', ledgered.out.scored[0]?.stance === 'ledger'); + + // RECONCILED, all three directions. + const stale = drive([[declaredFile, ['PAGINATION_CASES']]], + [{ driver: 'driver-x', file: 'driver-x/src/b.test.ts', why: 'measured', issue: '#0' }]); + expect('a ledger entry for a suite that now declares is an error', + stale.errs.length === 1 && /now declares a dialect stance/.test(stale.errs[0])); + const orphan = drive([[declaredFile, ['PAGINATION_CASES']]], + [{ driver: 'driver-x', file: 'driver-x/src/gone.test.ts', why: 'measured', issue: '#0' }]); + expect('a ledger entry for a suite that covers nothing is an error', + orphan.errs.length === 1 && /no longer covers a case-set cell/.test(orphan.errs[0])); + const wrongDriver = drive([[declaredFile, ['PAGINATION_CASES']]], + [{ driver: 'driver-y', file: 'driver-y/src/c.test.ts', why: 'measured', issue: '#0' }]); + expect('a ledger entry for a driver with no dialect matrix is an error', + wrongDriver.errs.length === 1 && /not a driver package with a dialect matrix/.test(wrongDriver.errs[0])); + + // A single-backend package is REPORTED, not silently dropped. + expect('a package with no dialect matrix is counted as single-backend', + green.out.singleBackend.join(',') === 'driver-y'); + expect('and its covering files are not scored on this axis', + green.out.scored.every((s) => s.driver === 'driver-x')); + + // A non-test covering file carries no stance, and is named rather than dropped. + const implFile = join(dsrc('driver-x'), 'impl.ts'); + writeFileSync(implFile, '// mentions PAGINATION_CASES\n'); + const nonTest = drive([[implFile, ['PAGINATION_CASES']]], []); + expect('a non-test covering file raises no stance error', nonTest.errs.length === 0); + expect('and is reported by name rather than filtered away in silence', + nonTest.out.notExecutable.join(',') === 'driver-x/src/impl.ts'); + } finally { + rmSync(tmpDialect, { recursive: true, force: true }); + } + + // -- coveringFiles returns ALL of them, which is what the axis needs. -- + const tmpMulti = join(ROOT, 'node_modules', '.check-driver-conformance-selftest-covering'); + try { + mkdirSync(join(tmpMulti, 'src'), { recursive: true }); + const drivenSrc = "import { PAGINATION_CASES } from '@objectstack/spec/data';\nfor (const c of PAGINATION_CASES) {}\n"; + writeFileSync(join(tmpMulti, 'src', 'a.test.ts'), drivenSrc); + writeFileSync(join(tmpMulti, 'src', 'b.test.ts'), drivenSrc); + expect('coveringFiles returns every suite over a case-set, not just the first (an undeclared ' + + 'suite must not be able to hide behind a declared sibling)', + coveringFiles(tmpMulti, 'PAGINATION_CASES').length === 2); + expect('consumes still answers with the first of them, unchanged', + consumes(tmpMulti, 'PAGINATION_CASES') === coveringFiles(tmpMulti, 'PAGINATION_CASES')[0]); + } finally { + rmSync(tmpMulti, { recursive: true, force: true }); + } + + // -- The #8435 authority convention, for the dialect ledger's own offer. -- + const dialected = dialectedMessage( + 'driver-example', 'packages/drivers/driver-example/src/a.test.ts', ['PAGINATION_CASES'], + { specifier: './kit.testkit.js', cellIds: ['sqlite', 'pg'] }, + ); + expect('#12014 — the dialect ratchet-offer DETECTOR still matches DIALECTED (else the check ' + + 'below is vacuous)', + DIALECT_RATCHET_EXPANSION_OFFER.test(dialected)); + expect(`#12014 — DIALECTED marks the dialect-ledger path ${RATCHET_AUTHORITY_MARKER} (a second ` + + 'ledger is a second way to buy green, so it carries the same authority as the first)', + dialectRemedyCarriesAuthority(dialected)); + { + const unmarkedDialectOffer = + 'DIALECTED: driver-example/src/a.test.ts never says which dialects it runs on. Route it ' + + `through the matrix, or add a measured entry to the dialect ledger in ${LEDGER_REL} saying ` + + 'why not.'; + if (!DIALECT_RATCHET_EXPANSION_OFFER.test(unmarkedDialectOffer)) { + expect('#12014 — the synthetic unmarked dialect-offer fixture is no longer recognised as an ' + + 'offer, so it cannot test discrimination. Re-spell it to match ' + + 'DIALECT_RATCHET_EXPANSION_OFFER', false); + } else { + expect('#12014 — dialectRemedyCarriesAuthority() REJECTS an offer carrying no marker', + !dialectRemedyCarriesAuthority(unmarkedDialectOffer)); + } + } + + // -- The real tree: the axis is WIRED IN, not merely defined. -- + const liveKit = discoverDialectTestkit(join(DRIVERS_DIR, 'driver-sql')); + expect('driver-sql is discovered as dialect-capable from disk', liveKit !== null); + expect('and D-A3\'s two minimum dialects are both cells of it ("SQLite, Postgres at minimum")', + ['sqlite', 'pg'].every((id) => liveKit?.cellIds.includes(id))); + expect('every row of the dialect ledger points at a file that exists', + DIALECT_LEDGER.every((e) => { + try { return statSync(join(ROOT, e.file)).isFile(); } catch { return false; } + })); + if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-driver-conformance --self-test: ${failures.length} failure(s).\n`); @@ -1176,7 +1871,12 @@ function selfTest() { + 'every entry under DRIVERS_DIR (a dropped or manifestless row is red, not a smaller matrix), ' + 'holds the dead-root hard error (red when a scan root is renamed, green when restored), and ' + 'keeps CONSUMED\'s ledger offer marked maintainer-only (#8435). It also declares the driver ' - + 'subtree dispatch-gates derives from, refusing the bare root and the case-set dir by name.', + + 'subtree dispatch-gates derives from, refusing the bare root and the case-set dir by name. ' + + 'And it holds the DIALECT axis (#12014): comments cannot declare a stance while strings and ' + + 'regexes survive stripping, a matrix / named-cell / undeclared reading is pinned in all ' + + 'three directions, an undeclared conformance suite is RED and declaring one is GREEN over ' + + 'the same synthetic tree, the dialect ledger reconciles in all three directions, and its ' + + 'offer is marked maintainer-only too.', ); } From fe29b0df7e5cd453d0db427e2f2b7c9b20a9a7b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 10:18:30 +0000 Subject: [PATCH 2/3] fix(devx): hold the dialect table's columns on a red run The UNDECLARED glyph is four characters wider than the others, so the run where the table is most worth reading was the one where its columns broke. Found by the ablation, which is the only run that produces that glyph. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o --- scripts/check-driver-conformance.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index 172821edab..dc4235c61f 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -1361,9 +1361,11 @@ function report() { const suites = dialect.scored.filter((s) => s.driver === driver); const w = Math.max(...suites.map((s) => s.file.split('/').pop().length), 5); for (const s of suites.sort((a, b) => a.file.localeCompare(b.file))) { - const glyph = { matrix: 'matrix', cell: 'cell ', ledger: 'LEDGER', undeclared: 'UNDECLARED' }[s.stance]; + // Padded to the widest glyph so the columns hold on a RED run too — the + // run where this table is most worth reading. + const glyph = { matrix: 'matrix', cell: 'cell', ledger: 'LEDGER', undeclared: 'UNDECLARED' }[s.stance]; console.log( - ` ${glyph} ${s.file.split('/').pop().padEnd(w)} ${s.markers.map((m) => m.replace(/_CASES$/, '')).join(', ')}`, + ` ${glyph.padEnd(10)} ${s.file.split('/').pop().padEnd(w)} ${s.markers.map((m) => m.replace(/_CASES$/, '')).join(', ')}`, ); } console.log(''); From bc203848dfb0d77f7b44f71ca7282ad1047f72df Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 10:22:39 +0000 Subject: [PATCH 3/3] refactor(devx): read the dialect stance through the shared comment mask The first cut of this axis carried its own stripComments. That is the exact anti-pattern scripts/js-comment-mask.mjs was written to delete: every source-scanning gate used to answer "comment or code" privately, and the copies drifted into two silent families -- a naive regex that opens a phantom comment on a `/*` inside a string, and a string-aware scanner that opens a phantom string on a quote inside a regex character class. The shared module is validated against @typescript-eslint/parser over the whole tree by check-comment-mask-corpus.mjs in CI; a private copy is validated by whatever its author thought of. stripComments is the documented projection for this caller: it feeds a scanner and reports neither a line number nor a byte offset. The census output is byte-identical across the swap, so this is a change of provenance and not of measurement. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o --- scripts/check-driver-conformance.mjs | 160 +++++++-------------------- 1 file changed, 39 insertions(+), 121 deletions(-) diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index dc4235c61f..6d1491b44c 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -113,6 +113,7 @@ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; +import { stripComments } from './js-comment-mask.mjs'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); @@ -940,7 +941,7 @@ function walkTsInto(dir, out) { * behind. Extracted so the dialect axis asserts a stance the same way this * script asserts a case-set, rather than inventing a second idiom next to it. * - * @param {string} src file text — pass it through {@link stripCommentsTs} when a + * @param {string} src file text — pass it through `stripComments` when a * comment mentioning the symbol must not count. (`consumes` deliberately does * not, so its verdicts stay byte-identical to what they were.) */ @@ -984,106 +985,28 @@ function consumes(driverDir, marker) { } // ── The dialect axis: detectors ───────────────────────────────────────────── - -/** Punctuation after which a `/` starts a regex literal rather than a division. */ -const REGEX_PREV_PUNCT = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '~', '^', '<', '>']); -/** ...and the keywords after which the same is true (`return /re/.test(x)`). */ -const REGEX_PREV_KEYWORD = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'case', 'do', 'else', 'yield', 'await', 'throw']); - -/** - * `src` with its comments removed, strings and regex literals left intact. - * - * ## Why this is load-bearing rather than tidiness - * - * The dialect stance is read off the source text, so a MENTION must not count as - * a declaration — and on this tree the mentions are everywhere, because the - * files that were FIXED describe the fix in prose: - * - * sql-driver-aggregation-conformance.test.ts the #11456 conversion. Its head - * note says "It used to construct `client: 'better-sqlite3'` as a literal", - * so a detector grepping for the literal flags the one file that was - * repaired. - * sql-driver.ts mentions `FILTER_LOGIC_CASES` and `client: 'postgres'` in - * comments only. Measured: 5 of this driver's 8 covering files change their - * client-literal answer between raw and stripped text. - * - * Strings stay because a stripper that ate them would have to be right about - * `'https://…'`; regex literals stay because a `/` mistaken for a comment start - * would swallow the rest of a line — and a swallowed line is a MISSING stance, - * i.e. a false red. Both directions are pinned in the self-test. - */ -function stripCommentsTs(src) { - let out = ''; - let i = 0; - const n = src.length; - let prevPunct = ''; - let prevWord = ''; - let prevWasWord = false; - const regexAllowed = () => (prevWasWord ? REGEX_PREV_KEYWORD.has(prevWord) : REGEX_PREV_PUNCT.has(prevPunct)); - while (i < n) { - const c = src[i]; - const d = src[i + 1]; - if (c === '/' && d === '/') { - while (i < n && src[i] !== '\n') i++; - continue; - } - if (c === '/' && d === '*') { - i += 2; - // Newlines are kept so line numbers survive for anything that reports them. - while (i < n && !(src[i] === '*' && src[i + 1] === '/')) { - if (src[i] === '\n') out += '\n'; - i++; - } - i += 2; - out += ' '; - prevPunct = ' '; - prevWasWord = false; - continue; - } - if (c === '"' || c === "'" || c === '`') { - out += c; - i++; - while (i < n) { - const s = src[i]; - if (s === '\\') { out += s + (src[i + 1] ?? ''); i += 2; continue; } - out += s; - i++; - if (s === c) break; - } - prevPunct = c; - prevWasWord = false; - continue; - } - if (c === '/' && regexAllowed()) { - out += c; - i++; - let inClass = false; - while (i < n) { - const s = src[i]; - if (s === '\\') { out += s + (src[i + 1] ?? ''); i += 2; continue; } - out += s; - i++; - if (s === '\n') break; // unterminated: it was a division after all - if (s === '[') inClass = true; - else if (s === ']') inClass = false; - else if (s === '/' && !inClass) break; - } - prevPunct = '/'; - prevWasWord = false; - continue; - } - out += c; - if (/[A-Za-z0-9_$]/.test(c)) { - prevWord = prevWasWord ? prevWord + c : c; - prevWasWord = true; - } else if (!/\s/.test(c)) { - prevPunct = c; - prevWasWord = false; - } - i++; - } - return out; -} +// +// The stance is read off source TEXT, so a MENTION must not count as a +// declaration — and on this tree the mentions are everywhere, because the files +// that were FIXED describe the fix in prose: +// +// sql-driver-aggregation-conformance.test.ts the #11456 conversion. Its head +// note says "It used to construct `client: 'better-sqlite3'` as a literal", +// and quotes the import it now uses — so a reader of raw text finds a whole +// import statement inside a comment and scores the file by its own +// changelog. +// sql-driver.ts names `FILTER_LOGIC_CASES` and `client: 'postgres'` in +// comments only. Measured: 5 of this driver's 8 covering files change their +// client-literal answer between raw and masked text. +// +// Separating the two is `scripts/js-comment-mask.mjs`, which exists because +// gates kept answering it privately and the copies drifted into two silent +// families — a naive regex that opens a phantom comment on a `/*` inside a +// string, and a string-aware scanner that opens a phantom string on a quote +// inside a regex character class. This gate deliberately does NOT grow a third +// copy. `stripComments` is the right projection of the two it offers: this +// caller feeds the result to a scanner and reports neither a line number nor a +// byte offset, which is the documented discriminator. /** * This driver's dialect-matrix testkit, or `null` when the package has none. @@ -1109,7 +1032,7 @@ function discoverDialectTestkit(driverDir) { // enough key that a file-wide scan would collect unrelated ones and report a // dialect list the driver does not speak. Bounded by the array's own // terminator, so a second `id:`-bearing export below it cannot leak in. - const body = stripCommentsTs(src).split(new RegExp(`^export const ${DIALECT_TESTKIT_EXPORT}\\b`, 'm'))[1] ?? ''; + const body = stripComments(src).split(new RegExp(`^export const ${DIALECT_TESTKIT_EXPORT}\\b`, 'm'))[1] ?? ''; const cellIds = [...(body.split(/^\]/m)[0] ?? '').matchAll(/\bid:\s*'([a-z0-9_-]+)'/g)].map((m) => m[1]); return { file, specifier: `./${base.replace(/\.ts$/, '.js')}`, cellIds }; } @@ -1127,7 +1050,7 @@ function discoverDialectTestkit(driverDir) { * @param {string} specifier the testkit's module specifier for this package */ function dialectStance(src, specifier) { - const clean = stripCommentsTs(src); + const clean = stripComments(src); if (DIALECT_MATRIX_SYMBOLS.some((s) => drivenFrom(clean, s, specifier))) return 'matrix'; if (DIALECT_CELL_SYMBOLS.some((s) => drivenFrom(clean, s, specifier))) return 'cell'; return 'undeclared'; @@ -1668,24 +1591,19 @@ function selfTest() { // classifier (both directions, plus the precedence), the testkit discovery, // and the invariant itself red-then-green over a synthetic tree. - // -- stripCommentsTs: comments go, code stays. -- - // The false-GREEN direction: a comment must not be able to declare anything. - expect('a line comment is removed', !stripCommentsTs('const a = 1; // DIALECT_CELLS\n').includes('DIALECT_CELLS')); - expect('a block comment is removed', !stripCommentsTs('/* DIALECT_CELLS */ const a = 1;\n').includes('DIALECT_CELLS')); - // The false-RED direction: eating code would hide a real stance. Each of these - // is a shape a naive `//`-scanner gets wrong, and getting it wrong here costs a - // suite that DOES declare its stance a red it cannot act on. - expect('a `//` inside a string is not a comment', - stripCommentsTs("const u = 'https://example.test/x'; const KEEP = 1;\n").includes('KEEP')); - expect('a `//` inside a regex literal is not a comment', - stripCommentsTs('const r = /a\\/\\/b/; const KEEP = 1;\n').includes('KEEP')); - expect('a regex after `return` is a regex, not a division (its quotes must not open a string)', - stripCommentsTs('function f(s) { return /[\'"]/.test(s); }\nconst KEEP = 1;\n').includes('KEEP')); - expect('a real division is still a division, and the comment after it still goes', - (() => { - const out = stripCommentsTs('const q = a / b; // DIALECT_CELLS\nconst KEEP = 1;\n'); - return out.includes('KEEP') && !out.includes('DIALECT_CELLS'); - })()); + // -- The comment mask, as THIS gate uses it. -- + // The mask's own contract — every literal form, both phantom-span families — + // is pinned by `node scripts/js-comment-mask.mjs --self-test` and by + // `check-comment-mask-corpus.mjs`, which diffs it against + // @typescript-eslint/parser over the whole tree in CI. Re-testing it here + // would be a second copy of the answer this gate deliberately does not keep. + // What IS asserted here is that this gate routes through it at all: the two + // cases below are the ones a private `stripComments` got wrong, so they name + // the failure family rather than re-deriving the fix. + expect('a comment cannot declare anything', !stripComments('const a = 1; // DIALECT_CELLS\n').includes('DIALECT_CELLS')); + expect('and a quote inside a regex character class does not swallow the code after it (the ' + + 'phantom-string family, which is why this gate uses the shared mask rather than its own)', + stripComments('function f(s) { return /[\'"]/.test(s); }\nconst KEEP = 1;\n').includes('KEEP')); // -- dialectStance: what a suite SAYS it runs on. -- const KIT_SPEC = './kit.testkit.js';