@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.
- package/README.md +130 -0
- package/dist/chunk-5I2DBHOQ.js +1042 -0
- package/dist/chunk-5I2DBHOQ.js.map +1 -0
- package/dist/chunk-CPYJACC5.js +1838 -0
- package/dist/chunk-CPYJACC5.js.map +1 -0
- package/dist/chunk-ZVCMIM4T.js +216 -0
- package/dist/chunk-ZVCMIM4T.js.map +1 -0
- package/dist/cypher/query-parser.d.ts +253 -0
- package/dist/cypher/query-parser.js +17 -0
- package/dist/cypher/query-parser.js.map +1 -0
- package/dist/graph-schema.d.ts +84 -0
- package/dist/graph-schema.js +17 -0
- package/dist/graph-schema.js.map +1 -0
- package/dist/graph-store.d.ts +938 -0
- package/dist/graph-store.js +16 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/index.d.ts +1953 -0
- package/dist/index.js +3509 -0
- package/dist/index.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +79 -0
- package/src/co-change.test.ts +175 -0
- package/src/co-change.ts +167 -0
- package/src/cypher/query-parser.test.ts +1107 -0
- package/src/cypher/query-parser.ts +1692 -0
- package/src/detect-changes.test.ts +533 -0
- package/src/detect-changes.ts +367 -0
- package/src/engine/emit.ts +556 -0
- package/src/engine/engine.test.ts +1417 -0
- package/src/engine/engine.ts +182 -0
- package/src/engine/extractors.ts +486 -0
- package/src/engine/fixtures.ts +364 -0
- package/src/engine/language-sniff.ts +56 -0
- package/src/engine/parser-backend.ts +206 -0
- package/src/engine/utf16-offsets.ts +68 -0
- package/src/git-invoker.test.ts +116 -0
- package/src/git-invoker.ts +426 -0
- package/src/graph-schema.test.ts +541 -0
- package/src/graph-schema.ts +383 -0
- package/src/graph-store-pr2.test.ts +1879 -0
- package/src/graph-store.test.ts +1420 -0
- package/src/graph-store.ts +3489 -0
- package/src/index-status.test.ts +303 -0
- package/src/index-status.ts +135 -0
- package/src/index.ts +384 -0
- package/src/lsp/byte-position.ts +173 -0
- package/src/lsp/characterization.test.ts +174 -0
- package/src/lsp/client.test.ts +275 -0
- package/src/lsp/client.ts +484 -0
- package/src/lsp/config.ts +219 -0
- package/src/lsp/degradation.ts +86 -0
- package/src/lsp/fixtures/fake-server.mjs +198 -0
- package/src/lsp/framing.test.ts +180 -0
- package/src/lsp/framing.ts +177 -0
- package/src/lsp/resolution.test.ts +497 -0
- package/src/lsp/resolution.ts +483 -0
- package/src/lsp/status.ts +140 -0
- package/src/lsp/types.ts +167 -0
- package/src/reindex.test.ts +1038 -0
- package/src/reindex.ts +908 -0
- package/src/row-types.ts +45 -0
- package/src/semantic/canonical-text.test.ts +150 -0
- package/src/semantic/canonical-text.ts +219 -0
- package/src/semantic/config.ts +235 -0
- package/src/semantic/index.ts +78 -0
- package/src/semantic/minhash.test.ts +197 -0
- package/src/semantic/minhash.ts +261 -0
- package/src/semantic/semantic-query.ts +173 -0
- package/src/semantic/semantic.test.ts +1315 -0
- package/src/semantic/similarity.ts +268 -0
- package/src/semantic/types.ts +145 -0
- 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":[]}
|