Public: anyone can read this; the crew can change it.

Automate a review finding as a lint or test: First fix: item 1 (make the record reachable). #11851

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/11843-automate-a-review-finding-as-a-lint-or-t → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/11843-automate-a-review-finding-as-a-lint-or-t was merged into main as 3b37d9f.

Review

Sign in to review this change.
3 files changed +105 −0
scripts/unread-tables-allow.json added +5
@@ -0,0 +1,5 @@
1 [
2 "keeper_beat",
3 "proof_run",
4 "sentinel_kept"
5 ]
scripts/unread-tables.ts added +45
@@ -0,0 +1,45 @@
1 /**
2 * Unread tables (#11843): each table a migration creates that no code under
3 * `src/` reads. A record the forge writes and nobody can read back is not done
4 * (docs/VISION.md: done means reachable); reviewers asked for "make the record
5 * reachable" four times (#9975, #10663, #10664, #11735). Read means the table
6 * follows `FROM` or `JOIN` in a `src/` file other than a migration, in any
7 * quoting. A table a later migration drops is not counted.
8 * `scripts/unread-tables-allow.json` is what main carries today;
9 * `test/unread-tables.test.ts` keeps it shrinking.
10 */
11
12 import { readFileSync } from "node:fs";
13 import { join } from "node:path";
14
15 const CREATE = /\bCREATE\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?([a-z_][a-z0-9_]*)["`]?/gi;
16 const DROP = /\bDROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["`]?([a-z_][a-z0-9_]*)["`]?/gi;
17 const RENAME = /\bALTER\s+TABLE\s+["`]?([a-z_][a-z0-9_]*)["`]?\s+RENAME\s+TO\s+["`]?([a-z_][a-z0-9_]*)["`]?/gi;
18
19 const scan = (root: string, glob: string) => [...new Bun.Glob(glob).scanSync({ cwd: root })].sort();
20
21 /** The tables the migrations under `root` create and keep, sorted. */
22 export function createdTables(root: string): string[] {
23 const made = new Set<string>();
24 for (const f of scan(root, "src/core/migrations/*.ts")) {
25 const text = readFileSync(join(root, f), "utf8");
26 for (const m of text.matchAll(CREATE)) made.add(m[1]!.toLowerCase());
27 for (const m of text.matchAll(RENAME)) {
28 made.delete(m[1]!.toLowerCase());
29 made.add(m[2]!.toLowerCase());
30 }
31 for (const m of text.matchAll(DROP)) made.delete(m[1]!.toLowerCase());
32 }
33 return [...made].sort();
34 }
35
36 /** The created tables no `src/` file outside migrations reads, sorted. */
37 export function unreadTables(root: string): string[] {
38 const text = scan(root, "src/**/*.ts")
39 .filter((f) => !f.startsWith("src/core/migrations/") && !f.endsWith(".test.ts"))
40 .map((f) => readFileSync(join(root, f), "utf8"))
41 .join("\n");
42 return createdTables(root).filter(
43 (t) => !new RegExp(`\\b(?:FROM|JOIN)\\s+["\`]?${t}\\b`, "i").test(text),

\b(?:FROM|JOIN) also matches DELETE FROM t, so a write-and-prune-only table counts as read. Exclude a preceding DELETE, and add a fixture.

44 );
45 }
test/unread-tables.test.ts added +55
@@ -0,0 +1,55 @@
1 /**
2 * A table a migration creates that no code under src/ reads is a record nobody
3 * can reach (#11843; reviewers asked "make the record reachable" in #9975,
4 * #10663, #10664, #11735). It fails here unless `scripts/unread-tables-allow.json`
5 * lists it, and a listed table that is now read fails too, so the list only shrinks.
6 */
7
8 import { expect, test } from "bun:test";
9 import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
10 import { tmpdir } from "node:os";
11 import { dirname, join } from "node:path";
12 import { createdTables, unreadTables } from "../scripts/unread-tables.ts";
13 import allowed from "../scripts/unread-tables-allow.json";
14
15 const root = new URL("..", import.meta.url).pathname;
16 const found = unreadTables(root);
17
18 test("every table a migration creates is read by code under src/, or allowed", () => {
19 const fresh = found.filter((t) => !allowed.includes(t));
20 expect(
21 fresh,
22 `rule "make the record reachable": no src/ code reads table ${fresh.join(", ")} (no FROM/JOIN); show it on a page, a /system line or an MCP tool, or it is not done (docs/VISION.md)`,
23 ).toEqual([]);
24 });
25
26 test("an allowed table that is now read leaves the list", () => {
27 const stale = allowed.filter((t) => !found.includes(t));
28 expect(stale, `now read or gone, remove from scripts/unread-tables-allow.json: ${stale.join(", ")}`).toEqual([]);
29 });
30
31 test("unreadTables() counts FROM and JOIN in src, not migrations or tests, and follows drops and renames", () => {
32 const dir = mkdtempSync(join(process.env.TMPDIR ?? tmpdir(), "unread-"));
33 const put = (path: string, text: string) => {
34 mkdirSync(dirname(join(dir, path)), { recursive: true });
35 writeFileSync(join(dir, path), text);
36 };
37 try {
38 put(
39 "src/core/migrations/1.ts",
40 "export default `CREATE TABLE read_me (a INT); CREATE TABLE joined (a INT); CREATE TABLE write_only (a INT);\n" +
41 "CREATE TABLE gone (a INT); CREATE TABLE tmp_new (a INT); CREATE TABLE test_only (a INT);`;\n",
42 );
43 put("src/core/migrations/2.ts", "export default `DROP TABLE gone; ALTER TABLE tmp_new RENAME TO final_name;`;\n");
44 put(
45 "src/a.ts",
46 'db.query("SELECT a FROM read_me JOIN joined ON 1");\ndb.query("INSERT INTO write_only (a) VALUES (1)");\n' +
47 'db.query("SELECT 1 FROM final_name");\n',
48 );
49 put("src/a.test.ts", 'db.query("SELECT 1 FROM test_only");\n');
50 expect(createdTables(dir)).toEqual(["final_name", "joined", "read_me", "test_only", "write_only"]);
51 expect(unreadTables(dir)).toEqual(["test_only", "write_only"]);
52 } finally {
53 rmSync(dir, { recursive: true, force: true });
54 }
55 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs