@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,216 @@
1
+ // src/row-types.ts
2
+ function isSqliteRow(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function expectRow(value, columns) {
6
+ if (!isSqliteRow(value)) return void 0;
7
+ for (const col of columns) {
8
+ if (!(col in value)) return void 0;
9
+ }
10
+ return value;
11
+ }
12
+ function expectRows(value, columns) {
13
+ if (!Array.isArray(value)) return [];
14
+ const out = [];
15
+ for (const row of value) {
16
+ const narrowed = expectRow(row, columns);
17
+ if (narrowed) out.push(narrowed);
18
+ }
19
+ return out;
20
+ }
21
+
22
+ // src/graph-schema.ts
23
+ var CODING_GRAPH_SCHEMA_VERSION = 1;
24
+ function ftsRowidForNodeId(nodeId) {
25
+ return BigInt(`0x${nodeId.slice(0, 16)}`) & BigInt("0x7fffffffffffffff");
26
+ }
27
+ var EDGE_PROVENANCE_VALUES = [
28
+ "heuristic",
29
+ "lsp",
30
+ "trace",
31
+ "semantic"
32
+ ];
33
+ function isEdgeProvenance(value) {
34
+ return typeof value === "string" && EDGE_PROVENANCE_VALUES.includes(value);
35
+ }
36
+ function applyCodingGraphSchema(db) {
37
+ const versionRow = expectRow(
38
+ db.prepare(
39
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'"
40
+ ).get(),
41
+ ["name"]
42
+ );
43
+ if (!versionRow) {
44
+ createTables(db);
45
+ writeSchemaVersion(db, CODING_GRAPH_SCHEMA_VERSION);
46
+ return;
47
+ }
48
+ const meta = expectRow(
49
+ db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get(),
50
+ ["value"]
51
+ );
52
+ const currentVersion = meta ? parseInt(meta.value, 10) : 0;
53
+ if (currentVersion > CODING_GRAPH_SCHEMA_VERSION) {
54
+ return;
55
+ }
56
+ createTables(db);
57
+ writeSchemaVersion(db, CODING_GRAPH_SCHEMA_VERSION);
58
+ }
59
+ function createTables(db) {
60
+ const provenanceList = EDGE_PROVENANCE_VALUES.map((v) => `'${v}'`).join(", ");
61
+ db.exec(`
62
+ CREATE TABLE IF NOT EXISTS meta (
63
+ key TEXT PRIMARY KEY,
64
+ value TEXT NOT NULL
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS files (
68
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
69
+ path TEXT NOT NULL UNIQUE,
70
+ lang TEXT NOT NULL,
71
+ content_hash TEXT NOT NULL
72
+ );
73
+ CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
74
+
75
+ CREATE TABLE IF NOT EXISTS nodes (
76
+ id TEXT PRIMARY KEY,
77
+ label TEXT NOT NULL,
78
+ name TEXT NOT NULL,
79
+ qualified_name TEXT NOT NULL,
80
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
81
+ span_start INTEGER NOT NULL,
82
+ span_end INTEGER NOT NULL,
83
+ lang TEXT NOT NULL
84
+ );
85
+ CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_id);
86
+ CREATE INDEX IF NOT EXISTS idx_nodes_qname ON nodes(qualified_name);
87
+ CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label);
88
+
89
+ CREATE TABLE IF NOT EXISTS edges (
90
+ src TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
91
+ dst TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
92
+ type TEXT NOT NULL,
93
+ confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0),
94
+ provenance TEXT NOT NULL CHECK (provenance IN (${provenanceList})),
95
+ UNIQUE (src, dst, type)
96
+ );
97
+ -- Destination-leading index. The UNIQUE(src,dst,type) key is
98
+ -- src-leading, so pruneFileNodes()'s 'WHERE dst IN (...)' count and
99
+ -- the ON DELETE CASCADE that follows a node delete (SQLite must
100
+ -- locate child edges by 'dst') would otherwise scan the whole edges
101
+ -- table. A dst-leading index turns ordinary symbol deletion into an
102
+ -- index lookup instead of a full scan
103
+ -- (chatgpt-codex-connector P2: 'Add an index for edge destination
104
+ -- lookups').
105
+ CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst);
106
+ `);
107
+ db.exec(`
108
+ CREATE TABLE IF NOT EXISTS node_attributes (
109
+ node_id TEXT PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
110
+ is_exported INTEGER NOT NULL CHECK (is_exported IN (0, 1)),
111
+ is_route_handler INTEGER NOT NULL CHECK (is_route_handler IN (0, 1))
112
+ );
113
+ CREATE INDEX IF NOT EXISTS idx_node_attributes_exported
114
+ ON node_attributes(is_exported) WHERE is_exported = 1;
115
+ CREATE INDEX IF NOT EXISTS idx_node_attributes_route
116
+ ON node_attributes(is_route_handler) WHERE is_route_handler = 1;
117
+ `);
118
+ db.exec(`
119
+ CREATE TABLE IF NOT EXISTS co_changes (
120
+ file_a TEXT NOT NULL,
121
+ file_b TEXT NOT NULL,
122
+ support INTEGER NOT NULL CHECK (support >= 0),
123
+ confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0),
124
+ UNIQUE (file_a, file_b)
125
+ );
126
+ CREATE INDEX IF NOT EXISTS idx_co_changes_a ON co_changes(file_a);
127
+ CREATE INDEX IF NOT EXISTS idx_co_changes_b ON co_changes(file_b);
128
+ `);
129
+ db.exec(`
130
+ CREATE TABLE IF NOT EXISTS symbol_vectors (
131
+ node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
132
+ model_id TEXT NOT NULL,
133
+ content_hash TEXT NOT NULL,
134
+ dims INTEGER NOT NULL CHECK (dims > 0),
135
+ vector BLOB NOT NULL,
136
+ PRIMARY KEY (node_id, model_id)
137
+ );
138
+ CREATE INDEX IF NOT EXISTS idx_symbol_vectors_model
139
+ ON symbol_vectors(model_id);
140
+ `);
141
+ db.exec(`
142
+ CREATE TABLE IF NOT EXISTS fts_index (
143
+ fts_rowid INTEGER PRIMARY KEY,
144
+ node_id TEXT NOT NULL UNIQUE
145
+ );
146
+ CREATE INDEX IF NOT EXISTS idx_fts_index_node ON fts_index(node_id);
147
+ `);
148
+ const ftsCreateSql = expectRow(
149
+ db.prepare(
150
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='nodes_fts'"
151
+ ).get(),
152
+ ["sql"]
153
+ );
154
+ const needsFtsRecreate = !ftsCreateSql || !ftsCreateSql.sql.includes("contentless_delete=1");
155
+ if (needsFtsRecreate) {
156
+ db.exec("DROP TABLE IF EXISTS nodes_fts;");
157
+ db.exec(`
158
+ CREATE VIRTUAL TABLE nodes_fts USING fts5(
159
+ name,
160
+ qualified_name,
161
+ id UNINDEXED,
162
+ content='',
163
+ contentless_delete=1,
164
+ tokenize='unicode61 remove_diacritics 2'
165
+ );
166
+ `);
167
+ const survivingNodes = expectRows(
168
+ db.prepare("SELECT id, name, qualified_name FROM nodes").all(),
169
+ ["id", "name", "qualified_name"]
170
+ );
171
+ if (survivingNodes.length > 0) {
172
+ const insertFts = db.prepare(
173
+ "INSERT INTO nodes_fts (rowid, name, qualified_name) VALUES (?, ?, ?)"
174
+ );
175
+ const upsertFtsIndex = db.prepare(
176
+ "INSERT OR REPLACE INTO fts_index (fts_rowid, node_id) VALUES (?, ?)"
177
+ );
178
+ for (const n of survivingNodes) {
179
+ const rowid = ftsRowidForNodeId(n.id);
180
+ insertFts.run(rowid, n.name, n.qualified_name);
181
+ upsertFtsIndex.run(rowid, n.id);
182
+ }
183
+ }
184
+ }
185
+ }
186
+ function writeSchemaVersion(db, version) {
187
+ db.prepare(
188
+ "INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)"
189
+ ).run(String(version));
190
+ }
191
+ function readSchemaVersion(db) {
192
+ const metaTable = expectRow(
193
+ db.prepare(
194
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'"
195
+ ).get(),
196
+ ["name"]
197
+ );
198
+ if (!metaTable) return 0;
199
+ const meta = expectRow(
200
+ db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get(),
201
+ ["value"]
202
+ );
203
+ return meta ? parseInt(meta.value, 10) : 0;
204
+ }
205
+
206
+ export {
207
+ expectRow,
208
+ expectRows,
209
+ CODING_GRAPH_SCHEMA_VERSION,
210
+ ftsRowidForNodeId,
211
+ EDGE_PROVENANCE_VALUES,
212
+ isEdgeProvenance,
213
+ applyCodingGraphSchema,
214
+ readSchemaVersion
215
+ };
216
+ //# sourceMappingURL=chunk-ZVCMIM4T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/row-types.ts","../src/graph-schema.ts"],"sourcesContent":["/**\n * Typed-row helpers for better-sqlite3 — narrow the library's `unknown`\n * return values with a runtime shape check before reading properties.\n *\n * better-sqlite3 returns `unknown` for `.get()` and `unknown[]` for\n * `.all()`. The codebase has historically relied on inline `as` casts;\n * this module provides a typed wrapper that validates the SHAPE at runtime\n * so a bad query (column renames, dropped columns) fails loudly instead\n * of silently reading `undefined`.\n */\n\nexport type SqliteRow = Record<string, unknown>;\n\nexport function isSqliteRow(value: unknown): value is SqliteRow {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Narrow a `.get()` result. Returns `undefined` when the row is missing\n * OR when the expected columns are absent. Callers can then assert the\n * expected shape (the columns are guaranteed to exist after this check).\n */\nexport function expectRow<T extends SqliteRow>(\n value: unknown,\n columns: readonly (keyof T)[],\n): T | undefined {\n if (!isSqliteRow(value)) return undefined;\n for (const col of columns) {\n if (!(col in value)) return undefined;\n }\n return value as T;\n}\n\nexport function expectRows<T extends SqliteRow>(\n value: unknown,\n columns: readonly (keyof T)[],\n): T[] {\n if (!Array.isArray(value)) return [];\n const out: T[] = [];\n for (const row of value) {\n const narrowed = expectRow<T>(row, columns);\n if (narrowed) out.push(narrowed);\n }\n return out;\n}\n","/**\n * Coding-graph SQLite schema — versioned meta + tables + FTS5 virtual table.\n *\n * Issue #1552 PR1 (Track B, Phase 1). The schema + write pipeline only;\n * traversal, search, dead-code and the openCypher subset land in PR2/PR3.\n *\n * PR2 additive table (`node_attributes`): tracks per-node exclusion flags\n * consumed by `deadCode()` — `is_exported`, `is_route_handler`. Added in\n * PR2 (issue #1552 step 5) as a SEPARATE table rather than ALTER TABLE on\n * `nodes`, so existing v1 databases gain the table via the same\n * `CREATE TABLE IF NOT EXISTS` pass without a schema-version bump or a\n * migration (rule 23 — additive, characterized before moving).\n *\n * Design anchors:\n * - `packages/remnic-core/src/lcm/schema.ts` (versioning + pragmas) —\n * copied VERBATIM for the WAL / busy_timeout / synchronous pragmas and\n * the meta-table version row. Do not invent a new pattern (rule 23/38).\n * - `packages/remnic-core/src/runtime/better-sqlite.ts` (`openBetterSqlite3`)\n * — the shared opener; native-binding lifecycle is paid for once there.\n * - issue://1552 \"Design\" — node ids hash sorted key material; file\n * contents are NEVER stored (only spans + content hashes); FTS5 covers\n * node names; provenance CHECK enforces the four-value enum.\n *\n * Schema versioning:\n * The schema_version row lives in `meta` (a generic key/value table, not\n * lcm_meta — independent store, separate version namespace). Fresh DB →\n * schema_version=1. Upgrade stub: meta v0 → v1 (rule 23, characterize\n * before moving).\n *\n * Dangling-edge policy (PR1 decision):\n * When `upsertFileBatch` deletes a file's prior nodes + edges, cross-file\n * edges whose `dst` was a node owned by the deleted file become dangling.\n * We DROP them. They are counted in `UpsertResult.droppedDanglingEdges`\n * so callers can surface the loss. Keeping them with a `dst_unresolved`\n * marker would leak orphans and bias `traverse()` results — drop is the\n * conservative choice for a write pipeline whose caller knows the\n * canonical file set on each batch (rule 11, 40).\n */\nimport type { BetterSqlite3Database } from \"@remnic/core/runtime/better-sqlite\";\n\nimport { expectRow, expectRows } from \"./row-types.js\";\n\nexport const CODING_GRAPH_SCHEMA_VERSION = 1;\n\n/**\n * FTS5 rowid derived from a deterministic node id. FTS5 rowids are\n * signed 64-bit integers; we slice the leading 16 hex chars (= 64 bits)\n * of the sha256 id and mask to the int64 positive range so SQLite\n * accepts it. The full 64-bit space is large enough that collisions\n * across distinct node ids are negligible. Contentless FTS5\n * (`content=''`) does NOT store UNINDEXED column values, so the only\n * reliable key into the virtual table is the rowid.\n *\n * Lives in graph-schema (not graph-store) so the schema migration path\n * can rebuild FTS rows from existing `nodes` without importing the\n * store module (which would create a circular dependency — graph-store\n * imports graph-schema).\n */\nexport function ftsRowidForNodeId(nodeId: string): bigint {\n return BigInt(`0x${nodeId.slice(0, 16)}`) & BigInt(\"0x7fffffffffffffff\");\n}\n\n/**\n * Provenance enum for edges — mirrors #1552's `heuristic|lsp|trace|semantic`\n * whitelist. The CHECK constraint rejects writes outside this set so a\n * buggy resolver can't sneak in unknown values (rule 23).\n */\nexport const EDGE_PROVENANCE_VALUES = [\n \"heuristic\",\n \"lsp\",\n \"trace\",\n \"semantic\",\n] as const;\n\nexport type EdgeProvenance = (typeof EDGE_PROVENANCE_VALUES)[number];\n\nexport function isEdgeProvenance(value: unknown): value is EdgeProvenance {\n return (\n typeof value === \"string\" &&\n (EDGE_PROVENANCE_VALUES as readonly string[]).includes(value)\n );\n}\n\n/**\n * Apply (or upgrade) the coding-graph schema on an already-open SQLite\n * handle. Public so test seams and migration tools can bootstrap an\n * in-memory database without going through {@link openCodingGraphDatabase}.\n *\n * Mirrors `applyLcmSchema` in `packages/remnic-core/src/lcm/schema.ts` —\n * distinct function, same shape, separate version namespace.\n */\nexport function applyCodingGraphSchema(db: BetterSqlite3Database): void {\n const versionRow = expectRow<{ name: string }>(\n db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type='table' AND name='meta'\",\n )\n .get(),\n [\"name\"],\n );\n\n if (!versionRow) {\n // Fresh DB — create every table and stamp the current version.\n createTables(db);\n writeSchemaVersion(db, CODING_GRAPH_SCHEMA_VERSION);\n return;\n }\n\n const meta = expectRow<{ value: string }>(\n db\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get(),\n [\"value\"],\n );\n const currentVersion = meta ? parseInt(meta.value, 10) : 0;\n\n // Only run createTables when the on-disk version is at or below this\n // code's version. For an AT-OR-BELOW DB (fresh-ish v0/v1) the pass is\n // additive: every core statement is CREATE TABLE IF NOT EXISTS, so the\n // PR2 `node_attributes` table appears on existing v1 databases without\n // a version bump (chatgpt-codex-connector P1: 'Create node_attributes\n // for existing v1 stores'), and a v0 DB is upgraded.\n //\n // A NEWER DB (currentVersion > CODING_GRAPH_SCHEMA_VERSION — older\n // code opening a future-version DB after a downgrade, or a parallel\n // install) must be left UNTOUCHED: createTables is NOT purely\n // additive because its FTS migration drops + recreates `nodes_fts`\n // when the stored CREATE SQL lacks `contentless_delete=1`. Running\n // that against a future schema that legitimately changed or removed\n // that table would mutate the newer schema while preserving its\n // version marker — silent corruption. Skip createTables AND the\n // version write for newer DBs (chatgpt-codex-connector P2: 'Skip\n // destructive DDL for future schema versions').\n if (currentVersion > CODING_GRAPH_SCHEMA_VERSION) {\n return;\n }\n createTables(db);\n writeSchemaVersion(db, CODING_GRAPH_SCHEMA_VERSION);\n}\n\nfunction createTables(db: BetterSqlite3Database): void {\n // Provenance whitelist must match EDGE_PROVENANCE_VALUES. SQLite CHECK\n // constraints are re-checked against every INSERT/UPDATE; bypassing this\n // gate would require raw exec, which we never do (rule 51).\n const provenanceList = EDGE_PROVENANCE_VALUES.map((v) => `'${v}'`).join(\", \");\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS files (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n path TEXT NOT NULL UNIQUE,\n lang TEXT NOT NULL,\n content_hash TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);\n\n CREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n label TEXT NOT NULL,\n name TEXT NOT NULL,\n qualified_name TEXT NOT NULL,\n file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n span_start INTEGER NOT NULL,\n span_end INTEGER NOT NULL,\n lang TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_id);\n CREATE INDEX IF NOT EXISTS idx_nodes_qname ON nodes(qualified_name);\n CREATE INDEX IF NOT EXISTS idx_nodes_label ON nodes(label);\n\n CREATE TABLE IF NOT EXISTS edges (\n src TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n dst TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n type TEXT NOT NULL,\n confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0),\n provenance TEXT NOT NULL CHECK (provenance IN (${provenanceList})),\n UNIQUE (src, dst, type)\n );\n -- Destination-leading index. The UNIQUE(src,dst,type) key is\n -- src-leading, so pruneFileNodes()'s 'WHERE dst IN (...)' count and\n -- the ON DELETE CASCADE that follows a node delete (SQLite must\n -- locate child edges by 'dst') would otherwise scan the whole edges\n -- table. A dst-leading index turns ordinary symbol deletion into an\n -- index lookup instead of a full scan\n -- (chatgpt-codex-connector P2: 'Add an index for edge destination\n -- lookups').\n CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst);\n `);\n // PR2 (issue #1552 step 5): per-node exclusion flags consumed by\n // `GraphStore.deadCode()`. Kept in a SEPARATE table rather than an\n // ALTER TABLE on `nodes` so existing v1 databases gain the table via\n // the same `CREATE TABLE IF NOT EXISTS` pass without a schema-version\n // bump or a data migration (rule 23 — additive, characterized before\n // moving). ON DELETE CASCADE on `nodes(id)` keeps the table in lockstep\n // with node lifetimes; the foreign_keys=ON pragma set in\n // `GraphStore.open()` enforces it.\n //\n // `is_exported`: 1 when the symbol's `name` matches an entry in the\n // FileIR's `exports` list (matched per-file at write time). The\n // dead-code query treats exported symbols as not-dead even with\n // zero inbound CALLS/USES_TYPE edges — they form the package's\n // public surface and may be called by external consumers the graph\n // cannot see.\n // `is_route_handler`: 1 when the symbol's `qualified_name` matches a\n // route's `handlerQualifiedName` in the FileIR's `routes` list.\n // Route handlers are reachable from HTTP requests regardless of\n // whether any other node CALLS them inside the indexed codebase.\n //\n // Both columns are NOT NULL with CHECK IN (0,1): a missing row means\n // \"neither flag set\" (the LEFT JOIN in deadCode() COALESCEs to 0).\n db.exec(`\n CREATE TABLE IF NOT EXISTS node_attributes (\n node_id TEXT PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,\n is_exported INTEGER NOT NULL CHECK (is_exported IN (0, 1)),\n is_route_handler INTEGER NOT NULL CHECK (is_route_handler IN (0, 1))\n );\n CREATE INDEX IF NOT EXISTS idx_node_attributes_exported\n ON node_attributes(is_exported) WHERE is_exported = 1;\n CREATE INDEX IF NOT EXISTS idx_node_attributes_route\n ON node_attributes(is_route_handler) WHERE is_route_handler = 1;\n `);\n // PR3 (issue #1553): co-change edges — file-level relationships mined\n // from git history. Stored separately from the symbol-level `edges`\n // table because co-change is a file-to-file concern, not symbol-to-\n // symbol. Additive to v1 (CREATE TABLE IF NOT EXISTS — same pattern\n // as `node_attributes` in PR2, rule 23).\n //\n // UNIQUE(file_a, file_b) ensures idempotent upserts; the mining\n // pipeline clears + repopulates each run so stale edges from history\n // changes are pruned automatically.\n db.exec(`\n CREATE TABLE IF NOT EXISTS co_changes (\n file_a TEXT NOT NULL,\n file_b TEXT NOT NULL,\n support INTEGER NOT NULL CHECK (support >= 0),\n confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0),\n UNIQUE (file_a, file_b)\n );\n CREATE INDEX IF NOT EXISTS idx_co_changes_a ON co_changes(file_a);\n CREATE INDEX IF NOT EXISTS idx_co_changes_b ON co_changes(file_b);\n `);\n // Semantic layer (issue #1556): symbol embedding vectors. Additive to v1\n // (CREATE TABLE IF NOT EXISTS — same rule-23 pattern as node_attributes).\n // One row per (node_id, model_id): a node re-embedded under a different\n // provider/model gets a distinct row so a provider swap does not\n // overwrite the prior vectors (the cache invalidation test covers this).\n // content_hash is the canonical-text hash (rule 37) — a re-index compares\n // it to decide whether to re-embed. CASCADE on nodes(id) keeps the table\n // in lockstep with node lifetimes (foreign_keys=ON is set in GraphStore.open).\n db.exec(`\n CREATE TABLE IF NOT EXISTS symbol_vectors (\n node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,\n model_id TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n dims INTEGER NOT NULL CHECK (dims > 0),\n vector BLOB NOT NULL,\n PRIMARY KEY (node_id, model_id)\n );\n CREATE INDEX IF NOT EXISTS idx_symbol_vectors_model\n ON symbol_vectors(model_id);\n `);\n // repopulate both tables in lockstep without ordering concerns.\n db.exec(`\n CREATE TABLE IF NOT EXISTS fts_index (\n fts_rowid INTEGER PRIMARY KEY,\n node_id TEXT NOT NULL UNIQUE\n );\n CREATE INDEX IF NOT EXISTS idx_fts_index_node ON fts_index(node_id);\n `);\n // FTS5 virtual table over node names + qualified_names for PR2's\n // name search. Contentless-delete mode (`content=''` plus\n // `contentless_delete=1`, SQLite 3.43+) lets us run standard\n // `DELETE` and `INSERT OR REPLACE` statements against the virtual\n // table — the write pipeline's only requirement. The `id UNINDEXED`\n // column mirrors the deterministic node id for human-readable\n // inspection; the write pipeline's actual key is the rowid derived\n // from the node-id hash (see `ftsRowidForNodeId` below).\n //\n // Migration: databases created before the contentless-FTS5 fix have\n // an OLD `nodes_fts` table whose CREATE SQL used `content=nodes`\n // (external-content) and lacks `contentless_delete=1`. The write\n // pipeline's `DELETE FROM nodes_fts WHERE rowid = ?` fails on such\n // a table while the source `nodes` row still exists, throwing and\n // aborting the whole batch (kilo WARNING: 'Contentless FTS5 assumes\n // fresh schema — old databases are not migrated'). We detect this\n // by inspecting sqlite_master for the `contentless_delete=1` token;\n // if absent, we DROP + RECREATE the virtual table and rebuild its\n // rows from the surviving `nodes` table so the index is immediately\n // usable after migration (rule 23: characterize before moving).\n const ftsCreateSql = expectRow<{ sql: string }>(\n db\n .prepare(\n \"SELECT sql FROM sqlite_master WHERE type='table' AND name='nodes_fts'\",\n )\n .get(),\n [\"sql\"],\n );\n const needsFtsRecreate =\n !ftsCreateSql || !ftsCreateSql.sql.includes(\"contentless_delete=1\");\n if (needsFtsRecreate) {\n db.exec(\"DROP TABLE IF EXISTS nodes_fts;\");\n db.exec(`\n CREATE VIRTUAL TABLE nodes_fts USING fts5(\n name,\n qualified_name,\n id UNINDEXED,\n content='',\n contentless_delete=1,\n tokenize='unicode61 remove_diacritics 2'\n );\n `);\n // Rebuild FTS + fts_index from surviving nodes so both are populated\n // after migration. Fresh databases have zero rows so this is a no-op.\n const survivingNodes = expectRows<{\n id: string;\n name: string;\n qualified_name: string;\n }>(\n db.prepare(\"SELECT id, name, qualified_name FROM nodes\").all(),\n [\"id\", \"name\", \"qualified_name\"],\n );\n if (survivingNodes.length > 0) {\n const insertFts = db.prepare(\n \"INSERT INTO nodes_fts (rowid, name, qualified_name) VALUES (?, ?, ?)\",\n );\n const upsertFtsIndex = db.prepare(\n \"INSERT OR REPLACE INTO fts_index (fts_rowid, node_id) VALUES (?, ?)\",\n );\n for (const n of survivingNodes) {\n const rowid = ftsRowidForNodeId(n.id);\n insertFts.run(rowid, n.name, n.qualified_name);\n upsertFtsIndex.run(rowid, n.id);\n }\n }\n }\n // NOTE: the schema_version marker is written by the caller\n // (applyCodingGraphSchema / writeSchemaVersion), NOT here. createTables\n // only owns additive DDL; the version-write concern (never downgrade a\n // newer DB) lives at the apply layer where currentVersion is known.\n}\n\n/**\n * Stamp the schema_version meta row. INSERT OR REPLACE so the upgrade\n * path can rewrite the row in place (rule 23 — one canonical form).\n * Callers MUST gate this on `currentVersion <= CODING_GRAPH_SCHEMA_VERSION`\n * to avoid downgrading a newer DB.\n */\nfunction writeSchemaVersion(\n db: BetterSqlite3Database,\n version: number,\n): void {\n db.prepare(\n \"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)\",\n ).run(String(version));\n}\n\n/**\n * Read the schema_version row. Returns 0 when the table is missing or the\n * row has not been written yet — used by the upgrade stub test.\n *\n * Mirrors the LCM helper's tolerance: a missing `meta` table is not an\n * error condition here, it just means the schema has never been applied.\n */\nexport function readSchemaVersion(db: BetterSqlite3Database): number {\n const metaTable = expectRow<{ name: string }>(\n db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type='table' AND name='meta'\",\n )\n .get(),\n [\"name\"],\n );\n if (!metaTable) return 0;\n const meta = expectRow<{ value: string }>(\n db.prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\").get(),\n [\"value\"],\n );\n return meta ? parseInt(meta.value, 10) : 0;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,253 @@
1
+ import { GraphStore } from '../graph-store.js';
2
+ import '../graph-schema.js';
3
+ import '@remnic/core/runtime/better-sqlite';
4
+ import '@remnic/core/coding/coding-graph-types';
5
+
6
+ /**
7
+ * openCypher read subset — hand-written recursive-descent parser + executor.
8
+ *
9
+ * Issue #1552 PR3. This module is the thin, deletable Cypher layer over the
10
+ * structured store API (`searchGraph` / `traverse`). It compiles a strict
11
+ * read-only subset of openCypher to those primitives — there is NO SQL
12
+ * string assembly from user input anywhere; the structured API already
13
+ * parameterizes every bind (rule 51).
14
+ *
15
+ * ## Supported grammar (strict subset)
16
+ *
17
+ * ```
18
+ * query := MATCH pattern [WHERE where_clause] RETURN return_list [LIMIT int]
19
+ * pattern := node_pattern (rel_pattern node_pattern)*
20
+ * node_pattern := '(' [var] [':' label] ['{' prop_map '}'] ')'
21
+ * prop_map := key ':' literal (',' key ':' literal)*
22
+ * rel_pattern := ('<-'? '--' bracket '--' '->'?)
23
+ * | ('<-' bracket '--') // incoming: <-[...]- (also <-[...]--)
24
+ * | ('--' bracket '->') // outgoing: -[...]-> (also --[...]->)
25
+ * | ('<-'? '--' bracket '--' '->'?) // canonical form
26
+ * bracket := '[' [':' type ('|' ':' type)*] ['*' range] ']'
27
+ * range := int ('..' int)? | '..' int
28
+ * where_clause := comparison ((AND | OR) comparison)*
29
+ * comparison := var '.' key op literal
30
+ * op := '=' | '<>' | '!=' | '>' | '<' | '>=' | '<='
31
+ * return_list := return_item (',' return_item)*
32
+ * return_item := var | var '.' key
33
+ * literal := string | number | 'true' | 'false' | 'null'
34
+ * ```
35
+ *
36
+ * Direction (resolved from the dashes/arrows around the bracket):
37
+ * - `-[...]->` → outgoing (follow src→dst edges)
38
+ * - `<-[...]-` → incoming (follow dst→src edges)
39
+ * - `-[...]-` → both
40
+ *
41
+ * Variable-length hops:
42
+ * - `-[:CALLS*1..3]->` → between 1 and 3 CALLS hops (inclusive)
43
+ * - `-[:CALLS*2]->` → exactly 2 hops
44
+ * - `-[:CALLS..3]->` → 1..3 hops (default min = 1)
45
+ * - `-[:CALLS*]->` → REJECTED (unbounded — see rejection table)
46
+ *
47
+ * ## Compile target
48
+ *
49
+ * Single-node patterns compile to `searchGraph({ label })`. Fixed-length
50
+ * relationship patterns compile to `traverse({ start, direction, edgeTypes,
51
+ * maxDepth })`, filtering the returned hits to the relationship's depth
52
+ * range. VARIABLE-length patterns (`*M..N` / `*N`) compile to the path-
53
+ * enumerating primitive `traversePaths` (issue #1650) so an exact `*N`
54
+ * honors concrete length-N paths; endpoints are filtered by PATH LENGTH
55
+ * and deduped by node id. Property filters in node patterns
56
+ * (`{name: "foo"}`) and WHERE conditions are applied in JS as post-filters
57
+ * on the bound nodes.
58
+ *
59
+ * Variable-length patterns enumerate concrete relationship-simple paths via
60
+ * `traversePaths` (issue #1650). Each path is cycle-safe under RELATIONSHIP
61
+ * UNIQUENESS (a single path never reuses an edge), capped at `maxHops` and a
62
+ * total-path cap. `*M..N` returns a node when a path of length in `[M, N]`
63
+ * reaches it; exact `*N` (N > 1) thus includes a node reachable at BOTH a
64
+ * shorter and a length-N path (the length-N path qualifies). The result is
65
+ * deduped by node id, so `*1..N` ("reachable within N hops") is unchanged
66
+ * from the prior BFS behavior — only exact `*N` (N > 1) gains paths the
67
+ * shortest-depth BFS dropped. If enumeration hits the store's maxPaths cap,
68
+ * the success result carries `truncated: true` so callers can detect a
69
+ * partial endpoint set instead of silently dropping reachable nodes.
70
+ *
71
+ * ## Read-only by construction
72
+ *
73
+ * The parser only recognizes the tokens `MATCH`, `WHERE`, `RETURN`,
74
+ * `LIMIT`, `AND`, `OR`, `true`, `false`, `null`. Every write/mutation
75
+ * clause token (`CREATE`, `MERGE`, `SET`, `DELETE`, `DETACH`, `REMOVE`,
76
+ * `DROP`, `CALL`, `YIELD`, `UNION`, `WITH`, `ORDER`, `BY`, `SKIP`,
77
+ * `OPTIONAL`, `EXPLAIN`, `PROFILE`, `USE`, `FOREACH`, `LOAD`,
78
+ * `CONSTRAINT`, `INDEX`) is rejected with a clear error naming the
79
+ * supported grammar (rule 51). The module has no code path that writes
80
+ * to the store.
81
+ *
82
+ * ## Rejection table (each has a dedicated test)
83
+ *
84
+ * - `CREATE (n:Function)` → unsupported clause (read-only)
85
+ * - `MATCH (n) DELETE n` → unsupported clause
86
+ * - `MATCH (n) SET n.x = 1` → unsupported clause
87
+ * - `MATCH (a)-[:CALLS*]->(b) ...` → unbounded `*` (must specify range)
88
+ * - `MATCH (a:NotALabel) ...` → unknown label (lists valid options)
89
+ * - `MATCH (a) RETURN *` → `RETURN *` not in subset
90
+ * - `MATCH (a)-[:CALLS]->(b) RETURN a, c` → unbound variable `c`
91
+ * - `MATCH (a:Function {name: 123}) ...` → wrong-type literal matches
92
+ * nothing (standard Cypher;
93
+ * NOT a parse error — a numeric
94
+ * `name` never equals a string)
95
+ * - `MATCH (a:Function WHERE ...` → missing `)` / missing RETURN
96
+ * - `MATCH (a:Function)` (no RETURN) → missing RETURN
97
+ *
98
+ * ## Scale caveat
99
+ *
100
+ * The subset is aimed at interactive exploration over indexed graphs. The
101
+ * start-node resolution uses `searchGraph` (capped at 1000 rows by the
102
+ * store); the inline `name`/`filePath` property filter and a supported
103
+ * single-conjunction first-variable WHERE equality term are pushed down
104
+ * to the index BEFORE the cap, so an exact-name lookup is found even when
105
+ * the matching node sorts after the cap on a large graph. Values
106
+ * containing LIKE metacharacters (`%`/`_`) and multi-group (OR) WHERE
107
+ * clauses are NOT pushed down — they fall back to the capped scan, so on
108
+ * graphs with more than 1000 nodes of the starting label such queries can
109
+ * still false-negative; use the inline literal form for guaranteed
110
+ * Relationship expansion uses `traverse` (fixed hops) or `traversePaths`
111
+ * (variable length, issue #1650); both are cycle-safe and depth/length
112
+ * capped. See the compile-target note for the path-length semantics of
113
+ * variable-length `*N`.
114
+ */
115
+
116
+ /**
117
+ * PascalCase Cypher label → the lowercase value stored in `nodes.label`.
118
+ * Labels whose DB form is not produced by ingest still map to a sensible
119
+ * storage key so the query is structurally valid (returns empty).
120
+ */
121
+ declare const CYPHER_LABEL_TO_DB_LABEL: Record<string, string>;
122
+ /** Sorted list of accepted Cypher labels — used in rejection messages. */
123
+ declare const VALID_CYPHER_LABELS: readonly string[];
124
+ /**
125
+ * A value projected by RETURN. Strings, numbers, booleans, or null. Whole
126
+ * nodes are returned as {@link CypherNodeValue} so callers can read every
127
+ * field without re-querying.
128
+ */
129
+ type CypherScalar = string | number | boolean | null;
130
+ /** A whole-node value (RETURN `var` with no property). */
131
+ interface CypherNodeValue {
132
+ nodeId: string;
133
+ qualifiedName: string;
134
+ name: string;
135
+ label: string;
136
+ filePath: string;
137
+ }
138
+ type CypherValue = CypherScalar | CypherNodeValue;
139
+ /** One result row — a map from RETURN-item column name to its value. */
140
+ type CypherRow = Record<string, CypherValue>;
141
+ /** Failure codes. Distinct from the store codes — Cypher has its own. */
142
+ type CypherFailureCode = "parse_error" | "unsupported_clause" | "unknown_label" | "unbound_variable" | "invalid_query" | "store_closed" | "db_locked" | "db_corrupt" | "db_error";
143
+ interface CypherFailure {
144
+ ok: false;
145
+ code: CypherFailureCode;
146
+ /** Human-readable explanation including the supported grammar hint. */
147
+ message: string;
148
+ /**
149
+ * Present only for `unknown_label`: the accepted label list, so callers
150
+ * can render a completion menu without re-deriving it.
151
+ */
152
+ validLabels?: readonly string[];
153
+ }
154
+ interface CypherSuccess {
155
+ ok: true;
156
+ /** Column names in RETURN order; each row has these keys. */
157
+ columns: string[];
158
+ rows: CypherRow[];
159
+ /**
160
+ * Present and `true` ONLY when a variable-length expansion hit the
161
+ * `traversePaths` maxPaths cap — the rows are a PARTIAL endpoint set and
162
+ * some reachable nodes may be omitted. Callers that must know the result
163
+ * is complete should treat `truncated: true` as unreliable. Absent means
164
+ * the enumeration completed (issue #1650).
165
+ */
166
+ truncated?: boolean;
167
+ }
168
+ type CypherResult = CypherSuccess | CypherFailure;
169
+ interface NodePattern {
170
+ /** Variable name; undefined for an anonymous node `( )`. */
171
+ varName?: string;
172
+ /** PascalCase label as written (`Function`, `Class`, ...). */
173
+ label?: string;
174
+ /** Inline property filters from `{key: value, ...}`. */
175
+ properties: Array<{
176
+ key: string;
177
+ value: CypherScalar;
178
+ }>;
179
+ }
180
+ type RelDirection = "outgoing" | "incoming" | "both";
181
+ interface RelPattern {
182
+ direction: RelDirection;
183
+ /** Edge types; empty means "any type". */
184
+ types: string[];
185
+ /** Inclusive minimum hop count (default 1). */
186
+ minHops: number;
187
+ /** Inclusive maximum hop count. Equal to minHops when `*N` form used. */
188
+ maxHops: number;
189
+ /** True when a `*` range was parsed (variable-length). Drives the path-enumerating compile target (issue #1650). */
190
+ isVarLength: boolean;
191
+ }
192
+ interface Comparison {
193
+ varName: string;
194
+ key: string;
195
+ op: "=" | "<>" | "!=" | ">" | "<" | ">=" | "<=";
196
+ value: CypherScalar;
197
+ }
198
+ type WhereTerm = Comparison;
199
+ interface WhereClause {
200
+ /** Flat OR-of-AND-of-terms. We support AND+OR; no precedence gymnastics. */
201
+ orGroups: WhereTerm[][];
202
+ }
203
+ type ReturnItem = {
204
+ kind: "var";
205
+ varName: string;
206
+ } | {
207
+ kind: "prop";
208
+ varName: string;
209
+ key: string;
210
+ };
211
+ interface MatchClause {
212
+ nodes: NodePattern[];
213
+ rels: RelPattern[];
214
+ }
215
+ interface CypherAst {
216
+ match: MatchClause;
217
+ where?: WhereClause;
218
+ return: ReturnItem[];
219
+ limit?: number;
220
+ }
221
+ declare class CypherParseError extends Error {
222
+ readonly code: CypherFailureCode;
223
+ readonly pos: number;
224
+ readonly validLabels?: readonly string[];
225
+ constructor(pos: number, message: string, code?: CypherFailureCode, validLabels?: readonly string[]);
226
+ }
227
+ type CypherParseResult = {
228
+ ok: true;
229
+ ast: CypherAst;
230
+ } | CypherFailure;
231
+ /**
232
+ * Parse a Cypher query string into an AST without executing it. Use this
233
+ * to validate query shape (e.g. at a tool boundary) before opening a
234
+ * store. The AST is an opaque internal type; callers should treat it as
235
+ * a handle to pass to {@link executeAst}.
236
+ */
237
+ declare function parseCypher(query: string): CypherParseResult;
238
+ /**
239
+ * Execute a parsed AST against a store. Exposed so callers that already
240
+ * hold an AST (e.g. a cached plan) can skip re-parsing.
241
+ */
242
+ declare function executeAst(store: GraphStore, ast: CypherAst): CypherResult;
243
+ /**
244
+ * Parse and execute a Cypher query against a store. Convenience wrapper
245
+ * around {@link parseCypher} + {@link executeAst}.
246
+ *
247
+ * @example
248
+ * const r = executeCypher(store, 'MATCH (f:Function {name: "foo"})-[:CALLS*1..2]->(g) WHERE g.label = "function" RETURN f.name, g.qualifiedName LIMIT 5');
249
+ * if (r.ok) for (const row of r.rows) console.log(row);
250
+ */
251
+ declare function executeCypher(store: GraphStore, query: string): CypherResult;
252
+
253
+ export { CYPHER_LABEL_TO_DB_LABEL, type CypherAst, type CypherFailure, type CypherFailureCode, type CypherNodeValue, CypherParseError, type CypherParseResult, type CypherResult, type CypherRow, type CypherScalar, type CypherSuccess, type CypherValue, VALID_CYPHER_LABELS, executeAst, executeCypher, parseCypher };
@@ -0,0 +1,17 @@
1
+ import {
2
+ CYPHER_LABEL_TO_DB_LABEL,
3
+ VALID_CYPHER_LABELS,
4
+ executeAst,
5
+ executeCypher,
6
+ parseCypher
7
+ } from "../chunk-5I2DBHOQ.js";
8
+ import "../chunk-CPYJACC5.js";
9
+ import "../chunk-ZVCMIM4T.js";
10
+ export {
11
+ CYPHER_LABEL_TO_DB_LABEL,
12
+ VALID_CYPHER_LABELS,
13
+ executeAst,
14
+ executeCypher,
15
+ parseCypher
16
+ };
17
+ //# sourceMappingURL=query-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,84 @@
1
+ import { BetterSqlite3Database } from '@remnic/core/runtime/better-sqlite';
2
+
3
+ /**
4
+ * Coding-graph SQLite schema — versioned meta + tables + FTS5 virtual table.
5
+ *
6
+ * Issue #1552 PR1 (Track B, Phase 1). The schema + write pipeline only;
7
+ * traversal, search, dead-code and the openCypher subset land in PR2/PR3.
8
+ *
9
+ * PR2 additive table (`node_attributes`): tracks per-node exclusion flags
10
+ * consumed by `deadCode()` — `is_exported`, `is_route_handler`. Added in
11
+ * PR2 (issue #1552 step 5) as a SEPARATE table rather than ALTER TABLE on
12
+ * `nodes`, so existing v1 databases gain the table via the same
13
+ * `CREATE TABLE IF NOT EXISTS` pass without a schema-version bump or a
14
+ * migration (rule 23 — additive, characterized before moving).
15
+ *
16
+ * Design anchors:
17
+ * - `packages/remnic-core/src/lcm/schema.ts` (versioning + pragmas) —
18
+ * copied VERBATIM for the WAL / busy_timeout / synchronous pragmas and
19
+ * the meta-table version row. Do not invent a new pattern (rule 23/38).
20
+ * - `packages/remnic-core/src/runtime/better-sqlite.ts` (`openBetterSqlite3`)
21
+ * — the shared opener; native-binding lifecycle is paid for once there.
22
+ * - issue://1552 "Design" — node ids hash sorted key material; file
23
+ * contents are NEVER stored (only spans + content hashes); FTS5 covers
24
+ * node names; provenance CHECK enforces the four-value enum.
25
+ *
26
+ * Schema versioning:
27
+ * The schema_version row lives in `meta` (a generic key/value table, not
28
+ * lcm_meta — independent store, separate version namespace). Fresh DB →
29
+ * schema_version=1. Upgrade stub: meta v0 → v1 (rule 23, characterize
30
+ * before moving).
31
+ *
32
+ * Dangling-edge policy (PR1 decision):
33
+ * When `upsertFileBatch` deletes a file's prior nodes + edges, cross-file
34
+ * edges whose `dst` was a node owned by the deleted file become dangling.
35
+ * We DROP them. They are counted in `UpsertResult.droppedDanglingEdges`
36
+ * so callers can surface the loss. Keeping them with a `dst_unresolved`
37
+ * marker would leak orphans and bias `traverse()` results — drop is the
38
+ * conservative choice for a write pipeline whose caller knows the
39
+ * canonical file set on each batch (rule 11, 40).
40
+ */
41
+
42
+ declare const CODING_GRAPH_SCHEMA_VERSION = 1;
43
+ /**
44
+ * FTS5 rowid derived from a deterministic node id. FTS5 rowids are
45
+ * signed 64-bit integers; we slice the leading 16 hex chars (= 64 bits)
46
+ * of the sha256 id and mask to the int64 positive range so SQLite
47
+ * accepts it. The full 64-bit space is large enough that collisions
48
+ * across distinct node ids are negligible. Contentless FTS5
49
+ * (`content=''`) does NOT store UNINDEXED column values, so the only
50
+ * reliable key into the virtual table is the rowid.
51
+ *
52
+ * Lives in graph-schema (not graph-store) so the schema migration path
53
+ * can rebuild FTS rows from existing `nodes` without importing the
54
+ * store module (which would create a circular dependency — graph-store
55
+ * imports graph-schema).
56
+ */
57
+ declare function ftsRowidForNodeId(nodeId: string): bigint;
58
+ /**
59
+ * Provenance enum for edges — mirrors #1552's `heuristic|lsp|trace|semantic`
60
+ * whitelist. The CHECK constraint rejects writes outside this set so a
61
+ * buggy resolver can't sneak in unknown values (rule 23).
62
+ */
63
+ declare const EDGE_PROVENANCE_VALUES: readonly ["heuristic", "lsp", "trace", "semantic"];
64
+ type EdgeProvenance = (typeof EDGE_PROVENANCE_VALUES)[number];
65
+ declare function isEdgeProvenance(value: unknown): value is EdgeProvenance;
66
+ /**
67
+ * Apply (or upgrade) the coding-graph schema on an already-open SQLite
68
+ * handle. Public so test seams and migration tools can bootstrap an
69
+ * in-memory database without going through {@link openCodingGraphDatabase}.
70
+ *
71
+ * Mirrors `applyLcmSchema` in `packages/remnic-core/src/lcm/schema.ts` —
72
+ * distinct function, same shape, separate version namespace.
73
+ */
74
+ declare function applyCodingGraphSchema(db: BetterSqlite3Database): void;
75
+ /**
76
+ * Read the schema_version row. Returns 0 when the table is missing or the
77
+ * row has not been written yet — used by the upgrade stub test.
78
+ *
79
+ * Mirrors the LCM helper's tolerance: a missing `meta` table is not an
80
+ * error condition here, it just means the schema has never been applied.
81
+ */
82
+ declare function readSchemaVersion(db: BetterSqlite3Database): number;
83
+
84
+ export { CODING_GRAPH_SCHEMA_VERSION, EDGE_PROVENANCE_VALUES, type EdgeProvenance, applyCodingGraphSchema, ftsRowidForNodeId, isEdgeProvenance, readSchemaVersion };
@@ -0,0 +1,17 @@
1
+ import {
2
+ CODING_GRAPH_SCHEMA_VERSION,
3
+ EDGE_PROVENANCE_VALUES,
4
+ applyCodingGraphSchema,
5
+ ftsRowidForNodeId,
6
+ isEdgeProvenance,
7
+ readSchemaVersion
8
+ } from "./chunk-ZVCMIM4T.js";
9
+ export {
10
+ CODING_GRAPH_SCHEMA_VERSION,
11
+ EDGE_PROVENANCE_VALUES,
12
+ applyCodingGraphSchema,
13
+ ftsRowidForNodeId,
14
+ isEdgeProvenance,
15
+ readSchemaVersion
16
+ };
17
+ //# sourceMappingURL=graph-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}