Skip to content

Commit 20f49a9

Browse files
committed
feat(scripts): check-tenant-chokepoint reads every sql-driver-*.ts seam module
SCAN_FILES widens to the non-test sql-driver-*.ts modules of driver-sql/src, listed from the directory (ruling 6591753301), and a module-level function is read as a class member is, under its own name, so a moved body is still a door. The gate prints its readings (bindings, read doors, tenant predicates, D8 arms) on one line. The watch hints declare each seam module by name; the self-test holds them to the listing both ways and gains a moved-bodies battery. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2gLghEqeDKTbDTC7Zd
1 parent 39a5d9e commit 20f49a9

1 file changed

Lines changed: 142 additions & 7 deletions

File tree

‎scripts/check-tenant-chokepoint.mjs‎

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@
7777
* inherit this chokepoint (`driver-sqlite-wasm`, `driver-turso`). Scanning the
7878
* subclasses is what stops a new door being added one layer down.
7979
*
80+
* And every `sql-driver-*.ts` module beside `sql-driver.ts` (tests excepted),
81+
* listed from the directory (#29520, ruling 6591753301). Splitting the class
82+
* moves method bodies into those modules as functions whose `this` is a host
83+
* the method hands them, so a door can sit one FILE over as well as one layer
84+
* down. A module-level function is read as a member is, under its own name;
85+
* the readings line (`getBuilder` bindings, read doors, tenant predicates, D8
86+
* arms) is what a split compares before and after, and it must not move.
87+
*
8088
* `driver-memory` and `driver-mongodb` are outside it, and NOT because #5499
8189
* froze them: they do not use this mechanism at all. Neither file contains
8290
* `getBuilder` or `applyTenantScope` (mongodb enforces its own wall in
@@ -157,7 +165,7 @@
157165
* (no class found, a builder it cannot classify, or a DISCOVERED floor missed).
158166
*/
159167

160-
import { readFileSync, existsSync } from 'node:fs';
168+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
161169
import { join, relative, sep } from 'node:path';
162170
import { fileURLToPath } from 'node:url';
163171
import { requireDefaultExport } from './import-prerequisite.mjs';
@@ -167,11 +175,35 @@ import { isEntrypoint } from './invoked-as.mjs';
167175

168176
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
169177

170-
/** The `SqlDriver` family — every class that inherits this chokepoint. */
178+
/** The directory whose `sql-driver-*.ts` seam modules carry `SqlDriver`'s moved bodies. */
179+
const SEAM_DIR = join('packages', 'drivers', 'driver-sql', 'src');
180+
181+
/**
182+
* Which basenames of {@link SEAM_DIR} are seam modules this gate reads:
183+
* `sql-driver-<anything>.ts`, never a test or spec file. A pure function of
184+
* the listing, so the self-test can drive it without the repo.
185+
*/
186+
export function seamModulesOf(basenames) {
187+
return basenames
188+
.filter((n) => /^sql-driver-.+\.ts$/.test(n) && !/\.(test|spec)\.ts$/.test(n) && !n.endsWith('.d.ts'))
189+
.sort();
190+
}
191+
192+
/**
193+
* The `SqlDriver` family — every class that inherits this chokepoint — and
194+
* every `sql-driver-*.ts` module its methods' bodies have moved into (#29520,
195+
* ruling 6591753301). A moved body keeps its `this.getBuilder(…)` and
196+
* `this.applyTenantScope(…)` calls on the host its method hands it, so the
197+
* door it builds is still a door, one file over; a gate that read only
198+
* `sql-driver.ts` would count fewer doors after every cut while reporting
199+
* clean. The population is LISTED from the directory, not enumerated by hand,
200+
* so a new seam module is read the day it lands.
201+
*/
171202
const SCAN_FILES = [
172203
join('packages', 'drivers', 'driver-sql', 'src', 'sql-driver.ts'),
173204
join('packages', 'drivers', 'driver-sqlite-wasm', 'src', 'sqlite-wasm-driver.ts'),
174205
join('packages', 'drivers', 'driver-turso', 'src', 'turso-driver.ts'),
206+
...seamModulesOf(existsSync(join(ROOT, SEAM_DIR)) ? readdirSync(join(ROOT, SEAM_DIR)) : []).map((n) => join(SEAM_DIR, n)),
175207
];
176208

177209
/**
@@ -181,9 +213,9 @@ const SCAN_FILES = [
181213
* derivation — which reads SOURCE TEXT — saw a handful of bare words and no path
182214
* at all. This gate was scored `undetermined` for EVERY card: absent from every
183215
* dispatch brief and every `--commands` harvest, while CI ran it on each pull
184-
* request. Worse than the usual cost, because this gate's population is THREE
185-
* FILES: the cards that actually implicate it are rare and precisely nameable,
186-
* and it was reaching none of them.
216+
* request. Worse than the usual cost, because this gate's population is a
217+
* handful of FILES: the cards that actually implicate it are rare and
218+
* precisely nameable, and it was reaching none of them.
187219
*
188220
* The spelling is `DECLARED_WATCH_HINTS` rather than the directory idiom: this
189221
* is an enumerated population, not a walked subtree, and each hint is a whole
@@ -192,7 +224,13 @@ const SCAN_FILES = [
192224
*
193225
* ⛔ Not `packages/drivers/**`, and not a whole-tree marker. Both would put this
194226
* gate on cards it does not read a byte for; a lead that is wrong on thousands
195-
* of files is worse than the silence it replaces.
227+
* of files is worse than the silence it replaces. ⛔ And not
228+
* `sql-driver-*.ts` either, although that is the population's own shape: the
229+
* same directory holds a few hundred `sql-driver-*.test.ts` files this gate
230+
* never reads, and a glob would name it on every one of them. So the seam
231+
* modules are declared one by one, and the self-test holds the list to the
232+
* directory's listing in both directions: a seam module added, renamed or
233+
* removed reds the self-test until it is declared here (#29520).
196234
*
197235
* The self-test derives the coupling from `SCAN_FILES` on both sides rather than
198236
* re-spelling it, so a driver added to or removed from the family cannot leave
@@ -204,6 +242,36 @@ const DECLARED_WATCH_HINTS = [
204242
'packages/drivers/driver-sql/src/sql-driver.ts',
205243
'packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-driver.ts',
206244
'packages/drivers/driver-turso/src/turso-driver.ts',
245+
'packages/drivers/driver-sql/src/sql-driver-aggregation.ts',
246+
'packages/drivers/driver-sql/src/sql-driver-autonumber.ts',
247+
'packages/drivers/driver-sql/src/sql-driver-bulk-operations.ts',
248+
'packages/drivers/driver-sql/src/sql-driver-column-creation.ts',
249+
'packages/drivers/driver-sql/src/sql-driver-cross-field-comparison.ts',
250+
'packages/drivers/driver-sql/src/sql-driver-crud.ts',
251+
'packages/drivers/driver-sql/src/sql-driver-database-helpers.ts',
252+
'packages/drivers/driver-sql/src/sql-driver-date-bucket.ts',
253+
'packages/drivers/driver-sql/src/sql-driver-distinct.ts',
254+
'packages/drivers/driver-sql/src/sql-driver-field-mapping.ts',
255+
'packages/drivers/driver-sql/src/sql-driver-filter-helpers.ts',
256+
'packages/drivers/driver-sql/src/sql-driver-internal-helpers.ts',
257+
'packages/drivers/driver-sql/src/sql-driver-introspection.ts',
258+
'packages/drivers/driver-sql/src/sql-driver-lifecycle.ts',
259+
'packages/drivers/driver-sql/src/sql-driver-managed-drift.ts',
260+
'packages/drivers/driver-sql/src/sql-driver-predicate-emitters.ts',
261+
'packages/drivers/driver-sql/src/sql-driver-presumed-timestamp.ts',
262+
'packages/drivers/driver-sql/src/sql-driver-query-plan.ts',
263+
'packages/drivers/driver-sql/src/sql-driver-raw-execution.ts',
264+
'packages/drivers/driver-sql/src/sql-driver-read-exit-fault.ts',
265+
'packages/drivers/driver-sql/src/sql-driver-rotation.ts',
266+
'packages/drivers/driver-sql/src/sql-driver-schema-sync-section.ts',
267+
'packages/drivers/driver-sql/src/sql-driver-schema-sync.ts',
268+
'packages/drivers/driver-sql/src/sql-driver-sequence-semantics.ts',
269+
'packages/drivers/driver-sql/src/sql-driver-sqlite-serialisation.ts',
270+
'packages/drivers/driver-sql/src/sql-driver-top-level.ts',
271+
'packages/drivers/driver-sql/src/sql-driver-transactions.ts',
272+
'packages/drivers/driver-sql/src/sql-driver-upsert-conflict-target.ts',
273+
'packages/drivers/driver-sql/src/sql-driver-window-function-builder.ts',
274+
'packages/drivers/driver-sql/src/sql-driver-window-functions.ts',
207275
];
208276

209277
/**
@@ -579,6 +647,13 @@ export function analyzeSource(fileName, text) {
579647
}
580648
}
581649
}
650+
// A module-level function is a moved method body (#29520): its `this` is
651+
// the host its method hands it, so `this.getBuilder(…)` there builds the
652+
// same door. Read under the function's name, which is the method's.
653+
// Top-level only: a function nested in a member is walked with the member.
654+
if (ts.isFunctionDeclaration(node) && node.name && ts.isSourceFile(node.parent)) {
655+
visitMethod(node.name.text, node.body);
656+
}
582657
ts.forEachChild(node, walkTop);
583658
};
584659
walkTop(sf);
@@ -638,6 +713,9 @@ const SELF_TEST_VERDICT = 'check-tenant-chokepoint self-test reached its verdict
638713
// does not make. The hoisted single battery is the shape PR #14896 and PR #15003
639714
// landed for exactly this case.
640715
//
716+
// #29520 adds a third, `moved bodies (#29520)`, opened by its own `battery()`
717+
// call ahead of the population declaration: the widened scan's own cases.
718+
//
641719
// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3
642720
// keeps a total "right" the moment a sibling grows.
643721
//
@@ -647,11 +725,12 @@ const SELF_TEST_VERDICT = 'check-tenant-chokepoint self-test reached its verdict
647725
const SELF_TEST_BATTERIES = Object.freeze({
648726
'check-tenant-chokepoint self-test': 17,
649727
'D8 null-arm rule': 24,
728+
'moved bodies (#29520)': 9,
650729
});
651730

652731
// DELETING an entry silences that battery's floor exactly as effectively as
653732
// zeroing it, so the roster's own size is pinned too.
654-
const SELF_TEST_BATTERY_FLOOR = 2;
733+
const SELF_TEST_BATTERY_FLOOR = 3;
655734

656735
// The key an assertion is filed under when no battery is open. It is not a
657736
// declared battery, so it reds by the same set difference rather than silently
@@ -972,6 +1051,50 @@ function selfTest() {
9721051
);
9731052
}
9741053

1054+
// ── #29520: a body moved to a sql-driver-*.ts module is still a door ──────
1055+
battery('moved bodies (#29520)');
1056+
{
1057+
// The shape the codemod writes: the body as a module function whose `this`
1058+
// is the host its method hands it.
1059+
const moved = (body) => analyzeSource('m.ts', `export async function door(this: Pick<H, 'getBuilder'>, object: string, options?: unknown) {\n${body}\n}\n`);
1060+
const unscoped = moved(` const builder = this.getBuilder(object, options);\n return await builder;`);
1061+
assert(unscoped.builders.length === 1 && violationsOf('m.ts', unscoped).length === 1,
1062+
'a moved body that builds and never scopes must REPORT — the module is no hiding place');
1063+
const scoped = moved(` const builder = this.getBuilder(object, options);\n this.applyTenantScope(builder, object, options);\n return await builder;`);
1064+
assert(scoped.builders.length === 1 && violationsOf('m.ts', scoped).length === 0,
1065+
'a moved body that scopes its builder is counted and silent');
1066+
assert(scoped.builders[0]?.method === 'door', 'a moved body is read under its function name, which is its method\'s');
1067+
const inline = moved(` return await this.getBuilder(object, options).select('*');`);
1068+
assert(violationsOf('m.ts', inline).some((p) => p.fatal), 'an inline getBuilder() in a moved body is fatal, as on the class');
1069+
// A function nested in a class member is walked with the member, once.
1070+
const nested = analyzeSource('t.ts', wrap(`async outer(object, options) {
1071+
function inner() { return 1; }
1072+
const builder = this.getBuilder(object, options);
1073+
this.applyTenantScope(builder, object, options);
1074+
return await builder;
1075+
}`));
1076+
assert(nested.builders.length === 1, 'a member is not counted twice');
1077+
// The stub's host hands the factory as a bound reference: not a call, so
1078+
// neither a binding nor an inline read — the call is the moved body's.
1079+
const stub = analyzeSource('t.ts', wrap(`async door(object, options) {
1080+
const host = { getBuilder: this.getBuilder.bind(this), applyTenantScope: (...a) => this.applyTenantScope(...a) };
1081+
return door.call(host, object, options);
1082+
}`));
1083+
assert(stub.builders.length === 0 && stub.unclassifiable.length === 0,
1084+
'a bound getBuilder reference in a stub is neither a door nor an unclassifiable read');
1085+
// The D8 rule already walks module functions; a moved arm is still refused.
1086+
assert(nullArmsOf('m.ts', `export function applyTenantScope(this: H, builder: any, object: string) {
1087+
const field = this.resolveTenantField(object);
1088+
builder.where(field, 'org').orWhereNull(field);
1089+
}`).arms.length === 1, 'D8: an IS NULL arm in a moved body reports');
1090+
// The population: every sql-driver-*.ts module, never a test.
1091+
const listed = seamModulesOf(['sql-driver.ts', 'sql-driver-crud.ts', 'sql-driver-crud.test.ts',
1092+
'sql-driver-x.spec.ts', 'sql-driver-types.d.ts', 'schema-drift.ts', 'sql-driver-top-level.ts']);
1093+
assert(JSON.stringify(listed) === JSON.stringify(['sql-driver-crud.ts', 'sql-driver-top-level.ts']),
1094+
`the seam population is sql-driver-*.ts minus tests, specs and declaration files (got ${listed.join(', ')})`);
1095+
assert(SCAN_FILES.length > 3, 'the live scan reads seam modules beside the three driver files');
1096+
}
1097+
9751098
// ── the dispatch-gates population declaration ────────────────────────────
9761099
battery('check-tenant-chokepoint self-test');
9771100
const scannedPosix = SCAN_FILES.map((f) => f.split(sep).join('/'));
@@ -1056,7 +1179,9 @@ function main() {
10561179
const problems = [];
10571180
const nullArms = [];
10581181
let tenantPredicates = 0;
1182+
let armsSeen = 0;
10591183
let discovered = 0;
1184+
let readDoors = 0;
10601185
let scannedFiles = 0;
10611186

10621187
for (const rel of SCAN_FILES) {
@@ -1073,10 +1198,12 @@ function main() {
10731198
const text = readFileSync(abs, 'utf8');
10741199
const analysis = analyzeSource(rel, text);
10751200
discovered += analysis.builders.length;
1201+
readDoors += analysis.builders.filter((b) => !b.write).length;
10761202
const posix = relative(ROOT, abs).replace(/\\/g, '/');
10771203
problems.push(...violationsOf(posix, analysis));
10781204
const d8 = nullArmsOf(rel, text);
10791205
tenantPredicates += d8.predicates;
1206+
armsSeen += d8.arms.length;
10801207
nullArms.push(...nullArmViolationsOf(posix, d8.arms));
10811208
}
10821209

@@ -1157,6 +1284,14 @@ NULL_ARM_EXEMPT in scripts/check-tenant-chokepoint.mjs — which reverses an ADR
11571284
decision and needs a superseding ADR first.`);
11581285
}
11591286

1287+
// The readings, on one line a base and a head can be compared by (#29520):
1288+
// a split that moves a door out of the files read changes one of them.
1289+
console.log(
1290+
`check:tenant-chokepoint readings: ${discovered} getBuilder() binding(s), ${readDoors} read door(s), ` +
1291+
`${discovered - readDoors} insert builder(s), ${tenantPredicates} tenant predicate(s), ` +
1292+
`${armsSeen} D8 arm(s) across ${scannedFiles} file(s)`,
1293+
);
1294+
11601295
if (problems.length > 0 || nullArms.length > 0) process.exit(1);
11611296

11621297
console.log(

0 commit comments

Comments
 (0)