@remnic/coding-graph 9.3.759

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +130 -0
  2. package/dist/chunk-5I2DBHOQ.js +1042 -0
  3. package/dist/chunk-5I2DBHOQ.js.map +1 -0
  4. package/dist/chunk-CPYJACC5.js +1838 -0
  5. package/dist/chunk-CPYJACC5.js.map +1 -0
  6. package/dist/chunk-ZVCMIM4T.js +216 -0
  7. package/dist/chunk-ZVCMIM4T.js.map +1 -0
  8. package/dist/cypher/query-parser.d.ts +253 -0
  9. package/dist/cypher/query-parser.js +17 -0
  10. package/dist/cypher/query-parser.js.map +1 -0
  11. package/dist/graph-schema.d.ts +84 -0
  12. package/dist/graph-schema.js +17 -0
  13. package/dist/graph-schema.js.map +1 -0
  14. package/dist/graph-store.d.ts +938 -0
  15. package/dist/graph-store.js +16 -0
  16. package/dist/graph-store.js.map +1 -0
  17. package/dist/index.d.ts +1953 -0
  18. package/dist/index.js +3509 -0
  19. package/dist/index.js.map +1 -0
  20. package/grammars/tree-sitter-bash.wasm +0 -0
  21. package/grammars/tree-sitter-c.wasm +0 -0
  22. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  23. package/grammars/tree-sitter-cpp.wasm +0 -0
  24. package/grammars/tree-sitter-go.wasm +0 -0
  25. package/grammars/tree-sitter-java.wasm +0 -0
  26. package/grammars/tree-sitter-javascript.wasm +0 -0
  27. package/grammars/tree-sitter-kotlin.wasm +0 -0
  28. package/grammars/tree-sitter-php.wasm +0 -0
  29. package/grammars/tree-sitter-python.wasm +0 -0
  30. package/grammars/tree-sitter-ruby.wasm +0 -0
  31. package/grammars/tree-sitter-rust.wasm +0 -0
  32. package/grammars/tree-sitter-swift.wasm +0 -0
  33. package/grammars/tree-sitter-tsx.wasm +0 -0
  34. package/grammars/tree-sitter-typescript.wasm +0 -0
  35. package/package.json +79 -0
  36. package/src/co-change.test.ts +175 -0
  37. package/src/co-change.ts +167 -0
  38. package/src/cypher/query-parser.test.ts +1107 -0
  39. package/src/cypher/query-parser.ts +1692 -0
  40. package/src/detect-changes.test.ts +533 -0
  41. package/src/detect-changes.ts +367 -0
  42. package/src/engine/emit.ts +556 -0
  43. package/src/engine/engine.test.ts +1417 -0
  44. package/src/engine/engine.ts +182 -0
  45. package/src/engine/extractors.ts +486 -0
  46. package/src/engine/fixtures.ts +364 -0
  47. package/src/engine/language-sniff.ts +56 -0
  48. package/src/engine/parser-backend.ts +206 -0
  49. package/src/engine/utf16-offsets.ts +68 -0
  50. package/src/git-invoker.test.ts +116 -0
  51. package/src/git-invoker.ts +426 -0
  52. package/src/graph-schema.test.ts +541 -0
  53. package/src/graph-schema.ts +383 -0
  54. package/src/graph-store-pr2.test.ts +1879 -0
  55. package/src/graph-store.test.ts +1420 -0
  56. package/src/graph-store.ts +3489 -0
  57. package/src/index-status.test.ts +303 -0
  58. package/src/index-status.ts +135 -0
  59. package/src/index.ts +384 -0
  60. package/src/lsp/byte-position.ts +173 -0
  61. package/src/lsp/characterization.test.ts +174 -0
  62. package/src/lsp/client.test.ts +275 -0
  63. package/src/lsp/client.ts +484 -0
  64. package/src/lsp/config.ts +219 -0
  65. package/src/lsp/degradation.ts +86 -0
  66. package/src/lsp/fixtures/fake-server.mjs +198 -0
  67. package/src/lsp/framing.test.ts +180 -0
  68. package/src/lsp/framing.ts +177 -0
  69. package/src/lsp/resolution.test.ts +497 -0
  70. package/src/lsp/resolution.ts +483 -0
  71. package/src/lsp/status.ts +140 -0
  72. package/src/lsp/types.ts +167 -0
  73. package/src/reindex.test.ts +1038 -0
  74. package/src/reindex.ts +908 -0
  75. package/src/row-types.ts +45 -0
  76. package/src/semantic/canonical-text.test.ts +150 -0
  77. package/src/semantic/canonical-text.ts +219 -0
  78. package/src/semantic/config.ts +235 -0
  79. package/src/semantic/index.ts +78 -0
  80. package/src/semantic/minhash.test.ts +197 -0
  81. package/src/semantic/minhash.ts +261 -0
  82. package/src/semantic/semantic-query.ts +173 -0
  83. package/src/semantic/semantic.test.ts +1315 -0
  84. package/src/semantic/similarity.ts +268 -0
  85. package/src/semantic/types.ts +145 -0
  86. package/src/semantic/vectors.ts +235 -0
