ai-mind-map 1.1.0

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/change-tracker/change-log.d.ts +160 -0
  4. package/dist/change-tracker/change-log.d.ts.map +1 -0
  5. package/dist/change-tracker/change-log.js +507 -0
  6. package/dist/change-tracker/change-log.js.map +1 -0
  7. package/dist/change-tracker/diff-engine.d.ts +149 -0
  8. package/dist/change-tracker/diff-engine.d.ts.map +1 -0
  9. package/dist/change-tracker/diff-engine.js +530 -0
  10. package/dist/change-tracker/diff-engine.js.map +1 -0
  11. package/dist/change-tracker/watcher.d.ts +137 -0
  12. package/dist/change-tracker/watcher.d.ts.map +1 -0
  13. package/dist/change-tracker/watcher.js +300 -0
  14. package/dist/change-tracker/watcher.js.map +1 -0
  15. package/dist/cli.d.ts +20 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +937 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/config.d.ts +38 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +222 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/context/compressor.d.ts +49 -0
  24. package/dist/context/compressor.d.ts.map +1 -0
  25. package/dist/context/compressor.js +769 -0
  26. package/dist/context/compressor.js.map +1 -0
  27. package/dist/context/progressive-disclosure.d.ts +71 -0
  28. package/dist/context/progressive-disclosure.d.ts.map +1 -0
  29. package/dist/context/progressive-disclosure.js +470 -0
  30. package/dist/context/progressive-disclosure.js.map +1 -0
  31. package/dist/context/token-budget.d.ts +121 -0
  32. package/dist/context/token-budget.d.ts.map +1 -0
  33. package/dist/context/token-budget.js +282 -0
  34. package/dist/context/token-budget.js.map +1 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +944 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/install.d.ts +66 -0
  40. package/dist/install.d.ts.map +1 -0
  41. package/dist/install.js +946 -0
  42. package/dist/install.js.map +1 -0
  43. package/dist/knowledge-graph/architecture.d.ts +213 -0
  44. package/dist/knowledge-graph/architecture.d.ts.map +1 -0
  45. package/dist/knowledge-graph/architecture.js +585 -0
  46. package/dist/knowledge-graph/architecture.js.map +1 -0
  47. package/dist/knowledge-graph/cypher.d.ts +113 -0
  48. package/dist/knowledge-graph/cypher.d.ts.map +1 -0
  49. package/dist/knowledge-graph/cypher.js +1051 -0
  50. package/dist/knowledge-graph/cypher.js.map +1 -0
  51. package/dist/knowledge-graph/dead-code.d.ts +121 -0
  52. package/dist/knowledge-graph/dead-code.d.ts.map +1 -0
  53. package/dist/knowledge-graph/dead-code.js +331 -0
  54. package/dist/knowledge-graph/dead-code.js.map +1 -0
  55. package/dist/knowledge-graph/flow-analyzer.d.ts +167 -0
  56. package/dist/knowledge-graph/flow-analyzer.d.ts.map +1 -0
  57. package/dist/knowledge-graph/flow-analyzer.js +739 -0
  58. package/dist/knowledge-graph/flow-analyzer.js.map +1 -0
  59. package/dist/knowledge-graph/graph.d.ts +291 -0
  60. package/dist/knowledge-graph/graph.d.ts.map +1 -0
  61. package/dist/knowledge-graph/graph.js +978 -0
  62. package/dist/knowledge-graph/graph.js.map +1 -0
  63. package/dist/knowledge-graph/index.d.ts +17 -0
  64. package/dist/knowledge-graph/index.d.ts.map +1 -0
  65. package/dist/knowledge-graph/index.js +14 -0
  66. package/dist/knowledge-graph/index.js.map +1 -0
  67. package/dist/knowledge-graph/indexer.d.ts +112 -0
  68. package/dist/knowledge-graph/indexer.d.ts.map +1 -0
  69. package/dist/knowledge-graph/indexer.js +506 -0
  70. package/dist/knowledge-graph/indexer.js.map +1 -0
  71. package/dist/knowledge-graph/pagerank.d.ts +141 -0
  72. package/dist/knowledge-graph/pagerank.d.ts.map +1 -0
  73. package/dist/knowledge-graph/pagerank.js +493 -0
  74. package/dist/knowledge-graph/pagerank.js.map +1 -0
  75. package/dist/knowledge-graph/parser.d.ts +55 -0
  76. package/dist/knowledge-graph/parser.d.ts.map +1 -0
  77. package/dist/knowledge-graph/parser.js +1090 -0
  78. package/dist/knowledge-graph/parser.js.map +1 -0
  79. package/dist/knowledge-graph/snapshot.d.ts +107 -0
  80. package/dist/knowledge-graph/snapshot.d.ts.map +1 -0
  81. package/dist/knowledge-graph/snapshot.js +435 -0
  82. package/dist/knowledge-graph/snapshot.js.map +1 -0
  83. package/dist/memory/decision-log.d.ts +151 -0
  84. package/dist/memory/decision-log.d.ts.map +1 -0
  85. package/dist/memory/decision-log.js +482 -0
  86. package/dist/memory/decision-log.js.map +1 -0
  87. package/dist/memory/persistent-memory.d.ts +182 -0
  88. package/dist/memory/persistent-memory.d.ts.map +1 -0
  89. package/dist/memory/persistent-memory.js +579 -0
  90. package/dist/memory/persistent-memory.js.map +1 -0
  91. package/dist/memory/session-memory.d.ts +165 -0
  92. package/dist/memory/session-memory.d.ts.map +1 -0
  93. package/dist/memory/session-memory.js +382 -0
  94. package/dist/memory/session-memory.js.map +1 -0
  95. package/dist/stress-test.d.ts +10 -0
  96. package/dist/stress-test.d.ts.map +1 -0
  97. package/dist/stress-test.js +258 -0
  98. package/dist/stress-test.js.map +1 -0
  99. package/dist/tools/advanced-tools.d.ts +32 -0
  100. package/dist/tools/advanced-tools.d.ts.map +1 -0
  101. package/dist/tools/advanced-tools.js +480 -0
  102. package/dist/tools/advanced-tools.js.map +1 -0
  103. package/dist/tools/change-tools.d.ts +76 -0
  104. package/dist/tools/change-tools.d.ts.map +1 -0
  105. package/dist/tools/change-tools.js +93 -0
  106. package/dist/tools/change-tools.js.map +1 -0
  107. package/dist/tools/context-tools.d.ts +68 -0
  108. package/dist/tools/context-tools.d.ts.map +1 -0
  109. package/dist/tools/context-tools.js +141 -0
  110. package/dist/tools/context-tools.js.map +1 -0
  111. package/dist/tools/debug-tools.d.ts +25 -0
  112. package/dist/tools/debug-tools.d.ts.map +1 -0
  113. package/dist/tools/debug-tools.js +286 -0
  114. package/dist/tools/debug-tools.js.map +1 -0
  115. package/dist/tools/evolving-tools.d.ts +23 -0
  116. package/dist/tools/evolving-tools.d.ts.map +1 -0
  117. package/dist/tools/evolving-tools.js +207 -0
  118. package/dist/tools/evolving-tools.js.map +1 -0
  119. package/dist/tools/flow-tools.d.ts +24 -0
  120. package/dist/tools/flow-tools.d.ts.map +1 -0
  121. package/dist/tools/flow-tools.js +265 -0
  122. package/dist/tools/flow-tools.js.map +1 -0
  123. package/dist/tools/graph-tools.d.ts +71 -0
  124. package/dist/tools/graph-tools.d.ts.map +1 -0
  125. package/dist/tools/graph-tools.js +165 -0
  126. package/dist/tools/graph-tools.js.map +1 -0
  127. package/dist/tools/memory-tools.d.ts +62 -0
  128. package/dist/tools/memory-tools.d.ts.map +1 -0
  129. package/dist/tools/memory-tools.js +195 -0
  130. package/dist/tools/memory-tools.js.map +1 -0
  131. package/dist/tools/smart-tools.d.ts +23 -0
  132. package/dist/tools/smart-tools.d.ts.map +1 -0
  133. package/dist/tools/smart-tools.js +482 -0
  134. package/dist/tools/smart-tools.js.map +1 -0
  135. package/dist/tools/snapshot-tools.d.ts +19 -0
  136. package/dist/tools/snapshot-tools.d.ts.map +1 -0
  137. package/dist/tools/snapshot-tools.js +149 -0
  138. package/dist/tools/snapshot-tools.js.map +1 -0
  139. package/dist/types.d.ts +181 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +45 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/utils/logger.d.ts +59 -0
  144. package/dist/utils/logger.d.ts.map +1 -0
  145. package/dist/utils/logger.js +142 -0
  146. package/dist/utils/logger.js.map +1 -0
  147. package/dist/utils/token-counter.d.ts +51 -0
  148. package/dist/utils/token-counter.d.ts.map +1 -0
  149. package/dist/utils/token-counter.js +181 -0
  150. package/dist/utils/token-counter.js.map +1 -0
  151. package/install.ps1 +321 -0
  152. package/install.sh +345 -0
  153. package/package.json +94 -0
  154. package/setup.bat +62 -0