@@ -0,0 +1,541 @@
1
+ /**
2
+ * graph-schema tests — ordered step 1 of issue #1552 PR1.
3
+ *
4
+ * Cover:
5
+ * - schema-creation (fresh DB → all tables present, meta schema_version=1)
6
+ * - upgrade stub (meta v0 → v1 migrates; idempotent re-apply is a no-op)
7
+ * - provenance CHECK rejects unknown values
8
+ *
9
+ * Issue #1552 ordered step 1 demands these tests run BEFORE the write
10
+ * pipeline ships. They are written to FAIL on a fresh DB with no schema,
11
+ * proving the test is meaningful.
12
+ */
13
+ import assert from "node:assert/strict";
14
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import path from "node:path";
17
+ import test from "node:test";
18
+
19
+ import {
20
+ openBetterSqlite3,
21
+ type BetterSqlite3Database,
22
+ } from "@remnic/core/runtime/better-sqlite";
23
+
24
+ import {
25
+ CODING_GRAPH_SCHEMA_VERSION,
26
+ EDGE_PROVENANCE_VALUES,
27
+ applyCodingGraphSchema,
28
+ ftsRowidForNodeId,
29
+ readSchemaVersion,
30
+ } from "./graph-schema.js";
31
+ import { expectRow, expectRows } from "./row-types.js";
32
+ import { nodeIdFor } from "./graph-store.js";
33
+
34
+ async function tempDir(): Promise<string> {
35
+ return mkdtemp(path.join(tmpdir(), "coding-graph-schema-"));
36
+ }
37
+
38
+ function openTempDb(dir: string, name: string): BetterSqlite3Database {
39
+ return openBetterSqlite3(path.join(dir, name));
40
+ }
41
+
42
+ test("schema-creation: fresh DB → meta + files + nodes + edges + nodes_fts exist, schema_version=1", async () => {
43
+ const dir = await tempDir();
44
+ try {
45
+ const db = openTempDb(dir, "fresh.sqlite");
46
+ // Pre-condition: empty DB. Prove the test would fail without the
47
+ // schema apply.
48
+ assert.equal(readSchemaVersion(db), 0);
49
+ const beforeTables = db
50
+ .prepare(
51
+ "SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY name",
52
+ )
53
+ .all() as { name: string }[];
54
+ assert.deepEqual(
55
+ beforeTables.map((r) => r.name),
56
+ [],
57
+ "fresh DB must have no tables",
58
+ );
59
+
60
+ applyCodingGraphSchema(db);
61
+
62
+ const tables = (
63
+ db
64
+ .prepare(
65
+ "SELECT name FROM sqlite_master WHERE type IN ('table') ORDER BY name",
66
+ )
67
+ .all() as { name: string }[]
68
+ ).map((r) => r.name);
69
+ assert.ok(tables.includes("meta"), `expected meta table; got ${tables}`);
70
+ assert.ok(tables.includes("files"), `expected files table; got ${tables}`);
71
+ assert.ok(tables.includes("nodes"), `expected nodes table; got ${tables}`);
72
+ assert.ok(tables.includes("edges"), `expected edges table; got ${tables}`);
73
+
74
+ // FTS5 virtual tables report via the same sqlite_master query.
75
+ const virtual = (
76
+ db
77
+ .prepare(
78
+ "SELECT name FROM sqlite_master WHERE type='table' AND sql LIKE '%VIRTUAL TABLE%nodes_fts%' ORDER BY name",
79
+ )
80
+ .all() as { name: string }[]
81
+ ).map((r) => r.name);
82
+ assert.ok(
83
+ virtual.includes("nodes_fts"),
84
+ `expected nodes_fts virtual table; got ${virtual}`,
85
+ );
86
+
87
+ assert.equal(readSchemaVersion(db), CODING_GRAPH_SCHEMA_VERSION);
88
+ assert.equal(readSchemaVersion(db), 1);
89
+ db.close();
90
+ } finally {
91
+ await rm(dir, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ test("schema-creation: id columns and constraints match the design (rule 23/38)", async () => {
96
+ const dir = await tempDir();
97
+ try {
98
+ const db = openTempDb(dir, "shape.sqlite");
99
+ applyCodingGraphSchema(db);
100
+
101
+ // files: id PK, path UNIQUE, lang NOT NULL, content_hash NOT NULL
102
+ const filesCols = (
103
+ db.prepare("PRAGMA table_info(files)").all() as {
104
+ name: string;
105
+ notnull: number;
106
+ pk: number;
107
+ }[]
108
+ ).map((c) => ({ name: c.name, notnull: c.notnull === 1, pk: c.pk > 0 }));
109
+ const filesByName = Object.fromEntries(filesCols.map((c) => [c.name, c]));
110
+ assert.equal(filesByName.id?.pk, true, "files.id must be PRIMARY KEY");
111
+ assert.equal(filesByName.path?.notnull, true, "files.path must be NOT NULL");
112
+ assert.equal(filesByName.lang?.notnull, true, "files.lang must be NOT NULL");
113
+ assert.equal(
114
+ filesByName.content_hash?.notnull,
115
+ true,
116
+ "files.content_hash must be NOT NULL",
117
+ );
118
+
119
+ // nodes: id PK (TEXT), qualified_name + file_id + span_start + span_end NOT NULL
120
+ const nodesCols = (
121
+ db.prepare("PRAGMA table_info(nodes)").all() as {
122
+ name: string;
123
+ notnull: number;
124
+ pk: number;
125
+ }[]
126
+ ).map((c) => ({ name: c.name, notnull: c.notnull === 1, pk: c.pk > 0 }));
127
+ const nodesByName = Object.fromEntries(nodesCols.map((c) => [c.name, c]));
128
+ assert.equal(nodesByName.id?.pk, true, "nodes.id must be PRIMARY KEY");
129
+ assert.equal(
130
+ nodesByName.qualified_name?.notnull,
131
+ true,
132
+ "nodes.qualified_name must be NOT NULL",
133
+ );
134
+ assert.equal(
135
+ nodesByName.file_id?.notnull,
136
+ true,
137
+ "nodes.file_id must be NOT NULL",
138
+ );
139
+ assert.equal(
140
+ nodesByName.span_start?.notnull,
141
+ true,
142
+ "nodes.span_start must be NOT NULL",
143
+ );
144
+ assert.equal(
145
+ nodesByName.span_end?.notnull,
146
+ true,
147
+ "nodes.span_end must be NOT NULL",
148
+ );
149
+
150
+ // edges: confidence REAL NOT NULL, provenance TEXT NOT NULL with CHECK
151
+ const edgesCols = (
152
+ db.prepare("PRAGMA table_info(edges)").all() as {
153
+ name: string;
154
+ type: string;
155
+ notnull: number;
156
+ }[]
157
+ ).map((c) => ({
158
+ name: c.name,
159
+ type: c.type,
160
+ notnull: c.notnull === 1,
161
+ }));
162
+ const edgesByName = Object.fromEntries(edgesCols.map((c) => [c.name, c]));
163
+ assert.equal(
164
+ edgesByName.confidence?.type,
165
+ "REAL",
166
+ "edges.confidence must be REAL",
167
+ );
168
+ assert.equal(
169
+ edgesByName.confidence?.notnull,
170
+ true,
171
+ "edges.confidence must be NOT NULL",
172
+ );
173
+ assert.equal(
174
+ edgesByName.provenance?.notnull,
175
+ true,
176
+ "edges.provenance must be NOT NULL",
177
+ );
178
+
179
+ db.close();
180
+ } finally {
181
+ await rm(dir, { recursive: true, force: true });
182
+ }
183
+ });
184
+
185
+ test("upgrade stub: meta row v0 (manually cleared) → re-apply recreates everything + version=1", async () => {
186
+ const dir = await tempDir();
187
+ try {
188
+ const db = openTempDb(dir, "upgrade.sqlite");
189
+ applyCodingGraphSchema(db);
190
+ assert.equal(readSchemaVersion(db), 1);
191
+
192
+ // Simulate the pre-v1 state by dropping the version row. Tables stay.
193
+ db.prepare("DELETE FROM meta WHERE key = 'schema_version'").run();
194
+ assert.equal(readSchemaVersion(db), 0);
195
+
196
+ applyCodingGraphSchema(db);
197
+ assert.equal(
198
+ readSchemaVersion(db),
199
+ CODING_GRAPH_SCHEMA_VERSION,
200
+ "re-apply with v0 meta must migrate to v1",
201
+ );
202
+ db.close();
203
+ } finally {
204
+ await rm(dir, { recursive: true, force: true });
205
+ }
206
+ });
207
+
208
+ test("provenance CHECK rejects unknown provenance values", async () => {
209
+ const dir = await tempDir();
210
+ try {
211
+ const db = openTempDb(dir, "prov.sqlite");
212
+ applyCodingGraphSchema(db);
213
+
214
+ // Seed a file + two nodes so we can attempt an edge insert.
215
+ db.prepare(
216
+ "INSERT INTO files (path, lang, content_hash) VALUES (?, ?, ?)",
217
+ ).run("a.ts", "ts", "h1");
218
+ db.prepare(
219
+ "INSERT INTO files (path, lang, content_hash) VALUES (?, ?, ?)",
220
+ ).run("b.ts", "ts", "h2");
221
+ const fileA = db
222
+ .prepare("SELECT id FROM files WHERE path = ?")
223
+ .get("a.ts") as { id: number };
224
+ const fileB = db
225
+ .prepare("SELECT id FROM files WHERE path = ?")
226
+ .get("b.ts") as { id: number };
227
+ db.prepare(
228
+ "INSERT INTO nodes (id, label, name, qualified_name, file_id, span_start, span_end, lang) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
229
+ ).run("id-a", "function", "a", "a", fileA.id, 0, 1, "ts");
230
+ db.prepare(
231
+ "INSERT INTO nodes (id, label, name, qualified_name, file_id, span_start, span_end, lang) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
232
+ ).run("id-b", "function", "b", "b", fileB.id, 0, 1, "ts");
233
+
234
+ // Each whitelisted provenance inserts cleanly.
235
+ for (const prov of EDGE_PROVENANCE_VALUES) {
236
+ db.prepare(
237
+ "INSERT INTO edges (src, dst, type, confidence, provenance) VALUES (?, ?, ?, ?, ?)",
238
+ ).run("id-a", "id-b", `t-${prov}`, 0.5, prov);
239
+ }
240
+
241
+ // An unknown provenance fails the CHECK constraint.
242
+ assert.throws(
243
+ () =>
244
+ db
245
+ .prepare(
246
+ "INSERT INTO edges (src, dst, type, confidence, provenance) VALUES (?, ?, ?, ?, ?)",
247
+ )
248
+ .run("id-a", "id-b", "t-bogus", 0.5, "bogus"),
249
+ /CHECK constraint failed/,
250
+ );
251
+
252
+ db.close();
253
+ } finally {
254
+ await rm(dir, { recursive: true, force: true });
255
+ }
256
+ });
257
+
258
+ test("pragmas match the lcm verbatim (WAL + busy_timeout=5000 + synchronous=NORMAL)", async () => {
259
+ const dir = await tempDir();
260
+ try {
261
+ const db = openTempDb(dir, "pragmas.sqlite");
262
+ // Apply the same pragmas the GraphStore.open() does. We assert at the
263
+ // helper layer (graph-schema.ts does not set pragmas; GraphStore does).
264
+ db.pragma("journal_mode = WAL");
265
+ db.pragma("busy_timeout = 5000");
266
+ db.pragma("synchronous = NORMAL");
267
+
268
+ const journal = db.pragma("journal_mode") as { journal_mode: string }[];
269
+ const busy = db.pragma("busy_timeout") as { timeout: number }[];
270
+ const sync = db.pragma("synchronous") as { synchronous: number }[];
271
+
272
+ assert.equal(journal[0]?.journal_mode, "wal");
273
+ assert.equal(busy[0]?.timeout, 5000);
274
+ assert.equal(sync[0]?.synchronous, 1, "synchronous=NORMAL == 1");
275
+
276
+ db.close();
277
+ } finally {
278
+ await rm(dir, { recursive: true, force: true });
279
+ }
280
+ });
281
+
282
+ // Keep the imports honest — write a tiny file that depends on dir exists.
283
+ test("temp dir helper produces an empty directory", async () => {
284
+ const dir = await tempDir();
285
+ try {
286
+ await writeFile(path.join(dir, "x"), "ok");
287
+ await mkdir(path.join(dir, "nested"), { recursive: true });
288
+ // Just exercising the FS to make sure the helper works.
289
+ } finally {
290
+ await rm(dir, { recursive: true, force: true });
291
+ }
292
+ });
293
+
294
+ test("FTS5 migration: old-schema nodes_fts (content=nodes, no contentless_delete) is dropped + recreated + rebuilt", async () => {
295
+ const dir = await tempDir();
296
+ // Valid deterministic node IDs (sha256 hex) — ftsRowidForNodeId
297
+ // requires hex input; using nodeIdFor mirrors real pipeline data.
298
+ const greetId = nodeIdFor({ qualifiedName: "a.greet", filePath: "a.ts", label: "function" });
299
+ const byeId = nodeIdFor({ qualifiedName: "a.bye", filePath: "a.ts", label: "function" });
300
+ try {
301
+ const db = openTempDb(dir, "fts-migrate.sqlite");
302
+
303
+ // Seed an OLD-schema database: meta + files + nodes + a nodes_fts
304
+ // created with external-content mode (content=nodes) and NO
305
+ // contentless_delete=1. This is the pre-fix schema shape that
306
+ // caused the write pipeline's DELETE FROM nodes_fts to throw.
307
+ db.exec(`
308
+ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
309
+ CREATE TABLE files (
310
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
311
+ path TEXT NOT NULL UNIQUE,
312
+ lang TEXT NOT NULL,
313
+ content_hash TEXT NOT NULL
314
+ );
315
+ CREATE TABLE nodes (
316
+ id TEXT PRIMARY KEY,
317
+ label TEXT NOT NULL,
318
+ name TEXT NOT NULL,
319
+ qualified_name TEXT NOT NULL,
320
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
321
+ span_start INTEGER NOT NULL,
322
+ span_end INTEGER NOT NULL,
323
+ lang TEXT NOT NULL
324
+ );
325
+ CREATE TABLE edges (
326
+ src TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
327
+ dst TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
328
+ type TEXT NOT NULL,
329
+ confidence REAL NOT NULL,
330
+ provenance TEXT NOT NULL,
331
+ UNIQUE (src, dst, type)
332
+ );
333
+ CREATE VIRTUAL TABLE nodes_fts USING fts5(
334
+ name, qualified_name, content=nodes
335
+ );
336
+ `);
337
+ db.prepare(
338
+ "INSERT INTO meta (key, value) VALUES ('schema_version', ?)",
339
+ ).run("0");
340
+
341
+ // Insert a file + two nodes so the FTS rebuild has content to work with.
342
+ db.prepare(
343
+ "INSERT INTO files (path, lang, content_hash) VALUES (?, ?, ?)",
344
+ ).run("a.ts", "ts", "h1");
345
+ const fileRow = expectRow<{ id: number }>(
346
+ db.prepare("SELECT id FROM files WHERE path = ?").get("a.ts"),
347
+ ["id"],
348
+ );
349
+ if (!fileRow) throw new Error("seed file insert failed");
350
+ db.prepare(
351
+ "INSERT INTO nodes (id, label, name, qualified_name, file_id, span_start, span_end, lang) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
352
+ ).run(greetId, "function", "greet", "a.greet", fileRow.id, 0, 10, "ts");
353
+ db.prepare(
354
+ "INSERT INTO nodes (id, label, name, qualified_name, file_id, span_start, span_end, lang) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
355
+ ).run(byeId, "function", "bye", "a.bye", fileRow.id, 10, 20, "ts");
356
+
357
+ // The old-schema FTS must exist before migration.
358
+ const oldSql = expectRow<{ sql: string }>(
359
+ db.prepare(
360
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='nodes_fts'",
361
+ ).get(),
362
+ ["sql"],
363
+ );
364
+ assert.ok(oldSql, "pre-condition: old nodes_fts must exist");
365
+ assert.ok(
366
+ !oldSql!.sql.includes("contentless_delete=1"),
367
+ "pre-condition: old schema must lack contentless_delete=1",
368
+ );
369
+
370
+ // Run the migration.
371
+ applyCodingGraphSchema(db);
372
+ assert.equal(readSchemaVersion(db), CODING_GRAPH_SCHEMA_VERSION);
373
+
374
+ // The nodes_fts table now has contentless_delete=1.
375
+ const newSql = expectRow<{ sql: string }>(
376
+ db.prepare(
377
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='nodes_fts'",
378
+ ).get(),
379
+ ["sql"],
380
+ );
381
+ assert.ok(newSql, "post-migration: nodes_fts must still exist");
382
+ assert.ok(
383
+ newSql!.sql.includes("contentless_delete=1"),
384
+ "post-migration: nodes_fts CREATE must include contentless_delete=1",
385
+ );
386
+
387
+ // The FTS index was rebuilt from surviving nodes — both terms are
388
+ // searchable and map back via fts_index. (Note: SQLite returns the
389
+ // rowid as a JS number which loses precision at int64 magnitude, so
390
+ // we assert via the fts_index reverse map, not raw rowid equality.)
391
+ const ftsHits = expectRows<{ rowid: number }>(
392
+ db.prepare("SELECT rowid FROM nodes_fts WHERE nodes_fts MATCH 'greet'").all(),
393
+ ["rowid"],
394
+ );
395
+ assert.equal(ftsHits.length, 1, "greet must be in the rebuilt FTS");
396
+ const ftsIndexHits = expectRows<{ node_id: string }>(
397
+ db.prepare("SELECT node_id FROM fts_index WHERE fts_rowid = ?").all(
398
+ ftsRowidForNodeId(greetId),
399
+ ),
400
+ ["node_id"],
401
+ );
402
+ assert.equal(ftsIndexHits.length, 1, "fts_index must map the greet rowid");
403
+ assert.equal(ftsIndexHits[0]?.node_id, greetId, "fts_index must map back to the correct node id");
404
+
405
+ // The original failure mode: a DELETE against nodes_fts while the
406
+ // source nodes row still exists must NOT throw (contentless mode
407
+ // allows this; external-content mode rejects it).
408
+ assert.doesNotThrow(() => {
409
+ db.prepare("DELETE FROM nodes_fts WHERE rowid = ?").run(
410
+ ftsRowidForNodeId(greetId),
411
+ );
412
+ });
413
+
414
+ db.close();
415
+ } finally {
416
+ await rm(dir, { recursive: true, force: true });
417
+ }
418
+ });
419
+
420
+ test("edges table has a destination-leading index for prune/cascade lookups (chatgpt-codex-connector P2: 'Add an index for edge destination lookups')", async () => {
421
+ const dir = await tempDir();
422
+ try {
423
+ const db = openTempDb(dir, "edges-idx.sqlite");
424
+ applyCodingGraphSchema(db);
425
+ const indexes = (
426
+ db.prepare("PRAGMA index_list('edges')").all() as { name: string }[]
427
+ ).map((r) => r.name);
428
+ assert.ok(
429
+ indexes.includes("idx_edges_dst"),
430
+ "edges must carry a dst-leading index (idx_edges_dst) so pruneFileNodes()'s WHERE dst IN (...) count and the ON DELETE CASCADE that follows a node delete resolve via an index lookup, not a full edges scan",
431
+ );
432
+ db.close();
433
+ } finally {
434
+ await rm(dir, { recursive: true, force: true });
435
+ }
436
+ });
437
+
438
+ test("node_attributes created on existing v1 stores (chatgpt-codex-connector P1: 'Create node_attributes for existing v1 stores')", async () => {
439
+ const dir = await tempDir();
440
+ try {
441
+ const db = openTempDb(dir, "v1-existing.sqlite");
442
+ applyCodingGraphSchema(db);
443
+ assert.equal(readSchemaVersion(db), 1);
444
+
445
+ // Simulate a PR1-era DB that predates the PR2 node_attributes table:
446
+ // drop it while leaving the meta schema_version=1 row in place.
447
+ db.prepare("DROP TABLE node_attributes").run();
448
+ const remaining = (
449
+ db
450
+ .prepare(
451
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='node_attributes'",
452
+ )
453
+ .all() as { name: string }[]
454
+ ).map((r) => r.name);
455
+ assert.equal(remaining.length, 0, "precondition: node_attributes dropped");
456
+
457
+ // Re-apply the schema as a normal open() would. Because every DDL
458
+ // statement is CREATE TABLE IF NOT EXISTS and applyCodingGraphSchema
459
+ // runs createTables unconditionally, the additive table reappears
460
+ // on the existing v1 DB without a version bump.
461
+ applyCodingGraphSchema(db);
462
+ assert.equal(readSchemaVersion(db), 1, "version unchanged (additive)");
463
+ const recreated = (
464
+ db
465
+ .prepare(
466
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='node_attributes'",
467
+ )
468
+ .all() as { name: string }[]
469
+ ).map((r) => r.name);
470
+ assert.equal(
471
+ recreated.length,
472
+ 1,
473
+ "node_attributes recreated on existing v1 store via the unconditional createTables pass",
474
+ );
475
+ db.close();
476
+ } finally {
477
+ await rm(dir, { recursive: true, force: true });
478
+ }
479
+ });
480
+
481
+ test("a newer (future) schema_version DB is NOT downgraded by applyCodingGraphSchema (chatgpt-codex-connector P2: 'Preserve newer schema_version rows')", async () => {
482
+ const dir = await tempDir();
483
+ try {
484
+ const db = openTempDb(dir, "future-v2.sqlite");
485
+ // Apply once to get a normal v1 schema, then simulate a future
486
+ // version having upgraded the DB to v2 (a version this code does
487
+ // not understand).
488
+ applyCodingGraphSchema(db);
489
+ assert.equal(readSchemaVersion(db), CODING_GRAPH_SCHEMA_VERSION);
490
+ db.prepare(
491
+ "INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)",
492
+ ).run("2");
493
+ assert.equal(readSchemaVersion(db), 2, "precondition: future v2 marker");
494
+
495
+ // Re-open path: older code applies its additive DDL against the v2
496
+ // DB. The version marker must be PRESERVED (2), not silently
497
+ // rewritten down to CODING_GRAPH_SCHEMA_VERSION (1) — otherwise an
498
+ // incompatible future schema is hidden from a later upgrade.
499
+ applyCodingGraphSchema(db);
500
+ assert.equal(
501
+ readSchemaVersion(db),
502
+ 2,
503
+ "a newer schema_version must not be downgraded to CODING_GRAPH_SCHEMA_VERSION",
504
+ );
505
+ db.close();
506
+ } finally {
507
+ await rm(dir, { recursive: true, force: true });
508
+ }
509
+ });
510
+
511
+ test("applyCodingGraphSchema does NOT run createTables against a future-version DB (chatgpt-codex-connector P2: 'Skip destructive DDL for future schema versions')", async () => {
512
+ const dir = await tempDir();
513
+ try {
514
+ const db = openTempDb(dir, "future-v2-noddl.sqlite");
515
+ applyCodingGraphSchema(db);
516
+ // Simulate a future v2 schema whose node_attributes table was
517
+ // dropped/renamed by the newer version.
518
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)").run("2");
519
+ db.prepare("DROP TABLE node_attributes").run();
520
+
521
+ // Older code re-opens the v2 DB. createTables must NOT run — if it
522
+ // did, its additive pass would recreate node_attributes, mutating
523
+ // the newer schema. Assert the table stays gone AND the version
524
+ // stays 2.
525
+ applyCodingGraphSchema(db);
526
+ assert.equal(readSchemaVersion(db), 2, "future version preserved");
527
+ const remaining = (
528
+ db
529
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='node_attributes'")
530
+ .all() as { name: string }[]
531
+ ).map((r) => r.name);
532
+ assert.equal(
533
+ remaining.length,
534
+ 0,
535
+ "createTables must not run against a future-version DB (node_attributes should not reappear)",
536
+ );
537
+ db.close();
538
+ } finally {
539
+ await rm(dir, { recursive: true, force: true });
540
+ }
541
+ });