@@ -0,0 +1,1051 @@
1
+ /**
2
+ * AI Mind Map — Mini Cypher Query Parser & Executor
3
+ *
4
+ * A read-only openCypher subset that compiles Cypher queries into SQL
5
+ * targeting the knowledge graph's `nodes` and `edges` tables in SQLite.
6
+ *
7
+ * Inspired by codebase-memory-mcp's Cypher-like query interface.
8
+ *
9
+ * Supported Cypher syntax:
10
+ * MATCH (f:Function) RETURN f.name LIMIT 10
11
+ * MATCH (f:Function)-[:CALLS]->(g:Function) WHERE f.name = 'auth' RETURN f.name, g.name
12
+ * MATCH (f:Function) WHERE f.filePath CONTAINS 'auth' RETURN f.name, f.signature
13
+ * MATCH (f:Function) WHERE NOT EXISTS { (f)<-[:CALLS]-() } RETURN f.name
14
+ */
15
+ // ============================================================
16
+ // Token Types
17
+ // ============================================================
18
+ var TokenType;
19
+ (function (TokenType) {
20
+ // Keywords
21
+ TokenType["MATCH"] = "MATCH";
22
+ TokenType["WHERE"] = "WHERE";
23
+ TokenType["RETURN"] = "RETURN";
24
+ TokenType["ORDER"] = "ORDER";
25
+ TokenType["BY"] = "BY";
26
+ TokenType["LIMIT"] = "LIMIT";
27
+ TokenType["DISTINCT"] = "DISTINCT";
28
+ TokenType["AND"] = "AND";
29
+ TokenType["OR"] = "OR";
30
+ TokenType["NOT"] = "NOT";
31
+ TokenType["IN"] = "IN";
32
+ TokenType["CONTAINS"] = "CONTAINS";
33
+ TokenType["STARTS"] = "STARTS";
34
+ TokenType["ENDS"] = "ENDS";
35
+ TokenType["WITH"] = "WITH";
36
+ TokenType["EXISTS"] = "EXISTS";
37
+ TokenType["ASC"] = "ASC";
38
+ TokenType["DESC"] = "DESC";
39
+ TokenType["AS"] = "AS";
40
+ TokenType["TRUE"] = "TRUE";
41
+ TokenType["FALSE"] = "FALSE";
42
+ TokenType["NULL"] = "NULL";
43
+ // Literals & identifiers
44
+ TokenType["IDENTIFIER"] = "IDENTIFIER";
45
+ TokenType["STRING"] = "STRING";
46
+ TokenType["NUMBER"] = "NUMBER";
47
+ // Symbols
48
+ TokenType["LPAREN"] = "LPAREN";
49
+ TokenType["RPAREN"] = "RPAREN";
50
+ TokenType["LBRACKET"] = "LBRACKET";
51
+ TokenType["RBRACKET"] = "RBRACKET";
52
+ TokenType["LBRACE"] = "LBRACE";
53
+ TokenType["RBRACE"] = "RBRACE";
54
+ TokenType["COLON"] = "COLON";
55
+ TokenType["DOT"] = "DOT";
56
+ TokenType["COMMA"] = "COMMA";
57
+ TokenType["DASH"] = "DASH";
58
+ TokenType["GT"] = "GT";
59
+ TokenType["LT"] = "LT";
60
+ TokenType["EQ"] = "EQ";
61
+ TokenType["NEQ"] = "NEQ";
62
+ TokenType["GTE"] = "GTE";
63
+ TokenType["LTE"] = "LTE";
64
+ TokenType["ARROW_RIGHT"] = "ARROW_RIGHT";
65
+ TokenType["ARROW_LEFT"] = "ARROW_LEFT";
66
+ TokenType["STAR"] = "STAR";
67
+ // End
68
+ TokenType["EOF"] = "EOF";
69
+ })(TokenType || (TokenType = {}));
70
+ // ============================================================
71
+ // Property name → SQL column name mapping
72
+ // ============================================================
73
+ /** Map Cypher property names to actual SQLite column names */
74
+ const PROPERTY_TO_COLUMN = {
75
+ name: 'name',
76
+ type: 'type',
77
+ id: 'id',
78
+ qualifiedName: 'qualifiedName',
79
+ filePath: 'filePath',
80
+ startLine: 'startLine',
81
+ endLine: 'endLine',
82
+ signature: 'signature',
83
+ docComment: 'docComment',
84
+ hash: 'hash',
85
+ language: 'language',
86
+ visibility: 'visibility',
87
+ isAsync: 'isAsync',
88
+ isStatic: 'isStatic',
89
+ isExported: 'isExported',
90
+ parameters: 'parameters',
91
+ returnType: 'returnType',
92
+ updatedAt: 'updatedAt',
93
+ };
94
+ /** Map Cypher labels (case-insensitive lookup) to node type values in the database */
95
+ const LABEL_TO_TYPE = {
96
+ file: 'file',
97
+ function: 'function',
98
+ class: 'class',
99
+ method: 'method',
100
+ interface: 'interface',
101
+ type_alias: 'type_alias',
102
+ typealias: 'type_alias',
103
+ enum: 'enum',
104
+ variable: 'variable',
105
+ constant: 'constant',
106
+ module: 'module',
107
+ namespace: 'namespace',
108
+ property: 'property',
109
+ constructor: 'constructor',
110
+ decorator: 'decorator',
111
+ route: 'route',
112
+ component: 'component',
113
+ hook: 'hook',
114
+ test: 'test',
115
+ config: 'config',
116
+ };
117
+ /** Map Cypher edge type labels (case-insensitive) to edge type values */
118
+ const EDGE_LABEL_TO_TYPE = {
119
+ calls: 'calls',
120
+ imports: 'imports',
121
+ exports: 'exports',
122
+ inherits: 'inherits',
123
+ implements: 'implements',
124
+ uses: 'uses',
125
+ decorates: 'decorates',
126
+ overrides: 'overrides',
127
+ contains: 'contains',
128
+ tests: 'tests',
129
+ depends_on: 'depends_on',
130
+ dependson: 'depends_on',
131
+ routes_to: 'routes_to',
132
+ routesto: 'routes_to',
133
+ };
134
+ // ============================================================
135
+ // Lexer
136
+ // ============================================================
137
+ const KEYWORDS = new Set([
138
+ 'MATCH', 'WHERE', 'RETURN', 'ORDER', 'BY', 'LIMIT', 'DISTINCT',
139
+ 'AND', 'OR', 'NOT', 'IN', 'CONTAINS', 'STARTS', 'ENDS', 'WITH',
140
+ 'EXISTS', 'ASC', 'DESC', 'AS', 'TRUE', 'FALSE', 'NULL',
141
+ 'CREATE', 'MERGE', 'SET', 'DELETE', 'REMOVE', 'DETACH',
142
+ ]);
143
+ /** Forbidden write keywords — we only allow read-only queries */
144
+ const WRITE_KEYWORDS = new Set([
145
+ 'CREATE', 'MERGE', 'SET', 'DELETE', 'REMOVE', 'DETACH',
146
+ ]);
147
+ /**
148
+ * Tokenize a Cypher query string into a list of tokens.
149
+ */
150
+ function tokenize(input) {
151
+ const tokens = [];
152
+ let pos = 0;
153
+ while (pos < input.length) {
154
+ // Skip whitespace
155
+ if (/\s/.test(input[pos])) {
156
+ pos++;
157
+ continue;
158
+ }
159
+ // Skip line comments //
160
+ if (input[pos] === '/' && input[pos + 1] === '/') {
161
+ while (pos < input.length && input[pos] !== '\n')
162
+ pos++;
163
+ continue;
164
+ }
165
+ const startPos = pos;
166
+ // String literals (single or double quoted)
167
+ if (input[pos] === '\'' || input[pos] === '"') {
168
+ const quote = input[pos];
169
+ pos++;
170
+ let value = '';
171
+ while (pos < input.length && input[pos] !== quote) {
172
+ if (input[pos] === '\\' && pos + 1 < input.length) {
173
+ pos++;
174
+ if (input[pos] === 'n')
175
+ value += '\n';
176
+ else if (input[pos] === 't')
177
+ value += '\t';
178
+ else
179
+ value += input[pos];
180
+ }
181
+ else {
182
+ value += input[pos];
183
+ }
184
+ pos++;
185
+ }
186
+ if (pos >= input.length) {
187
+ throw new CypherSyntaxError(`Unterminated string literal`, startPos);
188
+ }
189
+ pos++; // consume closing quote
190
+ tokens.push({ type: TokenType.STRING, value, position: startPos });
191
+ continue;
192
+ }
193
+ // Numbers (integers and decimals)
194
+ if (/\d/.test(input[pos]) || (input[pos] === '-' && pos + 1 < input.length && /\d/.test(input[pos + 1]) && (tokens.length === 0 || [TokenType.EQ, TokenType.NEQ, TokenType.GT, TokenType.LT, TokenType.GTE, TokenType.LTE, TokenType.COMMA, TokenType.LPAREN, TokenType.AND, TokenType.OR].includes(tokens[tokens.length - 1].type)))) {
195
+ let numStr = '';
196
+ if (input[pos] === '-') {
197
+ numStr = '-';
198
+ pos++;
199
+ }
200
+ while (pos < input.length && /[\d.]/.test(input[pos])) {
201
+ numStr += input[pos];
202
+ pos++;
203
+ }
204
+ tokens.push({ type: TokenType.NUMBER, value: numStr, position: startPos });
205
+ continue;
206
+ }
207
+ // Identifiers and keywords
208
+ if (/[a-zA-Z_]/.test(input[pos])) {
209
+ let ident = '';
210
+ while (pos < input.length && /[a-zA-Z0-9_]/.test(input[pos])) {
211
+ ident += input[pos];
212
+ pos++;
213
+ }
214
+ const upper = ident.toUpperCase();
215
+ if (WRITE_KEYWORDS.has(upper)) {
216
+ throw new CypherSyntaxError(`Write operations are not supported. This is a read-only query engine. Found: ${ident}`, startPos);
217
+ }
218
+ if (KEYWORDS.has(upper)) {
219
+ tokens.push({ type: upper, value: ident, position: startPos });
220
+ }
221
+ else {
222
+ tokens.push({ type: TokenType.IDENTIFIER, value: ident, position: startPos });
223
+ }
224
+ continue;
225
+ }
226
+ // Multi-character operators
227
+ if (input[pos] === '-' && input[pos + 1] === '>') {
228
+ tokens.push({ type: TokenType.ARROW_RIGHT, value: '->', position: startPos });
229
+ pos += 2;
230
+ continue;
231
+ }
232
+ if (input[pos] === '<' && input[pos + 1] === '-') {
233
+ tokens.push({ type: TokenType.ARROW_LEFT, value: '<-', position: startPos });
234
+ pos += 2;
235
+ continue;
236
+ }
237
+ if (input[pos] === '<' && input[pos + 1] === '>') {
238
+ tokens.push({ type: TokenType.NEQ, value: '<>', position: startPos });
239
+ pos += 2;
240
+ continue;
241
+ }
242
+ if (input[pos] === '>' && input[pos + 1] === '=') {
243
+ tokens.push({ type: TokenType.GTE, value: '>=', position: startPos });
244
+ pos += 2;
245
+ continue;
246
+ }
247
+ if (input[pos] === '<' && input[pos + 1] === '=') {
248
+ tokens.push({ type: TokenType.LTE, value: '<=', position: startPos });
249
+ pos += 2;
250
+ continue;
251
+ }
252
+ // Single-character tokens
253
+ const singleCharMap = {
254
+ '(': TokenType.LPAREN,
255
+ ')': TokenType.RPAREN,
256
+ '[': TokenType.LBRACKET,
257
+ ']': TokenType.RBRACKET,
258
+ '{': TokenType.LBRACE,
259
+ '}': TokenType.RBRACE,
260
+ ':': TokenType.COLON,
261
+ '.': TokenType.DOT,
262
+ ',': TokenType.COMMA,
263
+ '-': TokenType.DASH,
264
+ '>': TokenType.GT,
265
+ '<': TokenType.LT,
266
+ '=': TokenType.EQ,
267
+ '*': TokenType.STAR,
268
+ };
269
+ if (singleCharMap[input[pos]]) {
270
+ tokens.push({ type: singleCharMap[input[pos]], value: input[pos], position: startPos });
271
+ pos++;
272
+ continue;
273
+ }
274
+ throw new CypherSyntaxError(`Unexpected character: '${input[pos]}'`, pos);
275
+ }
276
+ tokens.push({ type: TokenType.EOF, value: '', position: pos });
277
+ return tokens;
278
+ }
279
+ // ============================================================
280
+ // Parser
281
+ // ============================================================
282
+ /**
283
+ * Recursive descent parser that produces a CypherAST from a token stream.
284
+ */
285
+ class CypherParser {
286
+ tokens;
287
+ pos;
288
+ constructor(tokens) {
289
+ this.tokens = tokens;
290
+ this.pos = 0;
291
+ }
292
+ /** Peek at the current token without consuming it */
293
+ peek() {
294
+ return this.tokens[this.pos];
295
+ }
296
+ /** Consume and return the current token */
297
+ advance() {
298
+ const token = this.tokens[this.pos];
299
+ this.pos++;
300
+ return token;
301
+ }
302
+ /** Expect a specific token type; throw if not matched */
303
+ expect(type) {
304
+ const token = this.peek();
305
+ if (token.type !== type) {
306
+ throw new CypherSyntaxError(`Expected ${type} but found ${token.type} ('${token.value}')`, token.position);
307
+ }
308
+ return this.advance();
309
+ }
310
+ /** Check if current token is of a given type (case-insensitive for keywords) */
311
+ check(type) {
312
+ return this.peek().type === type;
313
+ }
314
+ /** If current token matches, consume it and return true */
315
+ match(type) {
316
+ if (this.check(type)) {
317
+ this.advance();
318
+ return true;
319
+ }
320
+ return false;
321
+ }
322
+ /** Main parse entry point */
323
+ parse() {
324
+ this.expect(TokenType.MATCH);
325
+ const matchPattern = this.parseMatchPattern();
326
+ let where = null;
327
+ if (this.check(TokenType.WHERE)) {
328
+ this.advance();
329
+ where = this.parseWhereExpr();
330
+ }
331
+ this.expect(TokenType.RETURN);
332
+ let distinct = false;
333
+ if (this.check(TokenType.DISTINCT)) {
334
+ this.advance();
335
+ distinct = true;
336
+ }
337
+ const returnItems = this.parseReturnItems();
338
+ const orderBy = [];
339
+ if (this.check(TokenType.ORDER)) {
340
+ this.advance();
341
+ this.expect(TokenType.BY);
342
+ orderBy.push(...this.parseOrderByItems());
343
+ }
344
+ let limit = null;
345
+ if (this.check(TokenType.LIMIT)) {
346
+ this.advance();
347
+ const numToken = this.expect(TokenType.NUMBER);
348
+ limit = parseInt(numToken.value, 10);
349
+ if (isNaN(limit) || limit < 0) {
350
+ throw new CypherSyntaxError(`Invalid LIMIT value: ${numToken.value}`, numToken.position);
351
+ }
352
+ }
353
+ if (!this.check(TokenType.EOF)) {
354
+ const token = this.peek();
355
+ throw new CypherSyntaxError(`Unexpected token after query end: ${token.type} ('${token.value}')`, token.position);
356
+ }
357
+ return { matchPattern, where, returnItems, distinct, orderBy, limit };
358
+ }
359
+ // ---- MATCH pattern parsing ----
360
+ parseMatchPattern() {
361
+ const startNode = this.parseNodePattern();
362
+ const chain = [];
363
+ while (this.isEdgeStart()) {
364
+ const edge = this.parseEdgePattern();
365
+ const node = this.parseNodePattern();
366
+ chain.push({ edge, node });
367
+ }
368
+ return { startNode, chain };
369
+ }
370
+ isEdgeStart() {
371
+ return this.check(TokenType.DASH) || this.check(TokenType.ARROW_LEFT);
372
+ }
373
+ parseNodePattern() {
374
+ this.expect(TokenType.LPAREN);
375
+ let variable = null;
376
+ let label = null;
377
+ if (this.check(TokenType.IDENTIFIER)) {
378
+ variable = this.advance().value;
379
+ }
380
+ if (this.check(TokenType.COLON)) {
381
+ this.advance();
382
+ const labelToken = this.expect(TokenType.IDENTIFIER);
383
+ label = labelToken.value;
384
+ }
385
+ this.expect(TokenType.RPAREN);
386
+ return { variable, label };
387
+ }
388
+ parseEdgePattern() {
389
+ let direction = 'both';
390
+ let variable = null;
391
+ let edgeType = null;
392
+ if (this.match(TokenType.ARROW_LEFT)) {
393
+ // <-[...]- or <-
394
+ direction = 'left';
395
+ if (this.check(TokenType.LBRACKET)) {
396
+ this.advance();
397
+ ({ variable, edgeType } = this.parseEdgeBody());
398
+ this.expect(TokenType.RBRACKET);
399
+ }
400
+ this.expect(TokenType.DASH);
401
+ }
402
+ else {
403
+ // - or -[...]-> or -[...]-
404
+ this.expect(TokenType.DASH);
405
+ if (this.check(TokenType.LBRACKET)) {
406
+ this.advance();
407
+ ({ variable, edgeType } = this.parseEdgeBody());
408
+ this.expect(TokenType.RBRACKET);
409
+ }
410
+ if (this.match(TokenType.ARROW_RIGHT)) {
411
+ direction = 'right';
412
+ }
413
+ else if (this.check(TokenType.DASH)) {
414
+ this.advance();
415
+ direction = 'both';
416
+ }
417
+ }
418
+ return { variable, edgeType, direction };
419
+ }
420
+ parseEdgeBody() {
421
+ let variable = null;
422
+ let edgeType = null;
423
+ // Optional variable
424
+ if (this.check(TokenType.IDENTIFIER) && !this.isColonAhead()) {
425
+ variable = this.advance().value;
426
+ }
427
+ // Optional :TYPE
428
+ if (this.check(TokenType.COLON)) {
429
+ this.advance();
430
+ const typeToken = this.expect(TokenType.IDENTIFIER);
431
+ edgeType = typeToken.value;
432
+ }
433
+ return { variable, edgeType };
434
+ }
435
+ /** Look ahead to see if a colon follows the current token (for edge body parsing) */
436
+ isColonAhead() {
437
+ return this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1].type === TokenType.COLON;
438
+ }
439
+ // ---- WHERE clause parsing ----
440
+ parseWhereExpr() {
441
+ return this.parseOrExpr();
442
+ }
443
+ parseOrExpr() {
444
+ let left = this.parseAndExpr();
445
+ while (this.check(TokenType.OR)) {
446
+ this.advance();
447
+ const right = this.parseAndExpr();
448
+ left = { kind: 'or', left, right };
449
+ }
450
+ return left;
451
+ }
452
+ parseAndExpr() {
453
+ let left = this.parsePrimaryExpr();
454
+ while (this.check(TokenType.AND)) {
455
+ this.advance();
456
+ const right = this.parsePrimaryExpr();
457
+ left = { kind: 'and', left, right };
458
+ }
459
+ return left;
460
+ }
461
+ parsePrimaryExpr() {
462
+ // NOT
463
+ if (this.check(TokenType.NOT)) {
464
+ this.advance();
465
+ // NOT EXISTS { (f)<-[:CALLS]-() }
466
+ if (this.check(TokenType.EXISTS)) {
467
+ return this.parseNotExistsExpr();
468
+ }
469
+ const operand = this.parsePrimaryExpr();
470
+ return { kind: 'not', operand };
471
+ }
472
+ // Parenthesized expression
473
+ if (this.check(TokenType.LPAREN) && this.isSubExprParen()) {
474
+ this.advance();
475
+ const expr = this.parseWhereExpr();
476
+ this.expect(TokenType.RPAREN);
477
+ return expr;
478
+ }
479
+ // Property comparison: f.name = 'value'
480
+ return this.parseComparison();
481
+ }
482
+ /**
483
+ * Distinguish between a parenthesized sub-expression and a node pattern.
484
+ * If the token after `(` is an identifier followed by `.`, it's a comparison.
485
+ * Otherwise it's likely a sub-expression if followed by NOT or another `(`.
486
+ */
487
+ isSubExprParen() {
488
+ if (this.pos + 1 >= this.tokens.length)
489
+ return false;
490
+ const next = this.tokens[this.pos + 1];
491
+ // If we see NOT or another (, it's a sub-expression
492
+ if (next.type === TokenType.NOT || next.type === TokenType.LPAREN)
493
+ return true;
494
+ // If we see identifier.identifier, it's a comparison in parens
495
+ if (next.type === TokenType.IDENTIFIER && this.pos + 2 < this.tokens.length &&
496
+ this.tokens[this.pos + 2].type === TokenType.DOT)
497
+ return true;
498
+ return false;
499
+ }
500
+ parseNotExistsExpr() {
501
+ this.expect(TokenType.EXISTS);
502
+ this.expect(TokenType.LBRACE);
503
+ // Parse the pattern: (variable)<-[:TYPE]-() or (variable)-[:TYPE]->()
504
+ this.expect(TokenType.LPAREN);
505
+ const varToken = this.expect(TokenType.IDENTIFIER);
506
+ const variable = varToken.value;
507
+ this.expect(TokenType.RPAREN);
508
+ let direction = 'incoming';
509
+ let edgeType = null;
510
+ if (this.check(TokenType.ARROW_LEFT)) {
511
+ // <-[:TYPE]-()
512
+ this.advance();
513
+ direction = 'incoming';
514
+ if (this.check(TokenType.LBRACKET)) {
515
+ this.advance();
516
+ if (this.check(TokenType.COLON)) {
517
+ this.advance();
518
+ edgeType = this.expect(TokenType.IDENTIFIER).value;
519
+ }
520
+ this.expect(TokenType.RBRACKET);
521
+ }
522
+ this.expect(TokenType.DASH);
523
+ }
524
+ else if (this.check(TokenType.DASH)) {
525
+ // -[:TYPE]->()
526
+ this.advance();
527
+ direction = 'outgoing';
528
+ if (this.check(TokenType.LBRACKET)) {
529
+ this.advance();
530
+ if (this.check(TokenType.COLON)) {
531
+ this.advance();
532
+ edgeType = this.expect(TokenType.IDENTIFIER).value;
533
+ }
534
+ this.expect(TokenType.RBRACKET);
535
+ }
536
+ this.expect(TokenType.ARROW_RIGHT);
537
+ }
538
+ this.expect(TokenType.LPAREN);
539
+ this.expect(TokenType.RPAREN);
540
+ this.expect(TokenType.RBRACE);
541
+ return { kind: 'not_exists', variable, edgeType, direction };
542
+ }
543
+ parseComparison() {
544
+ const left = this.parsePropertyRef();
545
+ const opToken = this.peek();
546
+ let op;
547
+ switch (opToken.type) {
548
+ case TokenType.EQ:
549
+ this.advance();
550
+ op = '=';
551
+ break;
552
+ case TokenType.NEQ:
553
+ this.advance();
554
+ op = '<>';
555
+ break;
556
+ case TokenType.GT:
557
+ this.advance();
558
+ op = '>';
559
+ break;
560
+ case TokenType.LT:
561
+ this.advance();
562
+ op = '<';
563
+ break;
564
+ case TokenType.GTE:
565
+ this.advance();
566
+ op = '>=';
567
+ break;
568
+ case TokenType.LTE:
569
+ this.advance();
570
+ op = '<=';
571
+ break;
572
+ case TokenType.CONTAINS:
573
+ this.advance();
574
+ op = 'CONTAINS';
575
+ break;
576
+ case TokenType.STARTS:
577
+ this.advance();
578
+ this.expect(TokenType.WITH);
579
+ op = 'STARTS WITH';
580
+ break;
581
+ case TokenType.ENDS:
582
+ this.advance();
583
+ this.expect(TokenType.WITH);
584
+ op = 'ENDS WITH';
585
+ break;
586
+ case TokenType.IN:
587
+ this.advance();
588
+ op = 'IN';
589
+ break;
590
+ default:
591
+ throw new CypherSyntaxError(`Expected comparison operator but found ${opToken.type} ('${opToken.value}')`, opToken.position);
592
+ }
593
+ if (op === 'IN') {
594
+ const right = this.parseLiteralList();
595
+ return { kind: 'comparison', left, op, right };
596
+ }
597
+ const right = this.parseLiteral();
598
+ return { kind: 'comparison', left, op, right };
599
+ }
600
+ parsePropertyRef() {
601
+ const variableToken = this.expect(TokenType.IDENTIFIER);
602
+ this.expect(TokenType.DOT);
603
+ const propertyToken = this.expect(TokenType.IDENTIFIER);
604
+ return { variable: variableToken.value, property: propertyToken.value };
605
+ }
606
+ parseLiteral() {
607
+ const token = this.peek();
608
+ switch (token.type) {
609
+ case TokenType.STRING:
610
+ this.advance();
611
+ return token.value;
612
+ case TokenType.NUMBER:
613
+ this.advance();
614
+ return token.value.includes('.') ? parseFloat(token.value) : parseInt(token.value, 10);
615
+ case TokenType.TRUE:
616
+ this.advance();
617
+ return true;
618
+ case TokenType.FALSE:
619
+ this.advance();
620
+ return false;
621
+ case TokenType.NULL:
622
+ this.advance();
623
+ return null;
624
+ default:
625
+ throw new CypherSyntaxError(`Expected literal value but found ${token.type} ('${token.value}')`, token.position);
626
+ }
627
+ }
628
+ parseLiteralList() {
629
+ this.expect(TokenType.LBRACKET);
630
+ const values = [];
631
+ if (!this.check(TokenType.RBRACKET)) {
632
+ values.push(this.parseLiteral());
633
+ while (this.match(TokenType.COMMA)) {
634
+ values.push(this.parseLiteral());
635
+ }
636
+ }
637
+ this.expect(TokenType.RBRACKET);
638
+ return values;
639
+ }
640
+ // ---- RETURN clause parsing ----
641
+ parseReturnItems() {
642
+ const items = [];
643
+ items.push(this.parseReturnItem());
644
+ while (this.match(TokenType.COMMA)) {
645
+ items.push(this.parseReturnItem());
646
+ }
647
+ return items;
648
+ }
649
+ parseReturnItem() {
650
+ // Check for variable.* (return all properties)
651
+ if (this.check(TokenType.IDENTIFIER) && this.pos + 1 < this.tokens.length &&
652
+ this.tokens[this.pos + 1].type === TokenType.DOT &&
653
+ this.pos + 2 < this.tokens.length &&
654
+ this.tokens[this.pos + 2].type === TokenType.STAR) {
655
+ const variable = this.advance().value;
656
+ this.advance(); // .
657
+ this.advance(); // *
658
+ let alias = null;
659
+ if (this.check(TokenType.AS)) {
660
+ this.advance();
661
+ alias = this.expect(TokenType.IDENTIFIER).value;
662
+ }
663
+ return { expr: { kind: 'star', variable }, alias };
664
+ }
665
+ // variable.property
666
+ const ref = this.parsePropertyRef();
667
+ let alias = null;
668
+ if (this.check(TokenType.AS)) {
669
+ this.advance();
670
+ alias = this.expect(TokenType.IDENTIFIER).value;
671
+ }
672
+ return { expr: ref, alias };
673
+ }
674
+ // ---- ORDER BY clause parsing ----
675
+ parseOrderByItems() {
676
+ const items = [];
677
+ items.push(this.parseOrderByItem());
678
+ while (this.match(TokenType.COMMA)) {
679
+ items.push(this.parseOrderByItem());
680
+ }
681
+ return items;
682
+ }
683
+ parseOrderByItem() {
684
+ const expr = this.parsePropertyRef();
685
+ let direction = 'ASC';
686
+ if (this.check(TokenType.ASC)) {
687
+ this.advance();
688
+ direction = 'ASC';
689
+ }
690
+ else if (this.check(TokenType.DESC)) {
691
+ this.advance();
692
+ direction = 'DESC';
693
+ }
694
+ return { expr, direction };
695
+ }
696
+ }
697
+ // ============================================================
698
+ // SQL Code Generator / Executor
699
+ // ============================================================
700
+ /**
701
+ * Mini Cypher query engine that compiles Cypher queries into SQL
702
+ * and executes them against the knowledge graph's SQLite database.
703
+ *
704
+ * This is a read-only engine — no CREATE, MERGE, SET, or DELETE.
705
+ *
706
+ * @example
707
+ * ```ts
708
+ * const engine = new CypherEngine(db);
709
+ * const result = engine.execute("MATCH (f:Function) RETURN f.name LIMIT 10");
710
+ * console.log(result.rows);
711
+ * ```
712
+ */
713
+ export class CypherEngine {
714
+ db;
715
+ /**
716
+ * Create a new CypherEngine.
717
+ *
718
+ * @param graph - A KnowledgeGraph instance (the DB is extracted via graph.getDb())
719
+ */
720
+ constructor(graph) {
721
+ this.db = graph.getDb();
722
+ }
723
+ /**
724
+ * Execute a Cypher query and return the results.
725
+ *
726
+ * @param query - A Cypher query string (read-only subset)
727
+ * @param projectFilter - Optional project path to scope results by filePath
728
+ * @returns The query result with columns, rows, and metadata
729
+ * @throws {CypherSyntaxError} If the query has a syntax error
730
+ * @throws {CypherExecutionError} If the query fails during execution
731
+ */
732
+ execute(query, projectFilter) {
733
+ const startTime = performance.now();
734
+ try {
735
+ // 1. Tokenize
736
+ const tokens = tokenize(query.trim());
737
+ // 2. Parse into AST
738
+ const parser = new CypherParser(tokens);
739
+ const ast = parser.parse();
740
+ // 2b. If a project filter is given, inject a filePath CONTAINS condition
741
+ if (projectFilter) {
742
+ this.injectProjectFilter(ast, projectFilter);
743
+ }
744
+ // 3. Compile AST to SQL
745
+ const { sql, params, columns } = this.compileToSql(ast);
746
+ // 4. Execute SQL
747
+ const rows = this.db.prepare(sql).all(...params);
748
+ const executionTimeMs = Math.round((performance.now() - startTime) * 100) / 100;
749
+ return {
750
+ columns,
751
+ rows,
752
+ count: rows.length,
753
+ generatedSql: sql,
754
+ executionTimeMs,
755
+ };
756
+ }
757
+ catch (error) {
758
+ if (error instanceof CypherSyntaxError || error instanceof CypherExecutionError) {
759
+ throw error;
760
+ }
761
+ const message = error instanceof Error ? error.message : String(error);
762
+ throw new CypherExecutionError(`Query execution failed: ${message}`, query);
763
+ }
764
+ }
765
+ /**
766
+ * Inject a filePath filter into the AST's WHERE clause to scope results to a project path.
767
+ */
768
+ injectProjectFilter(ast, projectPath) {
769
+ // Find the first node variable in the MATCH pattern
770
+ const variable = ast.matchPattern.startNode.variable;
771
+ if (!variable)
772
+ return;
773
+ const filterExpr = {
774
+ kind: 'comparison',
775
+ left: { variable, property: 'filePath' },
776
+ op: 'STARTS WITH',
777
+ right: projectPath,
778
+ };
779
+ if (ast.where) {
780
+ ast.where = { kind: 'and', left: ast.where, right: filterExpr };
781
+ }
782
+ else {
783
+ ast.where = filterExpr;
784
+ }
785
+ }
786
+ /**
787
+ * Compile a CypherAST into a SQL query string, bound parameters, and column list.
788
+ */
789
+ compileToSql(ast) {
790
+ const params = [];
791
+ const { matchPattern, where, returnItems, distinct, orderBy, limit } = ast;
792
+ // Build a map from variable names to their SQL table aliases and node labels
793
+ const variableMap = new Map();
794
+ let tableIndex = 0;
795
+ let edgeIndex = 0;
796
+ const fromClauses = [];
797
+ const joinConditions = [];
798
+ // Process start node
799
+ const startAlias = `n${tableIndex++}`;
800
+ if (matchPattern.startNode.variable) {
801
+ variableMap.set(matchPattern.startNode.variable, {
802
+ alias: startAlias,
803
+ label: matchPattern.startNode.label,
804
+ });
805
+ }
806
+ fromClauses.push(`nodes AS ${startAlias}`);
807
+ if (matchPattern.startNode.label) {
808
+ const nodeType = this.resolveNodeLabel(matchPattern.startNode.label);
809
+ joinConditions.push(`${startAlias}.type = ?`);
810
+ params.push(nodeType);
811
+ }
812
+ // Process chain (edge→node pairs)
813
+ for (const { edge, node } of matchPattern.chain) {
814
+ const edgeAlias = `e${edgeIndex++}`;
815
+ const nodeAlias = `n${tableIndex++}`;
816
+ fromClauses.push(`edges AS ${edgeAlias}`);
817
+ fromClauses.push(`nodes AS ${nodeAlias}`);
818
+ if (node.variable) {
819
+ variableMap.set(node.variable, { alias: nodeAlias, label: node.label });
820
+ }
821
+ if (edge.variable) {
822
+ variableMap.set(edge.variable, { alias: edgeAlias, label: null });
823
+ }
824
+ // Wire up the edge based on direction
825
+ const prevAlias = `n${tableIndex - 2}`;
826
+ switch (edge.direction) {
827
+ case 'right':
828
+ // (prev)-[edge]->(node): prev.id = edge.sourceId, edge.targetId = node.id
829
+ joinConditions.push(`${edgeAlias}.sourceId = ${prevAlias}.id`);
830
+ joinConditions.push(`${edgeAlias}.targetId = ${nodeAlias}.id`);
831
+ break;
832
+ case 'left':
833
+ // (prev)<-[edge]-(node): prev.id = edge.targetId, edge.sourceId = node.id
834
+ joinConditions.push(`${edgeAlias}.targetId = ${prevAlias}.id`);
835
+ joinConditions.push(`${edgeAlias}.sourceId = ${nodeAlias}.id`);
836
+ break;
837
+ case 'both':
838
+ // Undirected: either direction
839
+ joinConditions.push(`((${edgeAlias}.sourceId = ${prevAlias}.id AND ${edgeAlias}.targetId = ${nodeAlias}.id) OR ` +
840
+ `(${edgeAlias}.targetId = ${prevAlias}.id AND ${edgeAlias}.sourceId = ${nodeAlias}.id))`);
841
+ break;
842
+ }
843
+ // Filter by edge type
844
+ if (edge.edgeType) {
845
+ const resolvedType = this.resolveEdgeType(edge.edgeType);
846
+ joinConditions.push(`${edgeAlias}.type = ?`);
847
+ params.push(resolvedType);
848
+ }
849
+ // Filter by node label
850
+ if (node.label) {
851
+ const nodeType = this.resolveNodeLabel(node.label);
852
+ joinConditions.push(`${nodeAlias}.type = ?`);
853
+ params.push(nodeType);
854
+ }
855
+ }
856
+ // Compile WHERE clause
857
+ if (where) {
858
+ const whereSql = this.compileWhereExpr(where, variableMap, params);
859
+ joinConditions.push(whereSql);
860
+ }
861
+ // Compile SELECT (RETURN) clause
862
+ const selectItems = [];
863
+ const columns = [];
864
+ for (const item of returnItems) {
865
+ if ('kind' in item.expr && item.expr.kind === 'star') {
866
+ // variable.* — return all columns with prefixed names
867
+ const info = variableMap.get(item.expr.variable);
868
+ if (!info) {
869
+ throw new CypherExecutionError(`Unknown variable '${item.expr.variable}' in RETURN clause`, '');
870
+ }
871
+ for (const [propName, colName] of Object.entries(PROPERTY_TO_COLUMN)) {
872
+ const alias = `${item.expr.variable}_${propName}`;
873
+ selectItems.push(`${info.alias}.${colName} AS ${alias}`);
874
+ columns.push(alias);
875
+ }
876
+ }
877
+ else {
878
+ const ref = item.expr;
879
+ const colExpr = this.resolvePropertyRef(ref, variableMap);
880
+ const alias = item.alias ?? `${ref.variable}_${ref.property}`;
881
+ selectItems.push(`${colExpr} AS ${alias}`);
882
+ columns.push(alias);
883
+ }
884
+ }
885
+ // Build the final query
886
+ let sql = `SELECT ${distinct ? 'DISTINCT ' : ''}${selectItems.join(', ')}\nFROM ${fromClauses.join(', ')}`;
887
+ if (joinConditions.length > 0) {
888
+ sql += `\nWHERE ${joinConditions.join('\n AND ')}`;
889
+ }
890
+ // ORDER BY
891
+ if (orderBy.length > 0) {
892
+ const orderClauses = orderBy.map(item => {
893
+ const colExpr = this.resolvePropertyRef(item.expr, variableMap);
894
+ return `${colExpr} ${item.direction}`;
895
+ });
896
+ sql += `\nORDER BY ${orderClauses.join(', ')}`;
897
+ }
898
+ // LIMIT
899
+ if (limit !== null) {
900
+ sql += `\nLIMIT ?`;
901
+ params.push(limit);
902
+ }
903
+ return { sql, params, columns };
904
+ }
905
+ /**
906
+ * Compile a WHERE expression into a SQL fragment, appending bound params.
907
+ */
908
+ compileWhereExpr(expr, variableMap, params) {
909
+ switch (expr.kind) {
910
+ case 'comparison':
911
+ return this.compileComparison(expr, variableMap, params);
912
+ case 'not_exists':
913
+ return this.compileNotExists(expr, variableMap, params);
914
+ case 'and':
915
+ return `(${this.compileWhereExpr(expr.left, variableMap, params)} AND ${this.compileWhereExpr(expr.right, variableMap, params)})`;
916
+ case 'or':
917
+ return `(${this.compileWhereExpr(expr.left, variableMap, params)} OR ${this.compileWhereExpr(expr.right, variableMap, params)})`;
918
+ case 'not':
919
+ return `NOT (${this.compileWhereExpr(expr.operand, variableMap, params)})`;
920
+ }
921
+ }
922
+ /**
923
+ * Compile a comparison expression into SQL.
924
+ */
925
+ compileComparison(expr, variableMap, params) {
926
+ const leftCol = this.resolvePropertyRef(expr.left, variableMap);
927
+ switch (expr.op) {
928
+ case '=':
929
+ case '<>':
930
+ case '>':
931
+ case '<':
932
+ case '>=':
933
+ case '<=':
934
+ params.push(this.toLiteralSqlValue(expr.right));
935
+ return `${leftCol} ${expr.op} ?`;
936
+ case 'CONTAINS':
937
+ params.push(`%${expr.right}%`);
938
+ return `${leftCol} LIKE ?`;
939
+ case 'STARTS WITH':
940
+ params.push(`${expr.right}%`);
941
+ return `${leftCol} LIKE ?`;
942
+ case 'ENDS WITH':
943
+ params.push(`%${expr.right}`);
944
+ return `${leftCol} LIKE ?`;
945
+ case 'IN': {
946
+ const list = expr.right;
947
+ const placeholders = list.map(() => '?').join(', ');
948
+ for (const v of list) {
949
+ params.push(this.toLiteralSqlValue(v));
950
+ }
951
+ return `${leftCol} IN (${placeholders})`;
952
+ }
953
+ }
954
+ }
955
+ /**
956
+ * Compile a NOT EXISTS pattern into a SQL NOT EXISTS subquery.
957
+ */
958
+ compileNotExists(expr, variableMap, _params) {
959
+ const info = variableMap.get(expr.variable);
960
+ if (!info) {
961
+ throw new CypherExecutionError(`Unknown variable '${expr.variable}' in NOT EXISTS pattern`, '');
962
+ }
963
+ let subquery;
964
+ const edgeTypeFilter = expr.edgeType
965
+ ? (() => {
966
+ const resolved = this.resolveEdgeType(expr.edgeType);
967
+ _params.push(resolved);
968
+ return ` AND _e.type = ?`;
969
+ })()
970
+ : '';
971
+ if (expr.direction === 'incoming') {
972
+ // No incoming edges to this node: NOT EXISTS (SELECT 1 FROM edges WHERE targetId = node.id)
973
+ subquery = `NOT EXISTS (SELECT 1 FROM edges _e WHERE _e.targetId = ${info.alias}.id${edgeTypeFilter})`;
974
+ }
975
+ else {
976
+ // No outgoing edges from this node
977
+ subquery = `NOT EXISTS (SELECT 1 FROM edges _e WHERE _e.sourceId = ${info.alias}.id${edgeTypeFilter})`;
978
+ }
979
+ return subquery;
980
+ }
981
+ /**
982
+ * Resolve a property reference to a SQL column expression.
983
+ */
984
+ resolvePropertyRef(ref, variableMap) {
985
+ const info = variableMap.get(ref.variable);
986
+ if (!info) {
987
+ throw new CypherExecutionError(`Unknown variable '${ref.variable}'. Available variables: ${[...variableMap.keys()].join(', ')}`, '');
988
+ }
989
+ const column = PROPERTY_TO_COLUMN[ref.property];
990
+ if (!column) {
991
+ throw new CypherExecutionError(`Unknown property '${ref.property}'. Available properties: ${Object.keys(PROPERTY_TO_COLUMN).join(', ')}`, '');
992
+ }
993
+ return `${info.alias}.${column}`;
994
+ }
995
+ /**
996
+ * Resolve a Cypher node label to the internal node type string.
997
+ */
998
+ resolveNodeLabel(label) {
999
+ const resolved = LABEL_TO_TYPE[label.toLowerCase()];
1000
+ if (!resolved) {
1001
+ throw new CypherExecutionError(`Unknown node label ':${label}'. Available labels: ${Object.keys(LABEL_TO_TYPE).join(', ')}`, '');
1002
+ }
1003
+ return resolved;
1004
+ }
1005
+ /**
1006
+ * Resolve a Cypher edge type label to the internal edge type string.
1007
+ */
1008
+ resolveEdgeType(edgeType) {
1009
+ const resolved = EDGE_LABEL_TO_TYPE[edgeType.toLowerCase()];
1010
+ if (!resolved) {
1011
+ throw new CypherExecutionError(`Unknown edge type '[:${edgeType}]'. Available types: ${Object.keys(EDGE_LABEL_TO_TYPE).join(', ')}`, '');
1012
+ }
1013
+ return resolved;
1014
+ }
1015
+ /**
1016
+ * Convert a Cypher literal to a SQL-safe value.
1017
+ */
1018
+ toLiteralSqlValue(value) {
1019
+ if (typeof value === 'boolean')
1020
+ return value ? 1 : 0;
1021
+ return value;
1022
+ }
1023
+ }
1024
+ // ============================================================
1025
+ // Error Classes
1026
+ // ============================================================
1027
+ /**
1028
+ * Syntax error in a Cypher query string.
1029
+ */
1030
+ export class CypherSyntaxError extends Error {
1031
+ /** Character position where the error occurred */
1032
+ position;
1033
+ constructor(message, position) {
1034
+ super(`Cypher syntax error at position ${position}: ${message}`);
1035
+ this.name = 'CypherSyntaxError';
1036
+ this.position = position;
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Runtime error during Cypher query execution.
1041
+ */
1042
+ export class CypherExecutionError extends Error {
1043
+ /** The original Cypher query that caused the error */
1044
+ query;
1045
+ constructor(message, query) {
1046
+ super(`Cypher execution error: ${message}`);
1047
+ this.name = 'CypherExecutionError';
1048
+ this.query = query;
1049
+ }
1050
+ }
1051
+ //# sourceMappingURL=cypher.js.map