@remnic/coding-graph 9.3.759

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +130 -0
  2. package/dist/chunk-5I2DBHOQ.js +1042 -0
  3. package/dist/chunk-5I2DBHOQ.js.map +1 -0
  4. package/dist/chunk-CPYJACC5.js +1838 -0
  5. package/dist/chunk-CPYJACC5.js.map +1 -0
  6. package/dist/chunk-ZVCMIM4T.js +216 -0
  7. package/dist/chunk-ZVCMIM4T.js.map +1 -0
  8. package/dist/cypher/query-parser.d.ts +253 -0
  9. package/dist/cypher/query-parser.js +17 -0
  10. package/dist/cypher/query-parser.js.map +1 -0
  11. package/dist/graph-schema.d.ts +84 -0
  12. package/dist/graph-schema.js +17 -0
  13. package/dist/graph-schema.js.map +1 -0
  14. package/dist/graph-store.d.ts +938 -0
  15. package/dist/graph-store.js +16 -0
  16. package/dist/graph-store.js.map +1 -0
  17. package/dist/index.d.ts +1953 -0
  18. package/dist/index.js +3509 -0
  19. package/dist/index.js.map +1 -0
  20. package/grammars/tree-sitter-bash.wasm +0 -0
  21. package/grammars/tree-sitter-c.wasm +0 -0
  22. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  23. package/grammars/tree-sitter-cpp.wasm +0 -0
  24. package/grammars/tree-sitter-go.wasm +0 -0
  25. package/grammars/tree-sitter-java.wasm +0 -0
  26. package/grammars/tree-sitter-javascript.wasm +0 -0
  27. package/grammars/tree-sitter-kotlin.wasm +0 -0
  28. package/grammars/tree-sitter-php.wasm +0 -0
  29. package/grammars/tree-sitter-python.wasm +0 -0
  30. package/grammars/tree-sitter-ruby.wasm +0 -0
  31. package/grammars/tree-sitter-rust.wasm +0 -0
  32. package/grammars/tree-sitter-swift.wasm +0 -0
  33. package/grammars/tree-sitter-tsx.wasm +0 -0
  34. package/grammars/tree-sitter-typescript.wasm +0 -0
  35. package/package.json +79 -0
  36. package/src/co-change.test.ts +175 -0
  37. package/src/co-change.ts +167 -0
  38. package/src/cypher/query-parser.test.ts +1107 -0
  39. package/src/cypher/query-parser.ts +1692 -0
  40. package/src/detect-changes.test.ts +533 -0
  41. package/src/detect-changes.ts +367 -0
  42. package/src/engine/emit.ts +556 -0
  43. package/src/engine/engine.test.ts +1417 -0
  44. package/src/engine/engine.ts +182 -0
  45. package/src/engine/extractors.ts +486 -0
  46. package/src/engine/fixtures.ts +364 -0
  47. package/src/engine/language-sniff.ts +56 -0
  48. package/src/engine/parser-backend.ts +206 -0
  49. package/src/engine/utf16-offsets.ts +68 -0
  50. package/src/git-invoker.test.ts +116 -0
  51. package/src/git-invoker.ts +426 -0
  52. package/src/graph-schema.test.ts +541 -0
  53. package/src/graph-schema.ts +383 -0
  54. package/src/graph-store-pr2.test.ts +1879 -0
  55. package/src/graph-store.test.ts +1420 -0
  56. package/src/graph-store.ts +3489 -0
  57. package/src/index-status.test.ts +303 -0
  58. package/src/index-status.ts +135 -0
  59. package/src/index.ts +384 -0
  60. package/src/lsp/byte-position.ts +173 -0
  61. package/src/lsp/characterization.test.ts +174 -0
  62. package/src/lsp/client.test.ts +275 -0
  63. package/src/lsp/client.ts +484 -0
  64. package/src/lsp/config.ts +219 -0
  65. package/src/lsp/degradation.ts +86 -0
  66. package/src/lsp/fixtures/fake-server.mjs +198 -0
  67. package/src/lsp/framing.test.ts +180 -0
  68. package/src/lsp/framing.ts +177 -0
  69. package/src/lsp/resolution.test.ts +497 -0
  70. package/src/lsp/resolution.ts +483 -0
  71. package/src/lsp/status.ts +140 -0
  72. package/src/lsp/types.ts +167 -0
  73. package/src/reindex.test.ts +1038 -0
  74. package/src/reindex.ts +908 -0
  75. package/src/row-types.ts +45 -0
  76. package/src/semantic/canonical-text.test.ts +150 -0
  77. package/src/semantic/canonical-text.ts +219 -0
  78. package/src/semantic/config.ts +235 -0
  79. package/src/semantic/index.ts +78 -0
  80. package/src/semantic/minhash.test.ts +197 -0
  81. package/src/semantic/minhash.ts +261 -0
  82. package/src/semantic/semantic-query.ts +173 -0
  83. package/src/semantic/semantic.test.ts +1315 -0
  84. package/src/semantic/similarity.ts +268 -0
  85. package/src/semantic/types.ts +145 -0
  86. package/src/semantic/vectors.ts +235 -0
package/dist/index.js ADDED
@@ -0,0 +1,3509 @@
1
+ import {
2
+ VALID_CYPHER_LABELS,
3
+ executeAst,
4
+ executeCypher,
5
+ parseCypher
6
+ } from "./chunk-5I2DBHOQ.js";
7
+ import {
8
+ DEAD_CODE_EXCLUSION,
9
+ DEFAULT_TRAVERSE_PATHS_MAX,
10
+ GraphStore,
11
+ MAX_TRAVERSE_PATHS_HOPS,
12
+ nodeIdFor
13
+ } from "./chunk-CPYJACC5.js";
14
+ import {
15
+ CODING_GRAPH_SCHEMA_VERSION,
16
+ EDGE_PROVENANCE_VALUES,
17
+ applyCodingGraphSchema,
18
+ isEdgeProvenance,
19
+ readSchemaVersion
20
+ } from "./chunk-ZVCMIM4T.js";
21
+
22
+ // src/index.ts
23
+ import {
24
+ CODING_GRAPH_ENGINE_VERSION as CODING_GRAPH_ENGINE_VERSION2,
25
+ TIER_1_LANGUAGES as TIER_1_LANGUAGES3
26
+ } from "@remnic/core";
27
+
28
+ // src/engine/engine.ts
29
+ import {
30
+ CODING_GRAPH_ENGINE_VERSION,
31
+ TIER_1_LANGUAGES as TIER_1_LANGUAGES2
32
+ } from "@remnic/core";
33
+
34
+ // src/engine/parser-backend.ts
35
+ import { Parser, Language } from "web-tree-sitter";
36
+ import { fileURLToPath } from "url";
37
+ import path from "path";
38
+ import { statSync } from "fs";
39
+ function resolveGrammarDir() {
40
+ const here = path.dirname(fileURLToPath(import.meta.url));
41
+ const candidates = [
42
+ path.join(here, "..", "grammars"),
43
+ // dist/ → ../grammars
44
+ path.join(here, "..", "..", "grammars"),
45
+ // src/engine/ → ../../grammars
46
+ path.join(here, "grammars")
47
+ // flat layout fallback
48
+ ];
49
+ for (const candidate of candidates) {
50
+ try {
51
+ if (statSync(candidate).isDirectory()) return candidate;
52
+ } catch {
53
+ }
54
+ }
55
+ throw new Error(
56
+ `coding-graph: could not locate the grammars/ directory from ${here}. Expected one of: ${candidates.join(", ")}`
57
+ );
58
+ }
59
+ var WasmTreeSitterBackend = class {
60
+ initialized = false;
61
+ initializing = null;
62
+ parser = null;
63
+ languages = /* @__PURE__ */ new Map();
64
+ loadingLanguages = /* @__PURE__ */ new Map();
65
+ grammarDir = "";
66
+ grammarDirResolved = false;
67
+ grammarDirHint;
68
+ constructor(grammarDir) {
69
+ this.grammarDirHint = grammarDir ?? null;
70
+ }
71
+ getGrammarDir() {
72
+ if (this.grammarDirResolved) return this.grammarDir;
73
+ this.grammarDir = this.grammarDirHint ?? resolveGrammarDir();
74
+ this.grammarDirResolved = true;
75
+ return this.grammarDir;
76
+ }
77
+ async init() {
78
+ if (this.initialized) return;
79
+ if (this.initializing) return this.initializing;
80
+ this.initializing = (async () => {
81
+ try {
82
+ await Parser.init();
83
+ this.parser = new Parser();
84
+ this.initialized = true;
85
+ } finally {
86
+ this.initializing = null;
87
+ }
88
+ })();
89
+ return this.initializing;
90
+ }
91
+ async ensureLanguage(lang) {
92
+ await this.init();
93
+ if (this.languages.has(lang)) return;
94
+ if (this.loadingLanguages.has(lang)) return this.loadingLanguages.get(lang);
95
+ const p = (async () => {
96
+ try {
97
+ const wasmPath = path.join(this.getGrammarDir(), grammarFileName(lang));
98
+ const language = await Language.load(wasmPath);
99
+ this.languages.set(lang, language);
100
+ } finally {
101
+ this.loadingLanguages.delete(lang);
102
+ }
103
+ })();
104
+ this.loadingLanguages.set(lang, p);
105
+ return p;
106
+ }
107
+ parse(lang, content) {
108
+ if (!this.parser) return null;
109
+ const language = this.languages.get(lang);
110
+ if (!language) return null;
111
+ this.parser.setLanguage(language);
112
+ return this.parser.parse(content);
113
+ }
114
+ /** Return the loaded Language object for `lang`, or null if not loaded. */
115
+ getLanguage(lang) {
116
+ return this.languages.get(lang) ?? null;
117
+ }
118
+ async dispose() {
119
+ if (this.parser) {
120
+ try {
121
+ this.parser.delete();
122
+ } catch {
123
+ }
124
+ this.parser = null;
125
+ }
126
+ this.languages.clear();
127
+ this.loadingLanguages.clear();
128
+ this.initialized = false;
129
+ this.initializing = null;
130
+ this.grammarDirResolved = false;
131
+ this.grammarDir = "";
132
+ }
133
+ };
134
+ function grammarFileName(lang) {
135
+ const map = {
136
+ typescript: "tree-sitter-typescript.wasm",
137
+ tsx: "tree-sitter-tsx.wasm",
138
+ javascript: "tree-sitter-javascript.wasm",
139
+ python: "tree-sitter-python.wasm",
140
+ go: "tree-sitter-go.wasm",
141
+ rust: "tree-sitter-rust.wasm",
142
+ java: "tree-sitter-java.wasm",
143
+ c: "tree-sitter-c.wasm",
144
+ cpp: "tree-sitter-cpp.wasm",
145
+ csharp: "tree-sitter-c_sharp.wasm",
146
+ ruby: "tree-sitter-ruby.wasm",
147
+ php: "tree-sitter-php.wasm",
148
+ kotlin: "tree-sitter-kotlin.wasm",
149
+ swift: "tree-sitter-swift.wasm",
150
+ bash: "tree-sitter-bash.wasm"
151
+ };
152
+ return map[lang];
153
+ }
154
+
155
+ // src/engine/emit.ts
156
+ import { createHash } from "crypto";
157
+ import { Query } from "web-tree-sitter";
158
+
159
+ // src/engine/extractors.ts
160
+ var JS_FAMILY_DEFINITIONS = `
161
+ (function_declaration name: (identifier) @name) @def.function
162
+ (method_definition name: (property_identifier) @name) @def.method
163
+ (class_declaration name: (type_identifier) @name) @def.class
164
+ (interface_declaration name: (type_identifier) @name) @def.interface
165
+ (enum_declaration name: (identifier) @name) @def.enum
166
+ (type_alias_declaration name: (type_identifier) @name) @def.type
167
+
168
+ ; const handler = () => {} / const handler = function () {}
169
+ ; Indexes arrow-function and function-expression declarations so route
170
+ ; handlers and React components defined this way appear as symbols.
171
+ (variable_declarator
172
+ name: (identifier) @name
173
+ value: [(arrow_function) (function_expression)]) @def.function
174
+ `.trim();
175
+ var JS_IMPORTS = `
176
+ (import_statement
177
+ source: (string (string_fragment) @import.module)) @__import.stmt
178
+ (import_statement
179
+ (import_clause (identifier) @import.name)
180
+ source: (string (string_fragment) @import.module)) @__import.stmt
181
+ (import_statement
182
+ (import_clause (namespace_import (identifier) @import.name))
183
+ source: (string (string_fragment) @import.module)) @__import.stmt
184
+ (import_statement
185
+ (import_clause (named_imports (import_specifier name: (identifier) @import.name)))
186
+ source: (string (string_fragment) @import.module)) @__import.stmt
187
+
188
+ ; CommonJS require("...") \u2014 capture the module specifier so dependency
189
+ ; edges exist for Node/CommonJS codebases, not just ES-module imports.
190
+ (call_expression
191
+ function: (identifier) @__import.require
192
+ arguments: (arguments (string (string_fragment) @import.module))
193
+ (#eq? @__import.require "require")) @__import.stmt
194
+ `.trim();
195
+ var TS_EXPORTS = `
196
+ (export_statement declaration: (function_declaration name: (identifier) @export.name))
197
+ (export_statement declaration: (class_declaration name: (type_identifier) @export.name))
198
+ (export_statement (lexical_declaration (variable_declarator name: (identifier) @export.name)))
199
+ (export_statement declaration: (enum_declaration name: (identifier) @export.name))
200
+ (export_statement declaration: (interface_declaration name: (type_identifier) @export.name))
201
+ (export_statement (export_clause (export_specifier name: (identifier) @export.name)))
202
+
203
+ ; export default App; (bare identifier re-export)
204
+ (export_statement (identifier) @export.name)
205
+ `.trim();
206
+ var JS_EXPORTS = `
207
+ (export_statement declaration: (function_declaration name: (identifier) @export.name))
208
+ (export_statement declaration: (class_declaration name: (identifier) @export.name))
209
+ (export_statement (lexical_declaration (variable_declarator name: (identifier) @export.name)))
210
+ (export_statement (export_clause (export_specifier name: (identifier) @export.name)))
211
+
212
+ ; export default App; (bare identifier re-export)
213
+ (export_statement (identifier) @export.name)
214
+
215
+ ; CommonJS: module.exports = { App, createRouter }
216
+ (assignment_expression
217
+ left: (member_expression object: (identifier) @__cjs.mod property: (property_identifier) @__cjs.exp)
218
+ right: (object (shorthand_property_identifier) @export.name)
219
+ (#eq? @__cjs.mod "module") (#eq? @__cjs.exp "exports"))
220
+ ; CommonJS: module.exports = { publicName: createRouter }
221
+ ; Capture the VALUE identifier (the actual symbol), not the key (public alias).
222
+ (assignment_expression
223
+ left: (member_expression object: (identifier) @__cjs.mod2 property: (property_identifier) @__cjs.exp2)
224
+ right: (object (pair key: (property_identifier) @__cjs.key2 value: (identifier) @export.name))
225
+ (#eq? @__cjs.mod2 "module") (#eq? @__cjs.exp2 "exports"))
226
+ ; Fallback: { foo: <non-identifier-value> } \u2014 capture the key as the public name
227
+ ; when the value is not a named symbol (arrow fn, literal, etc).
228
+ (assignment_expression
229
+ left: (member_expression object: (identifier) @__cjs.mod2b property: (property_identifier) @__cjs.exp2b)
230
+ right: (object (pair key: (property_identifier) @export.name value: (_) @__cjs.nonId))
231
+ (#eq? @__cjs.mod2b "module") (#eq? @__cjs.exp2b "exports")
232
+ (#not-match? @__cjs.nonId "^[A-Za-z_$][A-Za-z0-9_$]*$"))
233
+ ; CommonJS: module.exports = App
234
+ (assignment_expression
235
+ left: (member_expression object: (identifier) @__cjs.mod3 property: (property_identifier) @__cjs.exp3)
236
+ right: (identifier) @export.name
237
+ (#eq? @__cjs.mod3 "module") (#eq? @__cjs.exp3 "exports"))
238
+ ; CommonJS: exports.handler = handler
239
+ (assignment_expression
240
+ left: (member_expression object: (identifier) @__cjs.exp4 property: (property_identifier) @export.name)
241
+ (#eq? @__cjs.exp4 "exports"))
242
+ `.trim();
243
+ var JS_CALLS = `
244
+ (call_expression function: (identifier) @call.callee)
245
+ (call_expression function: (member_expression property: (property_identifier) @call.callee))
246
+ `.trim();
247
+ var JS_ROUTES = `
248
+ ; Unified route pattern: captures verb + path + the arguments node.
249
+ ; emit.ts extracts the handler from the LAST argument (identifier,
250
+ ; arrow_function, or function_expression), correctly handling middleware:
251
+ ; app.get("/users", requireAuth, getUsers) \u2192 handler=getUsers
252
+ (call_expression
253
+ function: (member_expression
254
+ object: (_) @__route.app
255
+ property: (property_identifier) @route.verb)
256
+ arguments: (arguments
257
+ . (string (string_fragment) @route.path)
258
+ ) @route.args
259
+ (#match? @route.verb "^(get|post|put|patch|delete|head|options|all|use)$"))
260
+ `.trim();
261
+ var TYPESCRIPT_EXTRACTOR = {
262
+ definitionsQuery: JS_FAMILY_DEFINITIONS,
263
+ importsQuery: JS_IMPORTS,
264
+ exportsQuery: TS_EXPORTS,
265
+ callSitesQuery: JS_CALLS,
266
+ routesQuery: JS_ROUTES
267
+ };
268
+ var TSX_EXTRACTOR = TYPESCRIPT_EXTRACTOR;
269
+ var JAVASCRIPT_EXTRACTOR = {
270
+ definitionsQuery: `
271
+ (function_declaration name: (identifier) @name) @def.function
272
+ (method_definition name: (property_identifier) @name) @def.method
273
+ (class_declaration name: (identifier) @name) @def.class
274
+
275
+ ; const handler = () => {} / const handler = function () {}
276
+ (variable_declarator
277
+ name: (identifier) @name
278
+ value: [(arrow_function) (function_expression)]) @def.function
279
+ `.trim(),
280
+ importsQuery: JS_IMPORTS,
281
+ exportsQuery: JS_EXPORTS,
282
+ callSitesQuery: JS_CALLS,
283
+ routesQuery: JS_ROUTES
284
+ };
285
+ var PYTHON_EXTRACTOR = {
286
+ definitionsQuery: `
287
+ (function_definition name: (identifier) @name) @def.function
288
+ (class_definition name: (identifier) @name) @def.class
289
+ `.trim(),
290
+ importsQuery: `
291
+ (import_statement (dotted_name) @import.module) @__import.stmt
292
+ (import_from_statement module_name: (dotted_name) @import.module) @__import.stmt
293
+ ; Python relative imports: from .models import User / from ..parent import X
294
+ ; tree-sitter-python wraps the module inside a relative_import node, so the
295
+ ; module_name field is NOT set. Capture the relative_import node itself so
296
+ ; the prefix dots are preserved (..parent, not just parent) \u2014 different
297
+ ; relative levels must not collapse to the same module name
298
+ ; (chatgpt-codex-connector #1688 P2: 'Preserve dots in Python relative imports').
299
+ (import_from_statement (relative_import) @import.module) @__import.stmt
300
+ `.trim(),
301
+ exportsQuery: ``,
302
+ callSitesQuery: `
303
+ (call function: (identifier) @call.callee)
304
+ (call function: (attribute attribute: (identifier) @call.callee))
305
+ `.trim(),
306
+ routesQuery: `
307
+ (decorated_definition
308
+ (decorator
309
+ (call function: (attribute attribute: (identifier) @route.verb)
310
+ arguments: (argument_list (string) @route.path)))
311
+ definition: (function_definition name: (identifier) @route.handler)
312
+ (#match? @route.verb "^(get|post|put|patch|delete|route|api_route)$"))
313
+ `.trim()
314
+ };
315
+ var GO_EXTRACTOR = {
316
+ definitionsQuery: `
317
+ (function_declaration name: (identifier) @name) @def.function
318
+
319
+ ; Go methods sit outside their receiver struct, so byte-span nesting
320
+ ; cannot compute qualified names. Capture the receiver type_identifier
321
+ ; so extractSymbols can prefix the method name (Server.Start).
322
+ (method_declaration
323
+ receiver: (parameter_list
324
+ (parameter_declaration type: (type_identifier) @__receiver.type))
325
+ name: (field_identifier) @name) @def.method
326
+ (method_declaration
327
+ receiver: (parameter_list
328
+ (parameter_declaration type: (pointer_type (type_identifier) @__receiver.type)))
329
+ name: (field_identifier) @name) @def.method
330
+ (type_spec name: (type_identifier) @name type: (struct_type)) @def.class
331
+ (type_spec name: (type_identifier) @name type: (interface_type)) @def.interface
332
+ (type_spec name: (type_identifier) @name type: (type_identifier)) @def.type
333
+ `.trim(),
334
+ importsQuery: `
335
+ (import_spec path: (interpreted_string_literal) @import.module) @__import.stmt
336
+ `.trim(),
337
+ exportsQuery: ``,
338
+ callSitesQuery: `
339
+ (call_expression function: (identifier) @call.callee)
340
+ (call_expression function: (selector_expression field: (field_identifier) @call.callee))
341
+ `.trim(),
342
+ routesQuery: ``
343
+ };
344
+ var RUST_EXTRACTOR = {
345
+ definitionsQuery: `
346
+ (function_item name: (identifier) @name) @def.function
347
+ (function_signature_item name: (identifier) @name) @def.function
348
+ (struct_item name: (type_identifier) @name) @def.class
349
+ (enum_item name: (type_identifier) @name) @def.enum
350
+ (trait_item name: (type_identifier) @name) @def.interface
351
+ (type_item name: (type_identifier) @name) @def.type
352
+ (mod_item name: (identifier) @name) @def.module
353
+
354
+ ; Rust impl methods \u2014 the impl block sits outside the struct's byte span,
355
+ ; so byte-span nesting cannot compute the parent struct. Capture the impl
356
+ ; type_identifier so extractSymbols can prefix qualified names (Config.new).
357
+ ; These also match functions caught by the general patterns above;
358
+ ; extractSymbols deduplicates by node identity (startByte+endByte+name).
359
+ (impl_item
360
+ type: (type_identifier) @__receiver.type
361
+ body: (declaration_list
362
+ (function_item name: (identifier) @name) @def.method))
363
+ (impl_item
364
+ type: (type_identifier) @__receiver.type
365
+ body: (declaration_list
366
+ (function_signature_item name: (identifier) @name) @def.method))
367
+ `.trim(),
368
+ importsQuery: `
369
+ (use_declaration (scoped_identifier) @import.module) @__import.stmt
370
+ (use_declaration (scoped_use_list) @import.module) @__import.stmt
371
+ `.trim(),
372
+ exportsQuery: ``,
373
+ callSitesQuery: `
374
+ (call_expression function: (identifier) @call.callee)
375
+ (call_expression function: (field_expression field: (field_identifier) @call.callee))
376
+ (call_expression function: (scoped_identifier) @call.callee)
377
+ `.trim(),
378
+ routesQuery: ``
379
+ };
380
+ var JAVA_EXTRACTOR = {
381
+ definitionsQuery: `
382
+ (class_declaration name: (identifier) @name) @def.class
383
+ (interface_declaration name: (identifier) @name) @def.interface
384
+ (enum_declaration name: (identifier) @name) @def.enum
385
+ (record_declaration name: (identifier) @name) @def.class
386
+ (method_declaration name: (identifier) @name) @def.method
387
+ (constructor_declaration name: (identifier) @name) @def.method
388
+ `.trim(),
389
+ importsQuery: `
390
+ (import_declaration (scoped_identifier) @import.module) @__import.stmt
391
+ `.trim(),
392
+ exportsQuery: ``,
393
+ callSitesQuery: `
394
+ (method_invocation name: (identifier) @call.callee)
395
+ `.trim(),
396
+ routesQuery: ``
397
+ };
398
+ var C_EXTRACTOR = {
399
+ definitionsQuery: `
400
+ (function_definition declarator: (function_declarator declarator: (identifier) @name)) @def.function
401
+ (type_definition declarator: (type_identifier) @name) @def.type
402
+ `.trim(),
403
+ importsQuery: `
404
+ (preproc_include path: (system_lib_string) @import.module) @__import.stmt
405
+ (preproc_include path: (string_literal) @import.module) @__import.stmt
406
+ `.trim(),
407
+ exportsQuery: ``,
408
+ callSitesQuery: `
409
+ (call_expression function: (identifier) @call.callee)
410
+ `.trim(),
411
+ routesQuery: ``
412
+ };
413
+ var CPP_EXTRACTOR = {
414
+ definitionsQuery: `
415
+ (function_definition declarator: (function_declarator declarator: (identifier) @name)) @def.function
416
+ (function_definition declarator: (function_declarator declarator: (field_identifier) @name)) @def.method
417
+ ; C++ out-of-class method definitions: void A::start() {}
418
+ ; The qualified_identifier (A::start) encodes the full qualified name.
419
+ (function_definition declarator: (function_declarator declarator: (qualified_identifier) @name)) @def.method
420
+ (class_specifier name: (type_identifier) @name) @def.class
421
+ (struct_specifier name: (type_identifier) @name) @def.class
422
+ (enum_specifier name: (type_identifier) @name) @def.enum
423
+ (namespace_definition name: (namespace_identifier) @name) @def.module
424
+ `.trim(),
425
+ importsQuery: `
426
+ (preproc_include path: (system_lib_string) @import.module) @__import.stmt
427
+ (preproc_include path: (string_literal) @import.module) @__import.stmt
428
+ `.trim(),
429
+ exportsQuery: ``,
430
+ callSitesQuery: `
431
+ (call_expression function: (identifier) @call.callee)
432
+ (call_expression function: (field_expression field: (field_identifier) @call.callee))
433
+ `.trim(),
434
+ routesQuery: ``
435
+ };
436
+ var CSHARP_EXTRACTOR = {
437
+ definitionsQuery: `
438
+ (class_declaration name: (identifier) @name) @def.class
439
+ (interface_declaration name: (identifier) @name) @def.interface
440
+ (enum_declaration name: (identifier) @name) @def.enum
441
+ (struct_declaration name: (identifier) @name) @def.class
442
+ (method_declaration name: (identifier) @name) @def.method
443
+ `.trim(),
444
+ importsQuery: `
445
+ (using_directive (identifier) @import.module) @__import.stmt
446
+ (using_directive (qualified_name) @import.module) @__import.stmt
447
+ `.trim(),
448
+ exportsQuery: ``,
449
+ callSitesQuery: `
450
+ (invocation_expression function: (identifier) @call.callee)
451
+ (invocation_expression function: (member_access_expression name: (identifier) @call.callee))
452
+ `.trim(),
453
+ routesQuery: ``
454
+ };
455
+ var RUBY_EXTRACTOR = {
456
+ definitionsQuery: `
457
+ (class name: (constant) @name) @def.class
458
+ (module name: (constant) @name) @def.module
459
+ (method name: (identifier) @name) @def.method
460
+ (singleton_method name: (identifier) @name) @def.method
461
+ `.trim(),
462
+ importsQuery: `
463
+ (call
464
+ method: (identifier) @__import.method
465
+ arguments: (argument_list (string) @import.module)
466
+ (#match? @__import.method "^(require|require_relative|load)$")) @__import.stmt
467
+ `.trim(),
468
+ exportsQuery: ``,
469
+ callSitesQuery: `
470
+ (call method: (identifier) @call.callee)
471
+ `.trim(),
472
+ routesQuery: ``
473
+ };
474
+ var PHP_EXTRACTOR = {
475
+ definitionsQuery: `
476
+ (class_declaration name: (name) @name) @def.class
477
+ (interface_declaration name: (name) @name) @def.interface
478
+ (trait_declaration name: (name) @name) @def.class
479
+ (function_definition name: (name) @name) @def.function
480
+ (method_declaration name: (name) @name) @def.method
481
+ `.trim(),
482
+ importsQuery: `
483
+ (namespace_use_declaration (namespace_use_clause (qualified_name) @import.module)) @__import.stmt
484
+ `.trim(),
485
+ exportsQuery: ``,
486
+ callSitesQuery: `
487
+ (function_call_expression function: (name) @call.callee)
488
+ (member_call_expression (name) @call.callee)
489
+ `.trim(),
490
+ routesQuery: ``
491
+ };
492
+ var KOTLIN_EXTRACTOR = {
493
+ definitionsQuery: `
494
+ (class_declaration (type_identifier) @name) @def.class
495
+ (object_declaration (type_identifier) @name) @def.module
496
+ (function_declaration (simple_identifier) @name) @def.function
497
+ `.trim(),
498
+ importsQuery: `
499
+ ; Capture the full identifier node \u2014 its .text is the complete import
500
+ ; path (e.g. "kotlin.collections"). Do NOT capture nested simple_identifier
501
+ ; children, which would emit bogus segment-level modules.
502
+ (import_header (identifier) @import.module) @__import.stmt
503
+ `.trim(),
504
+ exportsQuery: ``,
505
+ callSitesQuery: `
506
+ (call_expression (simple_identifier) @call.callee)
507
+ `.trim(),
508
+ routesQuery: ``
509
+ };
510
+ var SWIFT_EXTRACTOR = {
511
+ definitionsQuery: `
512
+ (class_declaration name: (type_identifier) @name) @def.class
513
+ (protocol_declaration name: (type_identifier) @name) @def.interface
514
+ (function_declaration name: (simple_identifier) @name) @def.function
515
+ `.trim(),
516
+ importsQuery: `
517
+ (import_declaration (identifier) @import.module) @__import.stmt
518
+ `.trim(),
519
+ exportsQuery: ``,
520
+ callSitesQuery: `
521
+ (call_expression (simple_identifier) @call.callee)
522
+ `.trim(),
523
+ routesQuery: ``
524
+ };
525
+ var BASH_EXTRACTOR = {
526
+ definitionsQuery: `
527
+ (function_definition name: (word) @name) @def.function
528
+ `.trim(),
529
+ importsQuery: ``,
530
+ exportsQuery: ``,
531
+ callSitesQuery: `
532
+ (command name: (command_name (word) @call.callee))
533
+ `.trim(),
534
+ routesQuery: ``
535
+ };
536
+ var EXTRACTORS = {
537
+ typescript: TYPESCRIPT_EXTRACTOR,
538
+ tsx: TSX_EXTRACTOR,
539
+ javascript: JAVASCRIPT_EXTRACTOR,
540
+ python: PYTHON_EXTRACTOR,
541
+ go: GO_EXTRACTOR,
542
+ rust: RUST_EXTRACTOR,
543
+ java: JAVA_EXTRACTOR,
544
+ c: C_EXTRACTOR,
545
+ cpp: CPP_EXTRACTOR,
546
+ csharp: CSHARP_EXTRACTOR,
547
+ ruby: RUBY_EXTRACTOR,
548
+ php: PHP_EXTRACTOR,
549
+ kotlin: KOTLIN_EXTRACTOR,
550
+ swift: SWIFT_EXTRACTOR,
551
+ bash: BASH_EXTRACTOR
552
+ };
553
+ var VALID_KINDS = /* @__PURE__ */ new Set([
554
+ "function",
555
+ "method",
556
+ "class",
557
+ "interface",
558
+ "enum",
559
+ "type",
560
+ "module"
561
+ ]);
562
+ function kindFromCapture(captureName) {
563
+ if (!captureName.startsWith("def.")) return null;
564
+ const kind = captureName.slice(4);
565
+ if (!VALID_KINDS.has(kind)) return null;
566
+ return kind;
567
+ }
568
+
569
+ // src/engine/utf16-offsets.ts
570
+ function buildUtf16ToByteOffsetMap(content) {
571
+ const map = new Uint32Array(content.length + 1);
572
+ let byteOffset = 0;
573
+ for (let i = 0; i < content.length; i++) {
574
+ map[i] = byteOffset;
575
+ const code = content.charCodeAt(i);
576
+ if (code < 128) {
577
+ byteOffset += 1;
578
+ } else if (code < 2048) {
579
+ byteOffset += 2;
580
+ } else if (code >= 55296 && code <= 56319) {
581
+ byteOffset += 4;
582
+ map[i + 1] = map[i];
583
+ i++;
584
+ } else if (code >= 56320 && code <= 57343) {
585
+ byteOffset += 3;
586
+ } else {
587
+ byteOffset += 3;
588
+ }
589
+ }
590
+ map[content.length] = byteOffset;
591
+ return map;
592
+ }
593
+ function utf16ToByte(map, utf16Offset) {
594
+ if (utf16Offset <= 0) return 0;
595
+ if (utf16Offset >= map.length) return map[map.length - 1];
596
+ return map[utf16Offset];
597
+ }
598
+
599
+ // src/engine/emit.ts
600
+ function hashContent(content) {
601
+ return createHash("sha256").update(content).digest("hex");
602
+ }
603
+ function cleanModuleSpecifier(raw) {
604
+ let s = raw.trim();
605
+ if (s.startsWith("<") && s.endsWith(">")) return s.slice(1, -1);
606
+ if (s.length >= 2) {
607
+ const f = s[0];
608
+ const l = s[s.length - 1];
609
+ if ((f === '"' || f === "'" || f === "`") && f === l) return s.slice(1, -1);
610
+ }
611
+ return s;
612
+ }
613
+ function extractSymbols(root, language, lang) {
614
+ const extractor = EXTRACTORS[lang];
615
+ const query = new Query(language, extractor.definitionsQuery);
616
+ try {
617
+ const matches = query.matches(root);
618
+ const rawDefs = [];
619
+ for (const match of matches) {
620
+ let kind = null;
621
+ let nameNode = null;
622
+ let defNode = null;
623
+ let receiverType = "";
624
+ for (const cap of match.captures) {
625
+ const k = kindFromCapture(cap.name);
626
+ if (k) {
627
+ kind = k;
628
+ defNode = cap.node;
629
+ } else if (cap.name === "name") {
630
+ nameNode = cap.node;
631
+ } else if (cap.name === "__receiver.type") {
632
+ receiverType = cap.node.text;
633
+ }
634
+ }
635
+ if (!kind || !defNode || !nameNode) continue;
636
+ rawDefs.push({
637
+ kind,
638
+ name: nameNode.text,
639
+ startByte: defNode.startIndex,
640
+ endByte: defNode.endIndex,
641
+ receiverType: receiverType || void 0
642
+ });
643
+ }
644
+ const seen = /* @__PURE__ */ new Map();
645
+ for (const def of rawDefs) {
646
+ const key = `${def.startByte}:${def.endByte}:${def.name}`;
647
+ const existing = seen.get(key);
648
+ if (!existing || def.receiverType && !existing.receiverType) {
649
+ seen.set(key, def);
650
+ }
651
+ }
652
+ const deduped = [...seen.values()];
653
+ deduped.sort((a, b) => a.startByte - b.startByte || a.name.localeCompare(b.name));
654
+ const stack = [];
655
+ const symbols = [];
656
+ for (const def of deduped) {
657
+ while (stack.length > 0 && stack[stack.length - 1].endByte <= def.startByte) {
658
+ stack.pop();
659
+ }
660
+ const parentQualifiedName = def.receiverType ?? (stack.length > 0 ? stack[stack.length - 1].qualifiedName : void 0);
661
+ const qualifiedName = parentQualifiedName ? `${parentQualifiedName}.${def.name}` : def.name;
662
+ const symbol = parentQualifiedName ? {
663
+ kind: def.kind,
664
+ name: def.name,
665
+ qualifiedName,
666
+ span: { startByte: def.startByte, endByte: def.endByte },
667
+ parentQualifiedName
668
+ } : {
669
+ kind: def.kind,
670
+ name: def.name,
671
+ qualifiedName,
672
+ span: { startByte: def.startByte, endByte: def.endByte }
673
+ };
674
+ symbols.push(symbol);
675
+ stack.push({ endByte: def.endByte, qualifiedName });
676
+ }
677
+ return symbols;
678
+ } finally {
679
+ query.delete();
680
+ }
681
+ }
682
+ function extractImports(root, language, lang) {
683
+ const extractor = EXTRACTORS[lang];
684
+ if (!extractor.importsQuery) return [];
685
+ const query = new Query(language, extractor.importsQuery);
686
+ try {
687
+ const matches = query.matches(root);
688
+ const groups = /* @__PURE__ */ new Map();
689
+ for (const match of matches) {
690
+ let moduleText = "";
691
+ let stmtStart = -1;
692
+ let stmtEnd = -1;
693
+ const names = [];
694
+ for (const cap of match.captures) {
695
+ if (cap.name === "import.module") {
696
+ moduleText = cleanModuleSpecifier(cap.node.text);
697
+ } else if (cap.name === "import.name") {
698
+ names.push(cap.node.text);
699
+ } else if (cap.name === "__import.stmt") {
700
+ stmtStart = cap.node.startIndex;
701
+ stmtEnd = cap.node.endIndex;
702
+ }
703
+ }
704
+ if (stmtStart < 0) {
705
+ const firstCap = match.captures[0];
706
+ if (firstCap) {
707
+ stmtStart = firstCap.node.startIndex;
708
+ stmtEnd = firstCap.node.endIndex;
709
+ } else {
710
+ continue;
711
+ }
712
+ }
713
+ const key = `${stmtStart}:${moduleText}`;
714
+ const existing = groups.get(key);
715
+ if (existing) {
716
+ for (const n of names) existing.names.add(n);
717
+ } else {
718
+ groups.set(key, {
719
+ module: moduleText,
720
+ names: new Set(names),
721
+ startByte: stmtStart,
722
+ endByte: stmtEnd
723
+ });
724
+ }
725
+ }
726
+ return Array.from(groups.values()).map((g) => ({
727
+ module: g.module,
728
+ importedNames: Array.from(g.names).sort(),
729
+ span: { startByte: g.startByte, endByte: g.endByte }
730
+ })).sort((a, b) => a.span.startByte - b.span.startByte || a.module.localeCompare(b.module));
731
+ } finally {
732
+ query.delete();
733
+ }
734
+ }
735
+ function extractExports(root, language, lang) {
736
+ const extractor = EXTRACTORS[lang];
737
+ if (!extractor.exportsQuery) return [];
738
+ const query = new Query(language, extractor.exportsQuery);
739
+ try {
740
+ const captures = query.captures(root);
741
+ const valueExportedPairs = /* @__PURE__ */ new Set();
742
+ const pairOf = (node) => {
743
+ let cur = node;
744
+ for (let i = 0; i < 5 && cur; i++) {
745
+ if (cur.type === "pair") return cur;
746
+ cur = cur.parent;
747
+ }
748
+ return null;
749
+ };
750
+ for (const cap of captures) {
751
+ if (cap.name !== "export.name") continue;
752
+ const pair = pairOf(cap.node);
753
+ if (pair && cap.node.type === "identifier") {
754
+ valueExportedPairs.add(pair.id);
755
+ }
756
+ }
757
+ const exports = [];
758
+ for (const cap of captures) {
759
+ if (cap.name !== "export.name") continue;
760
+ const pair = pairOf(cap.node);
761
+ if (pair && cap.node.type === "property_identifier" && valueExportedPairs.has(pair.id)) {
762
+ continue;
763
+ }
764
+ exports.push({
765
+ name: cap.node.text,
766
+ span: { startByte: cap.node.startIndex, endByte: cap.node.endIndex }
767
+ });
768
+ }
769
+ return exports.sort(
770
+ (a, b) => a.span.startByte - b.span.startByte || a.name.localeCompare(b.name)
771
+ );
772
+ } finally {
773
+ query.delete();
774
+ }
775
+ }
776
+ function extractCallSites(root, language, lang) {
777
+ const extractor = EXTRACTORS[lang];
778
+ if (!extractor.callSitesQuery) return [];
779
+ const query = new Query(language, extractor.callSitesQuery);
780
+ try {
781
+ const captures = query.captures(root);
782
+ const callSites = [];
783
+ for (const cap of captures) {
784
+ if (cap.name === "call.callee") {
785
+ callSites.push({
786
+ calleeNameCandidates: [cap.node.text],
787
+ span: { startByte: cap.node.startIndex, endByte: cap.node.endIndex }
788
+ });
789
+ }
790
+ }
791
+ return callSites.sort(
792
+ (a, b) => a.span.startByte - b.span.startByte || (a.calleeNameCandidates[0] ?? "").localeCompare(b.calleeNameCandidates[0] ?? "")
793
+ );
794
+ } finally {
795
+ query.delete();
796
+ }
797
+ }
798
+ var HTTP_CLIENT_OBJECT_PATTERNS = /^(http|https|client|httpClient|axios|fetch|request|req|res|\$|superagent|got)$/;
799
+ function extractRoutes(root, language, lang) {
800
+ const extractor = EXTRACTORS[lang];
801
+ if (!extractor.routesQuery) return [];
802
+ const query = new Query(language, extractor.routesQuery);
803
+ try {
804
+ const matches = query.matches(root);
805
+ const routes = [];
806
+ for (const match of matches) {
807
+ let verb = "";
808
+ let pathTemplate = "";
809
+ let handler = "";
810
+ let startByte = 0;
811
+ let endByte = 0;
812
+ let argsNode = null;
813
+ let routeObject = "";
814
+ for (const cap of match.captures) {
815
+ if (cap.name === "route.verb") {
816
+ verb = cap.node.text.toUpperCase();
817
+ startByte = cap.node.parent?.startIndex ?? cap.node.startIndex;
818
+ const memberExpr = cap.node.parent;
819
+ const objectNode = memberExpr?.childForFieldName("object");
820
+ if (objectNode) {
821
+ let receiver = objectNode;
822
+ for (let prop = receiver.childForFieldName("property"); prop; prop = receiver.childForFieldName("property")) {
823
+ receiver = prop;
824
+ }
825
+ routeObject = receiver.text;
826
+ }
827
+ } else if (cap.name === "route.path") {
828
+ pathTemplate = cleanModuleSpecifier(cap.node.text);
829
+ } else if (cap.name === "route.handler") {
830
+ handler = cap.node.type === "identifier" ? cap.node.text : findHandlerName(cap.node) ?? "anonymous";
831
+ endByte = cap.node.endIndex;
832
+ } else if (cap.name === "route.args") {
833
+ argsNode = cap.node;
834
+ endByte = cap.node.endIndex;
835
+ }
836
+ }
837
+ if (argsNode) {
838
+ handler = extractHandlerFromArgs(argsNode);
839
+ }
840
+ const isRoutePath = pathTemplate.startsWith("/") || pathTemplate.startsWith("*");
841
+ const isHttpClient = HTTP_CLIENT_OBJECT_PATTERNS.test(routeObject);
842
+ if (verb && pathTemplate && handler && isRoutePath && !isHttpClient) {
843
+ routes.push({
844
+ verb,
845
+ pathTemplate,
846
+ handlerQualifiedName: handler,
847
+ span: { startByte, endByte }
848
+ });
849
+ }
850
+ }
851
+ return routes.sort(
852
+ (a, b) => a.span.startByte - b.span.startByte || a.pathTemplate.localeCompare(b.pathTemplate)
853
+ );
854
+ } finally {
855
+ query.delete();
856
+ }
857
+ }
858
+ function extractHandlerFromArgs(argsNode) {
859
+ const realArgs = [];
860
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
861
+ const child = argsNode.namedChild(i);
862
+ if (child && child.type !== "comment") realArgs.push(child);
863
+ }
864
+ if (realArgs.length < 2) return "";
865
+ const lastArg = realArgs[realArgs.length - 1];
866
+ if (lastArg.type === "identifier") {
867
+ return lastArg.text;
868
+ }
869
+ if (lastArg.type === "function_expression") {
870
+ return findHandlerName(lastArg) ?? "anonymous";
871
+ }
872
+ if (lastArg.type === "arrow_function") {
873
+ return "anonymous";
874
+ }
875
+ return "";
876
+ }
877
+ function findHandlerName(node) {
878
+ for (const child of node.namedChildren) {
879
+ if (child && child.type === "identifier") return child.text;
880
+ }
881
+ const nameChild = node.childForFieldName("name");
882
+ if (nameChild) return nameChild.text;
883
+ return null;
884
+ }
885
+ function emitFileIR(filePath, lang, content, root, language, contentStr) {
886
+ const symbols = extractSymbols(root, language, lang);
887
+ const imports = extractImports(root, language, lang);
888
+ const exports = extractExports(root, language, lang);
889
+ const callSites = extractCallSites(root, language, lang);
890
+ const routes = extractRoutes(root, language, lang);
891
+ const offsetMap = buildUtf16ToByteOffsetMap(contentStr);
892
+ const convSymbols = symbols.map((s) => ({
893
+ ...s,
894
+ span: {
895
+ startByte: utf16ToByte(offsetMap, s.span.startByte),
896
+ endByte: utf16ToByte(offsetMap, s.span.endByte)
897
+ }
898
+ }));
899
+ const convImports = imports.map((i) => ({
900
+ ...i,
901
+ span: {
902
+ startByte: utf16ToByte(offsetMap, i.span.startByte),
903
+ endByte: utf16ToByte(offsetMap, i.span.endByte)
904
+ }
905
+ }));
906
+ const convExports = exports.map((e) => ({
907
+ ...e,
908
+ span: {
909
+ startByte: utf16ToByte(offsetMap, e.span.startByte),
910
+ endByte: utf16ToByte(offsetMap, e.span.endByte)
911
+ }
912
+ }));
913
+ const convCallSites = callSites.map((c) => ({
914
+ ...c,
915
+ span: {
916
+ startByte: utf16ToByte(offsetMap, c.span.startByte),
917
+ endByte: utf16ToByte(offsetMap, c.span.endByte)
918
+ }
919
+ }));
920
+ const convRoutes = routes.map((r) => ({
921
+ ...r,
922
+ span: {
923
+ startByte: utf16ToByte(offsetMap, r.span.startByte),
924
+ endByte: utf16ToByte(offsetMap, r.span.endByte)
925
+ }
926
+ }));
927
+ return {
928
+ path: filePath,
929
+ language: lang,
930
+ contentHash: hashContent(content),
931
+ symbols: convSymbols,
932
+ imports: convImports,
933
+ exports: convExports,
934
+ callSites: convCallSites,
935
+ routes: convRoutes
936
+ };
937
+ }
938
+
939
+ // src/engine/language-sniff.ts
940
+ import { TIER_1_LANGUAGES } from "@remnic/core";
941
+ var EXTENSION_MAP = {
942
+ ".ts": "typescript",
943
+ ".tsx": "tsx",
944
+ ".mts": "typescript",
945
+ ".cts": "typescript",
946
+ ".js": "javascript",
947
+ ".jsx": "javascript",
948
+ ".mjs": "javascript",
949
+ ".cjs": "javascript",
950
+ ".py": "python",
951
+ ".pyi": "python",
952
+ ".go": "go",
953
+ ".rs": "rust",
954
+ ".java": "java",
955
+ ".c": "c",
956
+ ".h": "c",
957
+ ".cpp": "cpp",
958
+ ".cc": "cpp",
959
+ ".cxx": "cpp",
960
+ ".hpp": "cpp",
961
+ ".hxx": "cpp",
962
+ ".cs": "csharp",
963
+ ".rb": "ruby",
964
+ ".php": "php",
965
+ ".kt": "kotlin",
966
+ ".kts": "kotlin",
967
+ ".swift": "swift",
968
+ ".sh": "bash",
969
+ ".bash": "bash"
970
+ };
971
+ function sniffLanguage(filePath) {
972
+ const dot = filePath.lastIndexOf(".");
973
+ if (dot < 0) return null;
974
+ const ext = filePath.slice(dot).toLowerCase();
975
+ return EXTENSION_MAP[ext] ?? null;
976
+ }
977
+ function isTier1Language(lang) {
978
+ return TIER_1_LANGUAGES.includes(lang);
979
+ }
980
+
981
+ // src/engine/engine.ts
982
+ var CodingGraphEngineImpl = class {
983
+ engineVersion;
984
+ supportedLanguages;
985
+ backend;
986
+ disposed = false;
987
+ /**
988
+ * Serialize parse calls. The backend's single Parser instance is shared
989
+ * across all languages, so concurrent setLanguage/parse calls would race.
990
+ * Each parseFile call awaits the previous before touching the parser.
991
+ */
992
+ parseChain = Promise.resolve();
993
+ constructor(backend) {
994
+ this.engineVersion = CODING_GRAPH_ENGINE_VERSION;
995
+ this.supportedLanguages = TIER_1_LANGUAGES2;
996
+ this.backend = backend;
997
+ }
998
+ async parseFile(input) {
999
+ if (this.disposed) {
1000
+ return {
1001
+ ok: false,
1002
+ code: "parse_failed",
1003
+ path: input.path,
1004
+ message: "engine has been disposed"
1005
+ };
1006
+ }
1007
+ const previous = this.parseChain;
1008
+ let release;
1009
+ this.parseChain = new Promise((resolve) => {
1010
+ release = resolve;
1011
+ });
1012
+ await previous;
1013
+ if (this.disposed) {
1014
+ release();
1015
+ return {
1016
+ ok: false,
1017
+ code: "parse_failed",
1018
+ path: input.path,
1019
+ message: "engine has been disposed"
1020
+ };
1021
+ }
1022
+ try {
1023
+ return await this.doParseFile(input);
1024
+ } finally {
1025
+ release();
1026
+ }
1027
+ }
1028
+ async doParseFile(input) {
1029
+ const lang = input.language ?? sniffLanguage(input.path);
1030
+ if (!lang || !isTier1Language(lang)) {
1031
+ return {
1032
+ ok: false,
1033
+ code: "parse_failed",
1034
+ path: input.path,
1035
+ message: `unsupported language for path "${input.path}"; supported extensions map to: ${TIER_1_LANGUAGES2.join(", ")}`
1036
+ };
1037
+ }
1038
+ try {
1039
+ await this.backend.ensureLanguage(lang);
1040
+ } catch (err) {
1041
+ return {
1042
+ ok: false,
1043
+ code: "parse_failed",
1044
+ path: input.path,
1045
+ message: `failed to load grammar for ${lang}: ${err instanceof Error ? err.message : String(err)}`
1046
+ };
1047
+ }
1048
+ const contentStr = Buffer.from(input.content).toString("utf-8");
1049
+ const tree = this.backend.parse(lang, contentStr);
1050
+ if (!tree) {
1051
+ return {
1052
+ ok: false,
1053
+ code: "parse_failed",
1054
+ path: input.path,
1055
+ message: `tree-sitter returned null for ${lang} (grammar may be corrupt)`
1056
+ };
1057
+ }
1058
+ try {
1059
+ const root = tree.rootNode;
1060
+ const language = this.backend.getLanguage(lang);
1061
+ if (!language) {
1062
+ return {
1063
+ ok: false,
1064
+ code: "parse_failed",
1065
+ path: input.path,
1066
+ message: `language object unavailable for ${lang}`
1067
+ };
1068
+ }
1069
+ const ir = emitFileIR(
1070
+ input.path,
1071
+ lang,
1072
+ input.content,
1073
+ root,
1074
+ language,
1075
+ contentStr
1076
+ );
1077
+ return { ok: true, ir };
1078
+ } catch (err) {
1079
+ return {
1080
+ ok: false,
1081
+ code: "parse_failed",
1082
+ path: input.path,
1083
+ message: `extraction failed for ${lang}: ${err instanceof Error ? err.message : String(err)}`
1084
+ };
1085
+ } finally {
1086
+ tree.delete();
1087
+ }
1088
+ }
1089
+ async dispose() {
1090
+ if (this.disposed) return;
1091
+ this.disposed = true;
1092
+ await this.parseChain;
1093
+ await this.backend.dispose();
1094
+ }
1095
+ };
1096
+ function createCodingGraphEngine(_options = {}) {
1097
+ const backend = new WasmTreeSitterBackend();
1098
+ return new CodingGraphEngineImpl(backend);
1099
+ }
1100
+
1101
+ // src/reindex.ts
1102
+ import { readFile as fsReadFile, realpath as fsRealpath } from "fs/promises";
1103
+ import path2 from "path";
1104
+ var META_KEY_LAST_HEAD = "last_indexed_head";
1105
+ var META_KEY_PENDING_PARSE_FAILURES = "pending_parse_failures";
1106
+ function planReindex(lastState, facts) {
1107
+ if (facts.currentHead === null) {
1108
+ return { mode: "noop", reason: "repo has no commits (HEAD is null)" };
1109
+ }
1110
+ if (lastState.lastHead === null) {
1111
+ return { mode: "full", reason: "no prior last_indexed_head \u2014 first index" };
1112
+ }
1113
+ if (lastState.lastHead === facts.currentHead) {
1114
+ return { mode: "noop", reason: "HEAD unchanged since last index" };
1115
+ }
1116
+ if (!facts.lastHeadReachable) {
1117
+ return {
1118
+ mode: "hash_scan",
1119
+ reason: `last_indexed_head ${lastState.lastHead.slice(0, 12)} is unreachable (rebase/force-push)`,
1120
+ // mismatchedPaths is filled by the executor (needs to read files).
1121
+ mismatchedPaths: []
1122
+ };
1123
+ }
1124
+ const changedPaths = [];
1125
+ for (const entry of facts.changedFiles) {
1126
+ changedPaths.push(entry.path);
1127
+ if (entry.oldPath !== void 0 && entry.oldPath !== entry.path) {
1128
+ changedPaths.push(entry.oldPath);
1129
+ }
1130
+ }
1131
+ return { mode: "incremental", changedPaths };
1132
+ }
1133
+ function readLastIndexedHead(store) {
1134
+ return store.readMeta(META_KEY_LAST_HEAD);
1135
+ }
1136
+ function readFileHashes(store) {
1137
+ return store.readFileHashes();
1138
+ }
1139
+ function isCanonicalRelativePath(p) {
1140
+ if (typeof p !== "string" || p.length === 0) return false;
1141
+ if (p.includes("\\")) return false;
1142
+ if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) return false;
1143
+ if (p.split("/").some((seg) => seg === "." || seg === "..")) return false;
1144
+ return true;
1145
+ }
1146
+ async function probeRead(repoRoot, relPath, readFile2) {
1147
+ if (!isCanonicalRelativePath(relPath)) return { kind: "skip" };
1148
+ const probeAbs = resolveRepoPath(repoRoot, relPath);
1149
+ if (await symlinkEscapesRoot(repoRoot, probeAbs)) return { kind: "skip" };
1150
+ try {
1151
+ const content = await readFile2(probeAbs);
1152
+ return { kind: "exists", content };
1153
+ } catch (e) {
1154
+ const code = e && typeof e === "object" ? e.code : void 0;
1155
+ if (code === "ENOENT") return { kind: "missing" };
1156
+ return { kind: "unknown" };
1157
+ }
1158
+ }
1159
+ function resolveRepoPath(repoRoot, relPath) {
1160
+ return path2.resolve(repoRoot, ...relPath.split("/"));
1161
+ }
1162
+ async function symlinkEscapesRoot(repoRoot, absPath) {
1163
+ try {
1164
+ const [realRoot, realAbs] = await Promise.all([
1165
+ fsRealpath(repoRoot),
1166
+ fsRealpath(absPath)
1167
+ ]);
1168
+ const rel = path2.relative(realRoot, realAbs);
1169
+ return rel === ".." || rel.startsWith(".." + path2.sep) || path2.isAbsolute(rel);
1170
+ } catch {
1171
+ return false;
1172
+ }
1173
+ }
1174
+ async function defaultReadFile(absPath) {
1175
+ const buf = await fsReadFile(absPath);
1176
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
1177
+ }
1178
+ var reindexLocks = /* @__PURE__ */ new WeakMap();
1179
+ async function executeReindex(options) {
1180
+ const { store, git, repoRoot, parseFile } = options;
1181
+ const readFile2 = options.readFile ?? defaultReadFile;
1182
+ const prev = reindexLocks.get(store) ?? Promise.resolve();
1183
+ let release;
1184
+ const next = new Promise((resolve) => {
1185
+ release = resolve;
1186
+ });
1187
+ reindexLocks.set(store, prev.then(() => next));
1188
+ await prev;
1189
+ try {
1190
+ return await runReindex(store, git, repoRoot, parseFile, readFile2, options);
1191
+ } finally {
1192
+ release();
1193
+ }
1194
+ }
1195
+ async function runReindex(store, git, repoRoot, parseFile, readFile2, options) {
1196
+ const lastHeadRead = readLastIndexedHead(store);
1197
+ if (!lastHeadRead.ok) {
1198
+ return {
1199
+ ok: false,
1200
+ code: "store_error",
1201
+ message: `read last_indexed_head: ${lastHeadRead.code}`
1202
+ };
1203
+ }
1204
+ const lastHead = lastHeadRead.value;
1205
+ const headResult = git.revParseHead(repoRoot);
1206
+ if (!headResult.ok) return headResult;
1207
+ let reachable = true;
1208
+ if (lastHead !== null && headResult.head !== null) {
1209
+ const reachResult = git.isReachable(repoRoot, lastHead);
1210
+ if (!reachResult.ok) return reachResult;
1211
+ reachable = reachResult.reachable;
1212
+ }
1213
+ let changedFiles = [];
1214
+ if (lastHead !== null && headResult.head !== null && reachable && lastHead !== headResult.head) {
1215
+ const diffResult = git.diffNameStatus(
1216
+ repoRoot,
1217
+ `${lastHead}..${headResult.head}`
1218
+ );
1219
+ if (!diffResult.ok) {
1220
+ reachable = false;
1221
+ } else {
1222
+ changedFiles = [...diffResult.entries];
1223
+ }
1224
+ }
1225
+ const facts = {
1226
+ currentHead: headResult.head,
1227
+ lastHeadReachable: reachable,
1228
+ changedFiles
1229
+ };
1230
+ const fileHashesRead = readFileHashes(store);
1231
+ if (!fileHashesRead.ok) {
1232
+ return {
1233
+ ok: false,
1234
+ code: "store_error",
1235
+ message: `readFileHashes: ${fileHashesRead.code}`
1236
+ };
1237
+ }
1238
+ const fileHashes = fileHashesRead.hashes;
1239
+ const lastState = {
1240
+ lastHead,
1241
+ fileHashes
1242
+ };
1243
+ const plan = planReindex(lastState, facts);
1244
+ const pendingRetryRead = readPendingParseFailures(store);
1245
+ if (!pendingRetryRead.ok) {
1246
+ return {
1247
+ ok: false,
1248
+ code: "store_error",
1249
+ message: `read pending_parse_failures: ${pendingRetryRead.code}`
1250
+ };
1251
+ }
1252
+ const pendingRetry = pendingRetryRead.paths;
1253
+ switch (plan.mode) {
1254
+ case "noop": {
1255
+ if (pendingRetry.length === 0) {
1256
+ return { ok: true, mode: "noop", filesIngested: 0, head: lastHead };
1257
+ }
1258
+ const ingestResult = await ingestFiles(
1259
+ store,
1260
+ repoRoot,
1261
+ parseFile,
1262
+ readFile2,
1263
+ pendingRetry
1264
+ );
1265
+ if (!ingestResult.ok) return ingestResult;
1266
+ store.writeMeta(
1267
+ META_KEY_PENDING_PARSE_FAILURES,
1268
+ JSON.stringify(ingestResult.parseFailedPaths)
1269
+ );
1270
+ return {
1271
+ ok: true,
1272
+ mode: "noop",
1273
+ filesIngested: ingestResult.count,
1274
+ head: lastHead
1275
+ };
1276
+ }
1277
+ case "full": {
1278
+ const candidatesProvided = options.candidatePaths !== void 0 && options.candidatePaths.length > 0;
1279
+ const candidates = options.candidatePaths ?? [];
1280
+ if (candidates.length === 0 && pendingRetry.length === 0) {
1281
+ return { ok: true, mode: "noop", filesIngested: 0, head: lastHead };
1282
+ }
1283
+ const toIngest = [.../* @__PURE__ */ new Set([...candidates, ...pendingRetry])];
1284
+ let fullDelete = [];
1285
+ if (candidatesProvided) {
1286
+ const fullCandidateSet = new Set(toIngest);
1287
+ fullDelete = [...fileHashes.keys()].filter(
1288
+ (p) => !fullCandidateSet.has(p)
1289
+ );
1290
+ }
1291
+ const ingestResult = await ingestFiles(
1292
+ store,
1293
+ repoRoot,
1294
+ parseFile,
1295
+ readFile2,
1296
+ toIngest,
1297
+ fullDelete
1298
+ );
1299
+ if (!ingestResult.ok) return ingestResult;
1300
+ store.writeMeta(
1301
+ META_KEY_PENDING_PARSE_FAILURES,
1302
+ JSON.stringify(
1303
+ computeNextPending({
1304
+ priorPending: pendingRetry,
1305
+ parseFailedPaths: ingestResult.parseFailedPaths,
1306
+ ingestedCandidates: toIngest,
1307
+ deleted: fullDelete
1308
+ })
1309
+ )
1310
+ );
1311
+ if (candidatesProvided) {
1312
+ store.writeMeta(META_KEY_LAST_HEAD, headResult.head ?? "");
1313
+ }
1314
+ return {
1315
+ ok: true,
1316
+ mode: "full",
1317
+ filesIngested: ingestResult.count,
1318
+ head: candidatesProvided ? headResult.head : lastHead
1319
+ };
1320
+ }
1321
+ case "incremental": {
1322
+ const seen = /* @__PURE__ */ new Set();
1323
+ for (const p2 of plan.changedPaths) seen.add(p2);
1324
+ for (const p2 of pendingRetry) seen.add(p2);
1325
+ const knownFiles = fileHashes;
1326
+ const toDelete = [];
1327
+ const toIngest = [];
1328
+ const missingNew = [];
1329
+ for (const p2 of seen) {
1330
+ const probe = await probeRead(repoRoot, p2, readFile2);
1331
+ if (probe.kind === "skip") {
1332
+ if (knownFiles.has(p2)) toDelete.push(p2);
1333
+ continue;
1334
+ }
1335
+ if (probe.kind === "exists" || probe.kind === "unknown") {
1336
+ toIngest.push(p2);
1337
+ } else if (probe.kind === "missing") {
1338
+ if (knownFiles.has(p2)) toDelete.push(p2);
1339
+ else missingNew.push(p2);
1340
+ }
1341
+ }
1342
+ const ingestResult = await ingestFiles(
1343
+ store,
1344
+ repoRoot,
1345
+ parseFile,
1346
+ readFile2,
1347
+ toIngest,
1348
+ toDelete
1349
+ );
1350
+ if (!ingestResult.ok) return ingestResult;
1351
+ store.writeMeta(
1352
+ META_KEY_PENDING_PARSE_FAILURES,
1353
+ JSON.stringify(
1354
+ computeNextPending({
1355
+ priorPending: pendingRetry,
1356
+ parseFailedPaths: ingestResult.parseFailedPaths,
1357
+ extraRetry: missingNew,
1358
+ ingestedCandidates: toIngest,
1359
+ deleted: toDelete
1360
+ })
1361
+ )
1362
+ );
1363
+ store.writeMeta(META_KEY_LAST_HEAD, headResult.head ?? "");
1364
+ return {
1365
+ ok: true,
1366
+ mode: "incremental",
1367
+ filesIngested: ingestResult.count,
1368
+ head: headResult.head
1369
+ };
1370
+ }
1371
+ case "hash_scan": {
1372
+ const hashScanCandidatesProvided = options.candidatePaths !== void 0 && options.candidatePaths.length > 0;
1373
+ const candidateSet = /* @__PURE__ */ new Set([
1374
+ ...options.candidatePaths ?? [],
1375
+ ...lastState.fileHashes.keys(),
1376
+ ...pendingRetry
1377
+ ]);
1378
+ const knownFiles = fileHashes;
1379
+ const toDelete = [];
1380
+ const toIngest = [];
1381
+ const hashScanRetry = [];
1382
+ for (const candidatePath of candidateSet) {
1383
+ const probe = await probeRead(repoRoot, candidatePath, readFile2);
1384
+ if (probe.kind === "skip") {
1385
+ if (knownFiles.has(candidatePath)) toDelete.push(candidatePath);
1386
+ continue;
1387
+ }
1388
+ if (probe.kind === "missing") {
1389
+ if (knownFiles.has(candidatePath)) toDelete.push(candidatePath);
1390
+ else hashScanRetry.push(candidatePath);
1391
+ continue;
1392
+ }
1393
+ if (probe.kind === "unknown") {
1394
+ hashScanRetry.push(candidatePath);
1395
+ continue;
1396
+ }
1397
+ const currentHash = hashContent(probe.content);
1398
+ const storedHash = lastState.fileHashes.get(candidatePath);
1399
+ if (storedHash !== currentHash) {
1400
+ toIngest.push(candidatePath);
1401
+ }
1402
+ }
1403
+ const ingestResult = await ingestFiles(
1404
+ store,
1405
+ repoRoot,
1406
+ parseFile,
1407
+ readFile2,
1408
+ toIngest,
1409
+ toDelete
1410
+ );
1411
+ if (!ingestResult.ok) return ingestResult;
1412
+ store.writeMeta(
1413
+ META_KEY_PENDING_PARSE_FAILURES,
1414
+ JSON.stringify(
1415
+ computeNextPending({
1416
+ priorPending: pendingRetry,
1417
+ parseFailedPaths: ingestResult.parseFailedPaths,
1418
+ extraRetry: hashScanRetry,
1419
+ ingestedCandidates: toIngest,
1420
+ deleted: toDelete
1421
+ })
1422
+ )
1423
+ );
1424
+ if (hashScanCandidatesProvided) {
1425
+ store.writeMeta(META_KEY_LAST_HEAD, headResult.head ?? "");
1426
+ }
1427
+ return {
1428
+ ok: true,
1429
+ mode: "hash_scan",
1430
+ filesIngested: ingestResult.count,
1431
+ head: hashScanCandidatesProvided ? headResult.head : lastHead
1432
+ };
1433
+ }
1434
+ }
1435
+ }
1436
+ function readPendingParseFailures(store) {
1437
+ const rawRead = store.readMeta(META_KEY_PENDING_PARSE_FAILURES);
1438
+ if (!rawRead.ok) return rawRead;
1439
+ const raw = rawRead.value;
1440
+ if (raw === null) return { ok: true, paths: [] };
1441
+ try {
1442
+ const parsed = JSON.parse(raw);
1443
+ if (!Array.isArray(parsed)) return { ok: true, paths: [] };
1444
+ return {
1445
+ ok: true,
1446
+ paths: parsed.filter((p) => typeof p === "string")
1447
+ };
1448
+ } catch {
1449
+ return { ok: true, paths: [] };
1450
+ }
1451
+ }
1452
+ function computeNextPending(args) {
1453
+ const failed = new Set(args.parseFailedPaths);
1454
+ const successfullyIngested = new Set(
1455
+ args.ingestedCandidates.filter((p) => !failed.has(p))
1456
+ );
1457
+ const deleted = new Set(args.deleted);
1458
+ const next = /* @__PURE__ */ new Set();
1459
+ for (const path7 of [
1460
+ ...args.priorPending,
1461
+ ...args.parseFailedPaths,
1462
+ ...args.extraRetry ?? []
1463
+ ]) {
1464
+ if (successfullyIngested.has(path7) || deleted.has(path7)) continue;
1465
+ next.add(path7);
1466
+ }
1467
+ return [...next];
1468
+ }
1469
+ async function ingestFiles(store, repoRoot, parseFile, readFile2, paths, deletePaths = []) {
1470
+ const batch = [];
1471
+ const parseFailedPaths = [];
1472
+ for (const relPath of paths) {
1473
+ if (!isCanonicalRelativePath(relPath)) {
1474
+ parseFailedPaths.push(relPath);
1475
+ continue;
1476
+ }
1477
+ const ingestAbs = resolveRepoPath(repoRoot, relPath);
1478
+ if (await symlinkEscapesRoot(repoRoot, ingestAbs)) continue;
1479
+ let content;
1480
+ try {
1481
+ content = await readFile2(ingestAbs);
1482
+ } catch {
1483
+ parseFailedPaths.push(relPath);
1484
+ continue;
1485
+ }
1486
+ const parseResult = await parseFile({ path: relPath, content });
1487
+ if (!parseResult.ok) {
1488
+ parseFailedPaths.push(relPath);
1489
+ continue;
1490
+ }
1491
+ batch.push(parseResult.ir);
1492
+ }
1493
+ const upsertResult = await store.upsertFileBatch(batch, deletePaths);
1494
+ if (!upsertResult.ok) {
1495
+ return {
1496
+ ok: false,
1497
+ code: "store_error",
1498
+ message: `upsertFileBatch failed: ${upsertResult.code}`
1499
+ };
1500
+ }
1501
+ return { ok: true, count: batch.length, parseFailedPaths };
1502
+ }
1503
+
1504
+ // src/detect-changes.ts
1505
+ var BLAST_RADIUS_EDGE_TYPES = [
1506
+ "CALLS",
1507
+ "IMPORTS",
1508
+ "USES_TYPE"
1509
+ ];
1510
+ var FAN_IN_ESCALATION_THRESHOLD = 5;
1511
+ var DEFAULT_BLAST_RADIUS_DEPTH = 3;
1512
+ var STORE_FAILURE_CODES = /* @__PURE__ */ new Set([
1513
+ "db_locked",
1514
+ "db_corrupt",
1515
+ "db_error",
1516
+ "store_closed"
1517
+ ]);
1518
+ function isStoreFailureCode(code) {
1519
+ return STORE_FAILURE_CODES.has(code);
1520
+ }
1521
+ function byteSpanToLines(content, startByte, endByte) {
1522
+ let line = 1;
1523
+ let startLine = 1;
1524
+ let endLine = 1;
1525
+ for (let i = 0; i < content.length; i += 1) {
1526
+ if (i === startByte) startLine = line;
1527
+ if (content[i] === 10) line += 1;
1528
+ if (i === endByte - 1 && content[i] === 10) {
1529
+ endLine = line;
1530
+ } else if (i + 1 === endByte) {
1531
+ endLine = line + (content[i] === 10 ? 0 : 1);
1532
+ }
1533
+ }
1534
+ if (startByte >= content.length) startLine = line;
1535
+ if (endByte > content.length) endLine = line + 1;
1536
+ return { startLine, endLine };
1537
+ }
1538
+ function rangesOverlap(a, b) {
1539
+ return a.startLine < b.endLine && b.startLine < a.endLine;
1540
+ }
1541
+ function classifyRisk(depth, fanIn) {
1542
+ let base;
1543
+ if (depth === 0) base = "direct";
1544
+ else if (depth === 1) base = "near";
1545
+ else base = "transitive";
1546
+ if (fanIn >= FAN_IN_ESCALATION_THRESHOLD) {
1547
+ if (base === "near") base = "direct";
1548
+ else if (base === "transitive") base = "near";
1549
+ }
1550
+ return base;
1551
+ }
1552
+ function findDirectlyAffectedSymbols(hunksByPath, freshIRs, contentsByPath) {
1553
+ const affected = /* @__PURE__ */ new Set();
1554
+ for (const [filePath, hunks] of hunksByPath) {
1555
+ const ir = freshIRs.get(filePath);
1556
+ if (!ir) continue;
1557
+ const content = contentsByPath.get(filePath);
1558
+ if (!content) continue;
1559
+ for (const sym of ir.symbols) {
1560
+ const symLines = byteSpanToLines(
1561
+ content,
1562
+ sym.span.startByte,
1563
+ sym.span.endByte
1564
+ );
1565
+ for (const hunk of hunks) {
1566
+ if (rangesOverlap(symLines, hunk.newRange)) {
1567
+ affected.add(nodeIdFor({ qualifiedName: sym.qualifiedName, filePath, label: sym.kind }));
1568
+ break;
1569
+ }
1570
+ }
1571
+ }
1572
+ }
1573
+ return affected;
1574
+ }
1575
+ function computeBlastRadius(store, directlyAffected, maxDepth = DEFAULT_BLAST_RADIUS_DEPTH) {
1576
+ if (directlyAffected.size === 0) return { ok: true, affected: [] };
1577
+ const hitByDepth = /* @__PURE__ */ new Map();
1578
+ const hitMeta = /* @__PURE__ */ new Map();
1579
+ for (const qname of directlyAffected) {
1580
+ const result = store.traverse({
1581
+ start: qname,
1582
+ direction: "incoming",
1583
+ edgeTypes: [...BLAST_RADIUS_EDGE_TYPES],
1584
+ maxDepth
1585
+ });
1586
+ if (!result.ok) {
1587
+ if (isStoreFailureCode(result.code)) {
1588
+ return { ok: false, code: "store_error" };
1589
+ }
1590
+ continue;
1591
+ }
1592
+ for (const hit of result.hits) {
1593
+ const existing = hitByDepth.get(hit.nodeId);
1594
+ if (existing === void 0 || hit.depth < existing) {
1595
+ hitByDepth.set(hit.nodeId, hit.depth);
1596
+ hitMeta.set(hit.nodeId, {
1597
+ qualifiedName: hit.qualifiedName,
1598
+ name: hit.name,
1599
+ label: hit.label,
1600
+ filePath: hit.filePath
1601
+ });
1602
+ }
1603
+ }
1604
+ }
1605
+ const out = [];
1606
+ for (const [nodeId, depth] of hitByDepth) {
1607
+ const meta = hitMeta.get(nodeId);
1608
+ if (!meta) continue;
1609
+ const inboundResult = store.traverse({
1610
+ start: nodeId,
1611
+ direction: "incoming",
1612
+ edgeTypes: [...BLAST_RADIUS_EDGE_TYPES],
1613
+ maxDepth: 1
1614
+ });
1615
+ let fanIn;
1616
+ if (inboundResult.ok) {
1617
+ fanIn = inboundResult.hits.filter((h) => h.depth > 0).length;
1618
+ } else if (isStoreFailureCode(inboundResult.code)) {
1619
+ return { ok: false, code: "store_error" };
1620
+ } else {
1621
+ fanIn = 0;
1622
+ }
1623
+ const risk = classifyRisk(depth, fanIn);
1624
+ out.push({
1625
+ qualifiedName: meta.qualifiedName,
1626
+ name: meta.name,
1627
+ label: meta.label,
1628
+ filePath: meta.filePath,
1629
+ risk,
1630
+ depth,
1631
+ fanIn
1632
+ });
1633
+ }
1634
+ const riskOrder = {
1635
+ direct: 0,
1636
+ near: 1,
1637
+ transitive: 2
1638
+ };
1639
+ const nodeIdByQFile = /* @__PURE__ */ new Map();
1640
+ for (const [nodeId, depth] of hitByDepth) {
1641
+ const m = hitMeta.get(nodeId);
1642
+ if (m) nodeIdByQFile.set(`${m.qualifiedName}\0${m.filePath}\0${m.label}`, nodeId);
1643
+ void depth;
1644
+ }
1645
+ out.sort((a, b) => {
1646
+ const riskDiff = riskOrder[a.risk] - riskOrder[b.risk];
1647
+ if (riskDiff !== 0) return riskDiff;
1648
+ const qCmp = a.qualifiedName.localeCompare(b.qualifiedName);
1649
+ if (qCmp !== 0) return qCmp;
1650
+ const fCmp = a.filePath.localeCompare(b.filePath);
1651
+ if (fCmp !== 0) return fCmp;
1652
+ const lCmp = a.label.localeCompare(b.label);
1653
+ if (lCmp !== 0) return lCmp;
1654
+ const aId = nodeIdByQFile.get(`${a.qualifiedName}\0${a.filePath}\0${a.label}`) ?? "";
1655
+ const bId = nodeIdByQFile.get(`${b.qualifiedName}\0${b.filePath}\0${b.label}`) ?? "";
1656
+ return aId.localeCompare(bId);
1657
+ });
1658
+ return { ok: true, affected: out };
1659
+ }
1660
+
1661
+ // src/co-change.ts
1662
+ var DEFAULT_CO_CHANGE_CONFIG = {
1663
+ maxCommits: 500,
1664
+ minSupport: 3,
1665
+ minConfidence: 0.3
1666
+ };
1667
+ function mineCoChangeEdges(entries, config = DEFAULT_CO_CHANGE_CONFIG) {
1668
+ const totalChanges = /* @__PURE__ */ new Map();
1669
+ for (const entry of entries) {
1670
+ for (const file of entry.files) {
1671
+ totalChanges.set(file, (totalChanges.get(file) ?? 0) + 1);
1672
+ }
1673
+ }
1674
+ const coChangeCounts = /* @__PURE__ */ new Map();
1675
+ for (const entry of entries) {
1676
+ const files = entry.files;
1677
+ for (let i = 0; i < files.length; i += 1) {
1678
+ for (let j = i + 1; j < files.length; j += 1) {
1679
+ const [a, b] = files[i] < files[j] ? [files[i], files[j]] : [files[j], files[i]];
1680
+ const key = `${a}\0${b}`;
1681
+ const existing = coChangeCounts.get(key);
1682
+ if (existing) {
1683
+ existing.count += 1;
1684
+ } else {
1685
+ coChangeCounts.set(key, { a, b, count: 1 });
1686
+ }
1687
+ }
1688
+ }
1689
+ }
1690
+ const edges = [];
1691
+ for (const { a, b, count } of coChangeCounts.values()) {
1692
+ if (count < config.minSupport) continue;
1693
+ const totalA = totalChanges.get(a) ?? 0;
1694
+ const totalB = totalChanges.get(b) ?? 0;
1695
+ const minTotal = Math.min(totalA, totalB);
1696
+ if (minTotal === 0) continue;
1697
+ const confidence = count / minTotal;
1698
+ if (confidence < config.minConfidence) continue;
1699
+ edges.push({ fileA: a, fileB: b, support: count, confidence });
1700
+ }
1701
+ edges.sort((x, y) => {
1702
+ const cmp = x.fileA.localeCompare(y.fileA);
1703
+ if (cmp !== 0) return cmp;
1704
+ return x.fileB.localeCompare(y.fileB);
1705
+ });
1706
+ return edges;
1707
+ }
1708
+ async function mineAndStoreCoChanges(options) {
1709
+ const { store, git, repoRoot } = options;
1710
+ const config = options.config ?? DEFAULT_CO_CHANGE_CONFIG;
1711
+ const logResult = git.logFiles(repoRoot, config.maxCommits);
1712
+ if (!logResult.ok) return logResult;
1713
+ const edges = mineCoChangeEdges(logResult.entries, config);
1714
+ const persistResult = await store.upsertCoChanges(edges);
1715
+ if (!persistResult.ok) {
1716
+ return { ok: false, code: persistResult.code };
1717
+ }
1718
+ return { ok: true, edges };
1719
+ }
1720
+
1721
+ // src/index-status.ts
1722
+ function getIndexStatus(store, git, repoRoot) {
1723
+ const lastHeadResult = store.readMeta(META_KEY_LAST_HEAD);
1724
+ const lastIndexedHead = lastHeadResult.ok ? lastHeadResult.value : null;
1725
+ const headResult = git.revParseHead(repoRoot);
1726
+ const stats = store.schemaStats();
1727
+ const fileCount = stats.ok ? stats.stats.files : 0;
1728
+ const nodeCount = stats.ok ? stats.stats.nodes : 0;
1729
+ if (!headResult.ok) {
1730
+ return {
1731
+ lastIndexedHead,
1732
+ currentHead: null,
1733
+ dirty: true,
1734
+ mode: "git_unavailable",
1735
+ fileCount,
1736
+ nodeCount
1737
+ };
1738
+ }
1739
+ const currentHead = headResult.head;
1740
+ if (lastIndexedHead === null) {
1741
+ return {
1742
+ lastIndexedHead: null,
1743
+ currentHead,
1744
+ dirty: currentHead !== null,
1745
+ mode: "empty",
1746
+ fileCount,
1747
+ nodeCount
1748
+ };
1749
+ }
1750
+ if (currentHead === null) {
1751
+ return {
1752
+ lastIndexedHead,
1753
+ currentHead: null,
1754
+ dirty: true,
1755
+ mode: "stale",
1756
+ fileCount,
1757
+ nodeCount
1758
+ };
1759
+ }
1760
+ const pendingResult = store.readMeta(META_KEY_PENDING_PARSE_FAILURES);
1761
+ const pendingRaw = pendingResult.ok ? pendingResult.value : null;
1762
+ let hasPendingFailures = false;
1763
+ if (pendingRaw !== null) {
1764
+ try {
1765
+ const parsed = JSON.parse(pendingRaw);
1766
+ hasPendingFailures = Array.isArray(parsed) && parsed.length > 0;
1767
+ } catch {
1768
+ hasPendingFailures = false;
1769
+ }
1770
+ }
1771
+ const dirty = lastIndexedHead !== currentHead || hasPendingFailures;
1772
+ const mode = hasPendingFailures ? "stale" : dirty ? "stale" : "fresh";
1773
+ return {
1774
+ lastIndexedHead,
1775
+ currentHead,
1776
+ dirty,
1777
+ mode,
1778
+ fileCount,
1779
+ nodeCount
1780
+ };
1781
+ }
1782
+
1783
+ // src/git-invoker.ts
1784
+ import { launchProcessSync } from "@remnic/core/runtime/child-process";
1785
+ var GIT_TIMEOUT_MS = 2e3;
1786
+ function runGit(cwd, args) {
1787
+ const result = launchProcessSync("git", [...args], {
1788
+ cwd,
1789
+ encoding: "utf-8",
1790
+ timeout: GIT_TIMEOUT_MS,
1791
+ shell: false
1792
+ });
1793
+ if (result.error) {
1794
+ return { ok: false, code: "git_unavailable" };
1795
+ }
1796
+ return {
1797
+ ok: true,
1798
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
1799
+ exitCode: typeof result.status === "number" ? result.status : 1
1800
+ };
1801
+ }
1802
+ function defaultCodingGitInvoker() {
1803
+ return {
1804
+ revParseHead(cwd) {
1805
+ const r = runGit(cwd, ["rev-parse", "HEAD"]);
1806
+ if (!r.ok) return r;
1807
+ const trimmed = r.stdout.trim();
1808
+ if (r.exitCode === 0 && trimmed.length > 0) {
1809
+ return { ok: true, head: trimmed };
1810
+ }
1811
+ const wt = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
1812
+ if (wt.ok && wt.exitCode === 0 && wt.stdout.trim() === "true") {
1813
+ return { ok: true, head: null };
1814
+ }
1815
+ return { ok: false, code: "git_error" };
1816
+ },
1817
+ isReachable(cwd, ref) {
1818
+ const r = runGit(cwd, ["rev-parse", "--verify", `${ref}^{commit}`]);
1819
+ if (!r.ok) return r;
1820
+ return { ok: true, reachable: r.exitCode === 0 };
1821
+ },
1822
+ diffNameStatus(cwd, range) {
1823
+ const r = runGit(cwd, ["diff", "--name-status", range]);
1824
+ if (!r.ok) return r;
1825
+ if (r.exitCode !== 0) {
1826
+ return { ok: false, code: "git_error" };
1827
+ }
1828
+ return { ok: true, entries: parseNameStatus(r.stdout) };
1829
+ },
1830
+ diffHunks(cwd, paths) {
1831
+ const pathArgs = [];
1832
+ if (paths.length > 0) {
1833
+ pathArgs.push("--");
1834
+ for (const p of paths) pathArgs.push(p);
1835
+ }
1836
+ const r = runGit(cwd, ["diff", "HEAD", "--unified=0", "--no-color", ...pathArgs]);
1837
+ if (!r.ok) return r;
1838
+ if (r.exitCode !== 0) {
1839
+ const fallback = runGit(cwd, ["diff", "--unified=0", "--no-color", ...pathArgs]);
1840
+ if (!fallback.ok) return fallback;
1841
+ if (fallback.exitCode !== 0) {
1842
+ return { ok: false, code: "git_error" };
1843
+ }
1844
+ return { ok: true, hunks: parseHunks(fallback.stdout) };
1845
+ }
1846
+ return { ok: true, hunks: parseHunks(r.stdout) };
1847
+ },
1848
+ logFiles(cwd, limit) {
1849
+ const r = runGit(cwd, [
1850
+ "log",
1851
+ `--format=%H`,
1852
+ "--name-only",
1853
+ `-n`,
1854
+ String(limit)
1855
+ ]);
1856
+ if (!r.ok) return r;
1857
+ if (r.exitCode !== 0) {
1858
+ return { ok: false, code: "git_error" };
1859
+ }
1860
+ return { ok: true, entries: parseLogFiles(r.stdout) };
1861
+ },
1862
+ listTrackedFiles(cwd) {
1863
+ const r = runGit(cwd, ["ls-files"]);
1864
+ if (!r.ok) return r;
1865
+ if (r.exitCode !== 0) {
1866
+ return { ok: false, code: "git_error" };
1867
+ }
1868
+ const paths = r.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
1869
+ return { ok: true, paths };
1870
+ }
1871
+ };
1872
+ }
1873
+ function parseNameStatus(stdout) {
1874
+ const out = [];
1875
+ const lines = stdout.split("\n");
1876
+ for (const line of lines) {
1877
+ if (line.length === 0) continue;
1878
+ const tabIdx = line.indexOf(" ");
1879
+ if (tabIdx < 0) continue;
1880
+ const status = line.slice(0, tabIdx);
1881
+ const rest = line.slice(tabIdx + 1);
1882
+ const secondTab = rest.indexOf(" ");
1883
+ if ((status.startsWith("R") || status.startsWith("C")) && secondTab >= 0) {
1884
+ const oldPath = rest.slice(0, secondTab);
1885
+ const newPath = rest.slice(secondTab + 1);
1886
+ if (oldPath.length > 0 && newPath.length > 0) {
1887
+ out.push({ status, path: newPath, oldPath });
1888
+ }
1889
+ } else {
1890
+ if (rest.length > 0) {
1891
+ out.push({ status, path: rest });
1892
+ }
1893
+ }
1894
+ }
1895
+ return out;
1896
+ }
1897
+ function parseHunks(stdout) {
1898
+ const out = [];
1899
+ let currentPath = null;
1900
+ const lines = stdout.split("\n");
1901
+ for (const line of lines) {
1902
+ if (line.startsWith("+++ ")) {
1903
+ if (line.startsWith("+++ /dev/null")) {
1904
+ currentPath = null;
1905
+ continue;
1906
+ }
1907
+ const raw = line.slice(4);
1908
+ currentPath = raw.startsWith("b/") ? raw.slice(2) : raw;
1909
+ continue;
1910
+ }
1911
+ if (line.startsWith("@@ ")) {
1912
+ const match = line.match(/\+(\d+)(?:,(\d+))?/);
1913
+ if (!match) continue;
1914
+ const startStr = match[1] ?? "";
1915
+ if (startStr.length === 0) continue;
1916
+ const startLine = parseInt(startStr, 10);
1917
+ const countStr = match[2] ?? "";
1918
+ const count = countStr.length > 0 ? parseInt(countStr, 10) : 1;
1919
+ if (!currentPath) continue;
1920
+ out.push({
1921
+ path: currentPath,
1922
+ newRange: {
1923
+ startLine,
1924
+ endLine: startLine + Math.max(count, 1)
1925
+ }
1926
+ });
1927
+ }
1928
+ }
1929
+ return out;
1930
+ }
1931
+ function parseLogFiles(stdout) {
1932
+ const out = [];
1933
+ let currentSha = null;
1934
+ let currentFiles = [];
1935
+ const seenInCommit = /* @__PURE__ */ new Set();
1936
+ const flush = () => {
1937
+ if (currentSha !== null) {
1938
+ out.push({ sha: currentSha, files: currentFiles });
1939
+ }
1940
+ currentSha = null;
1941
+ currentFiles = [];
1942
+ seenInCommit.clear();
1943
+ };
1944
+ for (const line of stdout.split("\n")) {
1945
+ if (/^[0-9a-f]{40}$/.test(line)) {
1946
+ flush();
1947
+ currentSha = line;
1948
+ } else if (line.length > 0 && currentSha !== null) {
1949
+ if (!seenInCommit.has(line)) {
1950
+ seenInCommit.add(line);
1951
+ currentFiles.push(line);
1952
+ }
1953
+ }
1954
+ }
1955
+ flush();
1956
+ return out;
1957
+ }
1958
+
1959
+ // src/lsp/client.ts
1960
+ import { spawn } from "child_process";
1961
+ import path3 from "path";
1962
+ import process2 from "process";
1963
+
1964
+ // src/lsp/framing.ts
1965
+ function encodeLspFrame(message) {
1966
+ const body = JSON.stringify(message);
1967
+ const byteLength = Buffer.byteLength(body, "utf8");
1968
+ return `Content-Length: ${byteLength}\r
1969
+ \r
1970
+ ${body}`;
1971
+ }
1972
+ var HEADER_SEPARATOR = Buffer.from("\r\n\r\n");
1973
+ var HEADER_SEPARATOR_LEN = HEADER_SEPARATOR.length;
1974
+ var LspFrameDecoder = class {
1975
+ buffer = Buffer.alloc(0);
1976
+ /**
1977
+ * Scan offset into {@link buffer}. The header scan resumes here on
1978
+ * the next feed() — never re-scans bytes already confirmed to not
1979
+ * contain the separator (rule 32). Reset to 0 after each consumed
1980
+ * frame because slicing the buffer discards those bytes.
1981
+ */
1982
+ scanOffset = 0;
1983
+ /**
1984
+ * Feed a raw chunk (Buffer or string) from the server's stdout.
1985
+ * Returns all complete messages parsed from the accumulated buffer
1986
+ * since the last call, or the first decode error encountered (the
1987
+ * decoder stops on error — a protocol violation means the stream is
1988
+ * corrupt and further parsing is undefined).
1989
+ */
1990
+ feed(chunk) {
1991
+ const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
1992
+ this.buffer = this.buffer.length === 0 ? Buffer.from(buf) : Buffer.concat([this.buffer, buf]);
1993
+ const messages = [];
1994
+ while (true) {
1995
+ const sepIdx = this.buffer.indexOf(HEADER_SEPARATOR, this.scanOffset);
1996
+ if (sepIdx < 0) {
1997
+ this.scanOffset = Math.max(0, this.buffer.length - (HEADER_SEPARATOR_LEN - 1));
1998
+ return { ok: true, messages };
1999
+ }
2000
+ const headerBlock = this.buffer.subarray(0, sepIdx).toString("utf8");
2001
+ const contentLength = parseContentLength(headerBlock);
2002
+ if (contentLength === null) {
2003
+ return {
2004
+ ok: false,
2005
+ error: {
2006
+ kind: "malformed_header",
2007
+ detail: `header block has no valid Content-Length: ${JSON.stringify(headerBlock)}`
2008
+ }
2009
+ };
2010
+ }
2011
+ const bodyStart = sepIdx + HEADER_SEPARATOR_LEN;
2012
+ const bodyEnd = bodyStart + contentLength;
2013
+ if (this.buffer.length < bodyEnd) {
2014
+ this.scanOffset = Math.max(0, sepIdx - (HEADER_SEPARATOR_LEN - 1));
2015
+ return { ok: true, messages };
2016
+ }
2017
+ const bodyBytes = this.buffer.subarray(bodyStart, bodyEnd);
2018
+ let parsed;
2019
+ try {
2020
+ parsed = JSON.parse(bodyBytes.toString("utf8"));
2021
+ } catch (e) {
2022
+ return {
2023
+ ok: false,
2024
+ error: {
2025
+ kind: "json_parse_error",
2026
+ detail: `body is not valid JSON: ${e instanceof Error ? e.message : String(e)}`
2027
+ }
2028
+ };
2029
+ }
2030
+ messages.push(parsed);
2031
+ this.buffer = this.buffer.subarray(bodyEnd);
2032
+ this.scanOffset = 0;
2033
+ }
2034
+ }
2035
+ /** True if there is un-consumed residual data in the buffer. */
2036
+ get hasResidual() {
2037
+ return this.buffer.length > 0;
2038
+ }
2039
+ /** Reset the decoder to a clean state (test seam). */
2040
+ reset() {
2041
+ this.buffer = Buffer.alloc(0);
2042
+ this.scanOffset = 0;
2043
+ }
2044
+ };
2045
+ function parseContentLength(headerBlock) {
2046
+ const lines = headerBlock.split("\r\n");
2047
+ for (const line of lines) {
2048
+ const match = /^Content-Length:\s*(\d+)\s*$/i.exec(line);
2049
+ if (match) {
2050
+ const n = Number(match[1]);
2051
+ return Number.isFinite(n) && n >= 0 ? n : null;
2052
+ }
2053
+ }
2054
+ return null;
2055
+ }
2056
+
2057
+ // src/lsp/degradation.ts
2058
+ function lspDegradation(code, detail) {
2059
+ return detail !== void 0 ? { backend: "lsp", code, detail } : { backend: "lsp", code };
2060
+ }
2061
+
2062
+ // src/lsp/client.ts
2063
+ var LspClient = class _LspClient {
2064
+ child;
2065
+ decoder = new LspFrameDecoder();
2066
+ rootUri;
2067
+ timeoutMs;
2068
+ nextId = 1;
2069
+ pending = /* @__PURE__ */ new Map();
2070
+ disposed = false;
2071
+ crashed = false;
2072
+ crashCode = null;
2073
+ serverCapabilities = null;
2074
+ constructor(child, rootUri, timeoutMs) {
2075
+ this.child = child;
2076
+ this.rootUri = rootUri;
2077
+ this.timeoutMs = timeoutMs;
2078
+ this.child.stdout?.setEncoding("utf8");
2079
+ this.child.stdout?.on("data", (chunk) => this.onStdoutData(chunk));
2080
+ this.child.stderr?.on("data", () => {
2081
+ });
2082
+ this.child.on("exit", (code, signal) => this.onChildExit(code, signal));
2083
+ this.child.on("error", (err) => this.onChildError(err));
2084
+ }
2085
+ /**
2086
+ * Spawn the server and perform the initialize handshake. Returns a
2087
+ * tagged result — `{ ok: true, client }` on success, or a degradation
2088
+ * on failure (server_missing, handshake_timeout, handshake_error).
2089
+ */
2090
+ static async connect(options) {
2091
+ let child;
2092
+ const spawnFn = options.spawnFn ?? spawn;
2093
+ try {
2094
+ child = spawnFn(options.launchSpec.command, [...options.launchSpec.args], {
2095
+ stdio: ["pipe", "pipe", "pipe"],
2096
+ shell: false,
2097
+ // The server inherits our cwd so relative rootUri paths resolve.
2098
+ cwd: process2.cwd()
2099
+ });
2100
+ } catch {
2101
+ return {
2102
+ ok: false,
2103
+ degradation: lspDegradation(
2104
+ "server_missing",
2105
+ `failed to spawn ${options.launchSpec.command}`
2106
+ )
2107
+ };
2108
+ }
2109
+ if (child.exitCode !== null && child.exitCode !== void 0) {
2110
+ return {
2111
+ ok: false,
2112
+ degradation: lspDegradation("server_missing")
2113
+ };
2114
+ }
2115
+ const client = new _LspClient(child, options.rootUri, options.timeoutMs);
2116
+ if (client.crashed) {
2117
+ return {
2118
+ ok: false,
2119
+ degradation: lspDegradation(client.crashCode ?? "server_crashed")
2120
+ };
2121
+ }
2122
+ const initParams = {
2123
+ processId: process2.pid,
2124
+ rootUri: options.rootUri,
2125
+ capabilities: {}
2126
+ };
2127
+ const initResult = await client.request("initialize", initParams);
2128
+ if (!initResult.ok) {
2129
+ await client.dispose();
2130
+ const code = initResult.degradation.code === "request_timeout" ? "handshake_timeout" : initResult.degradation.code;
2131
+ return {
2132
+ ok: false,
2133
+ degradation: lspDegradation(code, initResult.degradation.detail)
2134
+ };
2135
+ }
2136
+ if (initResult.value === null || typeof initResult.value !== "object") {
2137
+ await client.dispose();
2138
+ return {
2139
+ ok: false,
2140
+ degradation: lspDegradation("protocol_error", "initialize response missing or invalid")
2141
+ };
2142
+ }
2143
+ const initResponse = initResult.value;
2144
+ client.serverCapabilities = initResponse.capabilities;
2145
+ client.notify("initialized", {});
2146
+ return { ok: true, client };
2147
+ }
2148
+ /**
2149
+ * Send `textDocument/didOpen` — notifies the server about an open
2150
+ * document with its full content. No response expected.
2151
+ */
2152
+ didOpen(item) {
2153
+ this.notify("textDocument/didOpen", { textDocument: item });
2154
+ }
2155
+ /**
2156
+ * Send `textDocument/definition` for a position in a document. Returns
2157
+ * the definition locations (may be empty, a single location, or an
2158
+ * array). Degrades on timeout/error/crash — never throws.
2159
+ */
2160
+ async definition(params) {
2161
+ if (this.disposed || this.crashed) {
2162
+ return {
2163
+ ok: false,
2164
+ degradation: lspDegradation("server_crashed")
2165
+ };
2166
+ }
2167
+ const result = await this.request("textDocument/definition", params);
2168
+ if (!result.ok) {
2169
+ return result;
2170
+ }
2171
+ const value = result.value;
2172
+ let locations;
2173
+ if (value === null || value === void 0) {
2174
+ locations = [];
2175
+ } else if (Array.isArray(value)) {
2176
+ locations = value;
2177
+ } else {
2178
+ locations = [value];
2179
+ }
2180
+ return { ok: true, locations };
2181
+ }
2182
+ /**
2183
+ * Send `shutdown`, then `exit`, then SIGKILL if the process lingers.
2184
+ * Idempotent — safe to call multiple times. After dispose, no child
2185
+ * process remains (tested — zombie cleanup).
2186
+ */
2187
+ async dispose() {
2188
+ if (this.disposed) return;
2189
+ this.disposed = true;
2190
+ for (const [id, entry] of this.pending) {
2191
+ clearTimeout(entry.timer);
2192
+ entry.reject(lspDegradation("server_crashed"));
2193
+ this.pending.delete(id);
2194
+ }
2195
+ if (!this.crashed && this.child.stdin && !this.child.stdin.destroyed) {
2196
+ try {
2197
+ const promise = new Promise((resolve) => {
2198
+ this.child.stdin?.write(encodeLspFrame({ jsonrpc: "2.0", id: 0, method: "shutdown" }));
2199
+ this.child.stdin?.write(encodeLspFrame({ jsonrpc: "2.0", method: "exit" }));
2200
+ const timer = setTimeout(resolve, 500);
2201
+ this.child.on("exit", () => {
2202
+ clearTimeout(timer);
2203
+ resolve();
2204
+ });
2205
+ });
2206
+ await promise;
2207
+ } catch {
2208
+ }
2209
+ }
2210
+ this.hardKill();
2211
+ }
2212
+ /** Returns the pid of the child process (for zombie-cleanup tests). */
2213
+ get pid() {
2214
+ return this.child.pid;
2215
+ }
2216
+ /** True if the server reported definitionProvider capability. */
2217
+ get supportsDefinition() {
2218
+ const caps = this.serverCapabilities;
2219
+ return caps !== null && Boolean(caps.definitionProvider);
2220
+ }
2221
+ // ──────────────────────────────────────────────────────────────────────
2222
+ // Internal — JSON-RPC request/notification machinery
2223
+ // ──────────────────────────────────────────────────────────────────────
2224
+ /**
2225
+ * Send a request and await its response. Returns the `result` field
2226
+ * on success, or a degradation on timeout/error/crash/protocol-error.
2227
+ */
2228
+ request(method, params) {
2229
+ if (this.disposed || this.crashed) {
2230
+ return Promise.resolve({
2231
+ ok: false,
2232
+ degradation: lspDegradation("server_crashed")
2233
+ });
2234
+ }
2235
+ const id = this.nextId++;
2236
+ const message = { jsonrpc: "2.0", id, method, params };
2237
+ const promise = new Promise((resolve, reject) => {
2238
+ const timer = setTimeout(() => {
2239
+ const entry = this.pending.get(id);
2240
+ if (entry) {
2241
+ this.pending.delete(id);
2242
+ entry.reject(lspDegradation("request_timeout", `method=${method}`));
2243
+ }
2244
+ }, this.timeoutMs);
2245
+ this.pending.set(id, { resolve, reject, timer });
2246
+ try {
2247
+ const frame = encodeLspFrame(message);
2248
+ if (!this.child.stdin || this.child.stdin.destroyed) {
2249
+ clearTimeout(timer);
2250
+ this.pending.delete(id);
2251
+ reject(lspDegradation("server_crashed"));
2252
+ return;
2253
+ }
2254
+ this.child.stdin.write(frame);
2255
+ } catch {
2256
+ clearTimeout(timer);
2257
+ this.pending.delete(id);
2258
+ reject(lspDegradation("server_crashed"));
2259
+ }
2260
+ });
2261
+ return promise.then(
2262
+ (value) => ({ ok: true, value }),
2263
+ (degradation) => ({ ok: false, degradation })
2264
+ );
2265
+ }
2266
+ /**
2267
+ * Send a notification (no response expected). Best-effort — if the
2268
+ * write fails, the next request will surface the crash.
2269
+ */
2270
+ notify(method, params) {
2271
+ if (this.disposed || this.crashed) return;
2272
+ try {
2273
+ const message = { jsonrpc: "2.0", method, params };
2274
+ this.child.stdin?.write(encodeLspFrame(message));
2275
+ } catch {
2276
+ }
2277
+ }
2278
+ /**
2279
+ * Dispatch a decoded JSON-RPC message. Correlates responses to pending
2280
+ * requests by id; ignores server-initiated notifications (we don't
2281
+ * need them for the resolution pass).
2282
+ */
2283
+ /**
2284
+ * Dispatch a decoded JSON-RPC message. Correlates responses to pending
2285
+ * requests by id; ignores server-initiated notifications.
2286
+ */
2287
+ dispatchMessage(msg) {
2288
+ if (typeof msg !== "object" || msg === null) return;
2289
+ const rpc = msg;
2290
+ if (rpc.id === void 0 || rpc.result === void 0 && rpc.error === void 0) {
2291
+ return;
2292
+ }
2293
+ const id = typeof rpc.id === "number" ? rpc.id : Number(rpc.id);
2294
+ const entry = this.pending.get(id);
2295
+ if (!entry) return;
2296
+ clearTimeout(entry.timer);
2297
+ this.pending.delete(id);
2298
+ if (rpc.error !== void 0) {
2299
+ entry.reject(lspDegradation("request_error", "server returned an error response"));
2300
+ } else {
2301
+ entry.resolve(rpc.result);
2302
+ }
2303
+ }
2304
+ /**
2305
+ * stdout data handler — feed the decoder, dispatch complete messages,
2306
+ * detect protocol errors.
2307
+ */
2308
+ onStdoutData(chunk) {
2309
+ const result = this.decoder.feed(chunk);
2310
+ if (!result.ok) {
2311
+ this.handleProtocolError(result.error.detail);
2312
+ return;
2313
+ }
2314
+ for (const msg of result.messages) {
2315
+ this.dispatchMessage(msg);
2316
+ }
2317
+ }
2318
+ /**
2319
+ * Handle an unexpected child exit. All pending requests are rejected
2320
+ * with server_crashed.
2321
+ */
2322
+ onChildExit(code, signal) {
2323
+ if (this.disposed) return;
2324
+ this.crashed = true;
2325
+ this.crashCode = "server_crashed";
2326
+ const detail = code !== null ? `server exited with code ${code}` : `server killed by ${signal}`;
2327
+ for (const [id, entry] of this.pending) {
2328
+ clearTimeout(entry.timer);
2329
+ entry.reject(lspDegradation("server_crashed", detail));
2330
+ this.pending.delete(id);
2331
+ }
2332
+ }
2333
+ /**
2334
+ * Handle a spawn error (ENOENT etc). Marks the server as missing.
2335
+ */
2336
+ onChildError(err) {
2337
+ this.crashed = true;
2338
+ const isENOENT = err.message.includes("ENOENT");
2339
+ this.crashCode = isENOENT ? "server_missing" : "server_crashed";
2340
+ const detail = isENOENT ? void 0 : "spawn error";
2341
+ for (const [id, entry] of this.pending) {
2342
+ clearTimeout(entry.timer);
2343
+ entry.reject(lspDegradation(this.crashCode, detail));
2344
+ this.pending.delete(id);
2345
+ }
2346
+ }
2347
+ /**
2348
+ * Protocol error — the stream produced a malformed frame. Reject all
2349
+ * pending and mark disposed so no further requests can be sent.
2350
+ */
2351
+ handleProtocolError(detail) {
2352
+ this.crashed = true;
2353
+ for (const [id, entry] of this.pending) {
2354
+ clearTimeout(entry.timer);
2355
+ entry.reject(lspDegradation("protocol_error", detail));
2356
+ this.pending.delete(id);
2357
+ }
2358
+ }
2359
+ /**
2360
+ * Hard-kill the child process: SIGKILL. Called by dispose() as a
2361
+ * final cleanup guarantee. Also called if the graceful shutdown path
2362
+ * fails. Uses `kill` which is a no-op if the process already exited.
2363
+ */
2364
+ hardKill() {
2365
+ try {
2366
+ if (this.child.pid !== void 0 && !this.child.killed) {
2367
+ this.child.kill("SIGKILL");
2368
+ }
2369
+ } catch {
2370
+ }
2371
+ }
2372
+ };
2373
+ function pathToUri(filePath) {
2374
+ const abs = path3.isAbsolute(filePath) ? filePath : path3.resolve(filePath);
2375
+ const normalized = abs.replace(/\\/g, "/");
2376
+ const withSlash = /^[A-Za-z]:/.test(normalized) ? `/${normalized}` : normalized;
2377
+ return `file://${withSlash}`;
2378
+ }
2379
+ function uriToPath(uri) {
2380
+ if (uri.startsWith("file://")) {
2381
+ let rest = uri.slice("file://".length);
2382
+ try {
2383
+ rest = decodeURIComponent(rest);
2384
+ } catch {
2385
+ }
2386
+ if (/^\/[A-Za-z]:/.test(rest)) {
2387
+ return rest.slice(1).replace(/\//g, path3.sep);
2388
+ }
2389
+ return rest.replace(/\//g, path3.sep);
2390
+ }
2391
+ return uri;
2392
+ }
2393
+
2394
+ // src/lsp/config.ts
2395
+ import process3 from "process";
2396
+ var DEFAULT_LSP_TIMEOUT_MS = 3e3;
2397
+ var DEFAULT_LSP_MAX_REQUESTS_PER_RUN = 500;
2398
+ var DEFAULT_LSP_CONFIG = {
2399
+ enabled: false,
2400
+ servers: {},
2401
+ timeoutMs: DEFAULT_LSP_TIMEOUT_MS,
2402
+ maxRequestsPerRun: DEFAULT_LSP_MAX_REQUESTS_PER_RUN
2403
+ };
2404
+ function parseEnabledFlag(v) {
2405
+ if (typeof v === "boolean") return v;
2406
+ if (typeof v === "string") {
2407
+ const lower = v.trim().toLowerCase();
2408
+ return lower !== "false" && lower !== "0" && lower !== "no" && lower !== "";
2409
+ }
2410
+ return Boolean(v);
2411
+ }
2412
+ function parseLspConfig(raw, knownLanguages) {
2413
+ if (raw === null || raw === void 0) {
2414
+ return { ok: true, config: DEFAULT_LSP_CONFIG };
2415
+ }
2416
+ if (typeof raw !== "object" || Array.isArray(raw)) {
2417
+ return {
2418
+ ok: false,
2419
+ degradation: {
2420
+ backend: "lsp",
2421
+ code: "protocol_error",
2422
+ detail: "codingGraph.lsp must be an object"
2423
+ }
2424
+ };
2425
+ }
2426
+ const obj = raw;
2427
+ const knownSet = new Set(knownLanguages);
2428
+ const enabled = obj.enabled === void 0 ? false : parseEnabledFlag(obj.enabled);
2429
+ const timeoutMs = obj.timeoutMs === void 0 ? DEFAULT_LSP_TIMEOUT_MS : Number(obj.timeoutMs);
2430
+ const maxRequestsPerRun = obj.maxRequestsPerRun === void 0 ? DEFAULT_LSP_MAX_REQUESTS_PER_RUN : Number(obj.maxRequestsPerRun);
2431
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
2432
+ return {
2433
+ ok: false,
2434
+ degradation: {
2435
+ backend: "lsp",
2436
+ code: "protocol_error",
2437
+ detail: `lsp.timeoutMs must be a non-negative number, got ${JSON.stringify(obj.timeoutMs)}`
2438
+ }
2439
+ };
2440
+ }
2441
+ if (!Number.isFinite(maxRequestsPerRun) || maxRequestsPerRun < 0) {
2442
+ return {
2443
+ ok: false,
2444
+ degradation: {
2445
+ backend: "lsp",
2446
+ code: "protocol_error",
2447
+ detail: `lsp.maxRequestsPerRun must be a non-negative number, got ${JSON.stringify(obj.maxRequestsPerRun)}`
2448
+ }
2449
+ };
2450
+ }
2451
+ const servers = {};
2452
+ if (obj.servers !== void 0 && obj.servers !== null) {
2453
+ if (typeof obj.servers !== "object" || Array.isArray(obj.servers)) {
2454
+ return {
2455
+ ok: false,
2456
+ degradation: {
2457
+ backend: "lsp",
2458
+ code: "protocol_error",
2459
+ detail: "lsp.servers must be an object"
2460
+ }
2461
+ };
2462
+ }
2463
+ for (const [lang, spec] of Object.entries(obj.servers)) {
2464
+ if (!knownSet.has(lang)) {
2465
+ return {
2466
+ ok: false,
2467
+ degradation: {
2468
+ backend: "lsp",
2469
+ code: "unknown_language",
2470
+ detail: `unknown language "${lang}" in lsp.servers; supported: ${knownLanguages.join(", ")}`
2471
+ }
2472
+ };
2473
+ }
2474
+ if (typeof spec !== "object" || spec === null || Array.isArray(spec)) {
2475
+ return {
2476
+ ok: false,
2477
+ degradation: {
2478
+ backend: "lsp",
2479
+ code: "protocol_error",
2480
+ detail: `lsp.servers.${lang} must be an object`
2481
+ }
2482
+ };
2483
+ }
2484
+ const s = spec;
2485
+ if (typeof s.command !== "string" || s.command.length === 0) {
2486
+ return {
2487
+ ok: false,
2488
+ degradation: {
2489
+ backend: "lsp",
2490
+ code: "protocol_error",
2491
+ detail: `lsp.servers.${lang}.command must be a non-empty string`
2492
+ }
2493
+ };
2494
+ }
2495
+ if (!Array.isArray(s.args) || s.args.some((a) => typeof a !== "string")) {
2496
+ return {
2497
+ ok: false,
2498
+ degradation: {
2499
+ backend: "lsp",
2500
+ code: "protocol_error",
2501
+ detail: `lsp.servers.${lang}.args must be an array of strings`
2502
+ }
2503
+ };
2504
+ }
2505
+ servers[lang] = {
2506
+ command: s.command,
2507
+ args: [...s.args]
2508
+ };
2509
+ }
2510
+ }
2511
+ return { ok: true, config: { enabled, servers, timeoutMs, maxRequestsPerRun } };
2512
+ }
2513
+ function readLspEnabledEnv() {
2514
+ return process3.env.REMNIC_CODING_GRAPH_LSP_ENABLED ?? process3.env.ENGRAM_CODING_GRAPH_LSP_ENABLED ?? null;
2515
+ }
2516
+
2517
+ // src/lsp/resolution.ts
2518
+ import path4 from "path";
2519
+
2520
+ // src/lsp/byte-position.ts
2521
+ function utf8ByteLength(code, nextCode) {
2522
+ if (code >= 55296 && code <= 56319 && nextCode >= 56320 && nextCode <= 57343) {
2523
+ return 4;
2524
+ }
2525
+ if (code < 128) return 1;
2526
+ if (code < 2048) return 2;
2527
+ return 3;
2528
+ }
2529
+ function isHighSurrogate(code, nextCode) {
2530
+ return code >= 55296 && code <= 56319 && nextCode >= 56320 && nextCode <= 57343;
2531
+ }
2532
+ function buildLineOffsetMap(content) {
2533
+ const text = typeof content === "string" ? content : content.toString("utf8");
2534
+ const lineStarts = [0];
2535
+ let byteOffset = 0;
2536
+ for (let i = 0; i < text.length; i++) {
2537
+ const code = text.charCodeAt(i);
2538
+ const nextCode = i + 1 < text.length ? text.charCodeAt(i + 1) : 0;
2539
+ if (code === 13 && nextCode === 10) {
2540
+ byteOffset += 2;
2541
+ lineStarts.push(byteOffset);
2542
+ i++;
2543
+ } else if (code === 10 || code === 13) {
2544
+ byteOffset += 1;
2545
+ lineStarts.push(byteOffset);
2546
+ } else {
2547
+ byteOffset += utf8ByteLength(code, nextCode);
2548
+ if (isHighSurrogate(code, nextCode)) i++;
2549
+ }
2550
+ }
2551
+ return { lineStarts };
2552
+ }
2553
+ function byteOffsetToPosition(content, byteOffset, map) {
2554
+ const lineIdx = binarySearchLine(map.lineStarts, byteOffset);
2555
+ const lineByteStart = map.lineStarts[lineIdx];
2556
+ let strIdx = 0;
2557
+ let byteAccum = 0;
2558
+ for (strIdx = 0; strIdx < content.length; strIdx++) {
2559
+ if (byteAccum >= lineByteStart) break;
2560
+ const code = content.charCodeAt(strIdx);
2561
+ const nextCode = strIdx + 1 < content.length ? content.charCodeAt(strIdx + 1) : 0;
2562
+ byteAccum += utf8ByteLength(code, nextCode);
2563
+ if (isHighSurrogate(code, nextCode)) strIdx++;
2564
+ }
2565
+ let charCount = 0;
2566
+ let currentByte = lineByteStart;
2567
+ for (let i = strIdx; i < content.length; i++) {
2568
+ if (currentByte >= byteOffset) break;
2569
+ const code = content.charCodeAt(i);
2570
+ const nextCode = i + 1 < content.length ? content.charCodeAt(i + 1) : 0;
2571
+ currentByte += utf8ByteLength(code, nextCode);
2572
+ if (isHighSurrogate(code, nextCode)) {
2573
+ charCount += 2;
2574
+ i++;
2575
+ } else {
2576
+ charCount++;
2577
+ }
2578
+ }
2579
+ return { line: lineIdx, character: charCount };
2580
+ }
2581
+ function positionToByteOffset(content, position, map) {
2582
+ const lineIdx = Math.min(position.line, map.lineStarts.length - 1);
2583
+ const lineByteStart = map.lineStarts[lineIdx];
2584
+ let strIdx = 0;
2585
+ let byteAccum = 0;
2586
+ for (strIdx = 0; strIdx < content.length; strIdx++) {
2587
+ if (byteAccum >= lineByteStart) break;
2588
+ const code = content.charCodeAt(strIdx);
2589
+ const nextCode = strIdx + 1 < content.length ? content.charCodeAt(strIdx + 1) : 0;
2590
+ byteAccum += utf8ByteLength(code, nextCode);
2591
+ if (isHighSurrogate(code, nextCode)) strIdx++;
2592
+ }
2593
+ let charCount = 0;
2594
+ let byteOffset = lineByteStart;
2595
+ for (let i = strIdx; i < content.length && charCount < position.character; i++) {
2596
+ const code = content.charCodeAt(i);
2597
+ const nextCode = i + 1 < content.length ? content.charCodeAt(i + 1) : 0;
2598
+ byteOffset += utf8ByteLength(code, nextCode);
2599
+ if (isHighSurrogate(code, nextCode)) {
2600
+ charCount += 2;
2601
+ i++;
2602
+ } else {
2603
+ charCount++;
2604
+ }
2605
+ }
2606
+ return byteOffset;
2607
+ }
2608
+ function binarySearchLine(lineStarts, byteOffset) {
2609
+ let lo = 0;
2610
+ let hi = lineStarts.length - 1;
2611
+ while (lo < hi) {
2612
+ const mid = lo + hi + 1 >> 1;
2613
+ if (lineStarts[mid] <= byteOffset) {
2614
+ lo = mid;
2615
+ } else {
2616
+ hi = mid - 1;
2617
+ }
2618
+ }
2619
+ return lo;
2620
+ }
2621
+
2622
+ // src/lsp/resolution.ts
2623
+ function planLspUpgrades(callSites, budget) {
2624
+ const sorted = [...callSites].sort(
2625
+ (a, b) => a.filePath.localeCompare(b.filePath) || a.calleeByteOffset - b.calleeByteOffset
2626
+ );
2627
+ const max = Math.max(0, budget.maxRequests);
2628
+ const planned = sorted.slice(0, max);
2629
+ const exhausted = sorted.length - planned.length;
2630
+ const maps = /* @__PURE__ */ new Map();
2631
+ const requests = planned.map((cs) => {
2632
+ let map = maps.get(cs.filePath);
2633
+ if (!map) {
2634
+ map = buildLineOffsetMap(cs.content);
2635
+ maps.set(cs.filePath, map);
2636
+ }
2637
+ const position = byteOffsetToPosition(cs.content, cs.calleeByteOffset, map);
2638
+ return {
2639
+ filePath: cs.filePath,
2640
+ language: cs.language,
2641
+ content: cs.content,
2642
+ calleeName: cs.calleeName,
2643
+ srcQualifiedName: cs.srcQualifiedName,
2644
+ position
2645
+ };
2646
+ });
2647
+ return { requests, budgetExhausted: exhausted };
2648
+ }
2649
+ var LANGUAGE_ID_MAP = {
2650
+ tsx: "typescriptreact",
2651
+ bash: "shellscript"
2652
+ };
2653
+ async function executeLspResolution(requests, options) {
2654
+ const { client, nodeLocator, applyUpgrades } = options;
2655
+ const byFile = /* @__PURE__ */ new Map();
2656
+ for (const req of requests) {
2657
+ let batch = byFile.get(req.filePath);
2658
+ if (!batch) {
2659
+ batch = [];
2660
+ byFile.set(req.filePath, batch);
2661
+ }
2662
+ batch.push(req);
2663
+ }
2664
+ let upgraded = 0;
2665
+ let unresolved = 0;
2666
+ let degradation;
2667
+ for (const [filePath, batchReqs] of byFile) {
2668
+ const upgrades = [];
2669
+ let batchFailed = false;
2670
+ const firstReq = batchReqs[0];
2671
+ client.didOpen({
2672
+ uri: filePathToUri(filePath, options.workspaceRoot),
2673
+ languageId: LANGUAGE_ID_MAP[firstReq.language] ?? firstReq.language,
2674
+ version: 1,
2675
+ text: firstReq.content
2676
+ });
2677
+ for (let i = 0; i < batchReqs.length; i++) {
2678
+ const req = batchReqs[i];
2679
+ if (degradation) {
2680
+ unresolved += batchReqs.length - i;
2681
+ break;
2682
+ }
2683
+ const defResult = await client.definition({
2684
+ textDocument: { uri: filePathToUri(req.filePath, options.workspaceRoot) },
2685
+ position: req.position
2686
+ });
2687
+ if (!defResult.ok) {
2688
+ if (defResult.degradation.code === "server_crashed" || defResult.degradation.code === "protocol_error") {
2689
+ degradation = defResult.degradation;
2690
+ unresolved += batchReqs.length - i;
2691
+ batchFailed = true;
2692
+ break;
2693
+ }
2694
+ unresolved++;
2695
+ continue;
2696
+ }
2697
+ const dstQName = mapLocationToNode(
2698
+ defResult.locations,
2699
+ {
2700
+ callerFilePath: req.filePath,
2701
+ callerContent: req.content,
2702
+ workspaceRoot: options.workspaceRoot,
2703
+ resolveContent: options.resolveContent
2704
+ },
2705
+ nodeLocator
2706
+ );
2707
+ if (dstQName === null) {
2708
+ unresolved++;
2709
+ continue;
2710
+ }
2711
+ upgrades.push({
2712
+ srcQualifiedName: req.srcQualifiedName,
2713
+ dstQualifiedName: dstQName,
2714
+ type: "CALLS",
2715
+ confidence: 0.9,
2716
+ provenance: "lsp"
2717
+ });
2718
+ }
2719
+ if (batchFailed) {
2720
+ unresolved += upgrades.length;
2721
+ break;
2722
+ }
2723
+ if (upgrades.length > 0) {
2724
+ try {
2725
+ await applyUpgrades(upgrades);
2726
+ upgraded += upgrades.length;
2727
+ } catch {
2728
+ unresolved += upgrades.length;
2729
+ }
2730
+ }
2731
+ }
2732
+ return {
2733
+ upgraded,
2734
+ unresolved,
2735
+ budgetExhausted: 0,
2736
+ // set by the caller from the planner result
2737
+ degradation
2738
+ };
2739
+ }
2740
+ function mapLocationToNode(locations, context, nodeLocator) {
2741
+ for (const loc of locations) {
2742
+ const filePath = normalizeLocationPath(loc.uri, context.workspaceRoot);
2743
+ let content;
2744
+ if (filePath === context.callerFilePath) {
2745
+ content = context.callerContent;
2746
+ } else if (context.resolveContent) {
2747
+ const resolved = context.resolveContent(filePath);
2748
+ if (resolved === null) continue;
2749
+ content = resolved;
2750
+ } else {
2751
+ content = context.callerContent;
2752
+ }
2753
+ const map = buildLineOffsetMap(content);
2754
+ const startByte = positionToByteOffset(content, loc.range.start, map);
2755
+ const qName = nodeLocator(filePath, startByte);
2756
+ if (qName !== null) return qName;
2757
+ }
2758
+ return null;
2759
+ }
2760
+ function filePathToUri(filePath, workspaceRoot) {
2761
+ const isAbsolute = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath);
2762
+ if (isAbsolute) {
2763
+ return `file://${filePath.replace(/\\/g, "/")}`;
2764
+ }
2765
+ if (workspaceRoot) {
2766
+ const abs = path4.join(workspaceRoot, filePath).replace(/\\/g, "/");
2767
+ return `file://${abs}`;
2768
+ }
2769
+ return `file:///${filePath.replace(/\\/g, "/")}`;
2770
+ }
2771
+ function normalizeLocationPath(uri, workspaceRoot) {
2772
+ const absPath = uriToPath(uri);
2773
+ if (workspaceRoot) {
2774
+ const root = path4.resolve(workspaceRoot);
2775
+ const rel = path4.relative(root, absPath);
2776
+ if (!rel.startsWith("..") && rel !== "") {
2777
+ return rel.replace(/\\/g, "/");
2778
+ }
2779
+ }
2780
+ return absPath;
2781
+ }
2782
+
2783
+ // src/lsp/status.ts
2784
+ function getLspStatus(input) {
2785
+ return input.languages.map((lang) => {
2786
+ const probed = input.probeResults.get(lang) ?? false;
2787
+ const degradationCode = input.degradations.get(lang);
2788
+ const requestsUsed = input.requestCounts.get(lang) ?? 0;
2789
+ return {
2790
+ language: lang,
2791
+ enabled: input.config.enabled,
2792
+ probed,
2793
+ degraded: degradationCode !== void 0,
2794
+ degradationCode,
2795
+ requestsUsed
2796
+ };
2797
+ });
2798
+ }
2799
+ function formatLspStatusLine(entry) {
2800
+ if (!entry.enabled) {
2801
+ return `${entry.language}: lsp [disabled]`;
2802
+ }
2803
+ const parts = [];
2804
+ if (entry.probed) {
2805
+ parts.push("probed");
2806
+ } else {
2807
+ parts.push("not_probed");
2808
+ }
2809
+ if (entry.degraded && entry.degradationCode) {
2810
+ parts[parts.length - 1] = `degraded:${entry.degradationCode}`;
2811
+ }
2812
+ const stateStr = parts.join(" ");
2813
+ return `${entry.language}: lsp [${stateStr}] ${entry.requestsUsed} requests`;
2814
+ }
2815
+ function resolutionResultToStatusMaps(language, result, degradations, requestCounts) {
2816
+ requestCounts.set(language, result.upgraded + result.unresolved);
2817
+ if (result.degradation) {
2818
+ degradations.set(language, result.degradation.code);
2819
+ }
2820
+ }
2821
+
2822
+ // src/semantic/config.ts
2823
+ var DEFAULT_SIMILAR_TO_THRESHOLD = 0.92;
2824
+ var DEFAULT_MAX_SYMBOLS_PER_RUN = 0;
2825
+ var MINHASH_ONLY_CONFIDENCE = 0.5;
2826
+ var SIMILAR_TO_EDGE_TYPE = "SIMILAR_TO";
2827
+ var SEMANTIC_PROVENANCE = "semantic";
2828
+ var DEFAULT_CANONICAL_BODY_LINES = 16;
2829
+ var MINHASH_SHINGLE_WIDTH = 2;
2830
+ var MINHASH_NUM_PERMUTATIONS = 128;
2831
+ var LSH_NUM_BANDS = 32;
2832
+ var LSH_ROWS_PER_BAND = MINHASH_NUM_PERMUTATIONS / LSH_NUM_BANDS;
2833
+ var LSH_CANDIDATE_JACCARD_FLOOR = Math.pow(1 / LSH_NUM_BANDS, 1 / LSH_ROWS_PER_BAND);
2834
+ var ENV_ENABLED = ["REMNIC_CODING_GRAPH_SEMANTIC_ENABLED", "ENGRAM_CODING_GRAPH_SEMANTIC_ENABLED"];
2835
+ var ENV_THRESHOLD = ["REMNIC_CODING_GRAPH_SEMANTIC_SIMILAR_TO_THRESHOLD", "ENGRAM_CODING_GRAPH_SEMANTIC_SIMILAR_TO_THRESHOLD"];
2836
+ var ENV_MAX_SYMBOLS = ["REMNIC_CODING_GRAPH_SEMANTIC_MAX_SYMBOLS_PER_RUN", "ENGRAM_CODING_GRAPH_SEMANTIC_MAX_SYMBOLS_PER_RUN"];
2837
+ function coerceHostBool(value) {
2838
+ if (value === void 0 || value === null) return void 0;
2839
+ if (typeof value === "boolean") return value;
2840
+ if (typeof value === "string") {
2841
+ const v = value.trim().toLowerCase();
2842
+ if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
2843
+ return false;
2844
+ }
2845
+ if (typeof value === "number") return value !== 0;
2846
+ return Boolean(value);
2847
+ }
2848
+ function coerceHostNumber(value) {
2849
+ if (value === void 0 || value === null) return void 0;
2850
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
2851
+ if (typeof value === "string") {
2852
+ const n = Number(value.trim());
2853
+ return Number.isFinite(n) ? n : void 0;
2854
+ }
2855
+ return void 0;
2856
+ }
2857
+ function resolveBoolEnv(names, fallback, env) {
2858
+ for (const name of names) {
2859
+ const raw = env[name];
2860
+ if (raw === void 0) continue;
2861
+ const v = raw.trim().toLowerCase();
2862
+ if (v === "true" || v === "1") return true;
2863
+ if (v === "false" || v === "0") return false;
2864
+ }
2865
+ return fallback;
2866
+ }
2867
+ function resolveNumberEnv(names, fallback, env) {
2868
+ for (const name of names) {
2869
+ const raw = env[name];
2870
+ if (raw === void 0) continue;
2871
+ const n = Number(raw);
2872
+ if (Number.isFinite(n) && n >= 0) return n;
2873
+ }
2874
+ return fallback;
2875
+ }
2876
+ function resolveSemanticConfig(host, env = process.env) {
2877
+ const enabled = coerceHostBool(host?.enabled) ?? resolveBoolEnv(ENV_ENABLED, false, env);
2878
+ const similarToThreshold = coerceHostNumber(host?.similarToThreshold) ?? resolveNumberEnv(ENV_THRESHOLD, DEFAULT_SIMILAR_TO_THRESHOLD, env);
2879
+ const maxSymbolsPerRun = coerceHostNumber(host?.maxSymbolsPerRun) ?? resolveNumberEnv(ENV_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS_PER_RUN, env);
2880
+ const canonicalBodyLines = coerceHostNumber(host?.canonicalBodyLines) ?? DEFAULT_CANONICAL_BODY_LINES;
2881
+ return {
2882
+ enabled,
2883
+ // Clamp threshold into [0,1] — a malformed env must not produce an
2884
+ // out-of-range confidence gate.
2885
+ similarToThreshold: Math.min(1, Math.max(0, similarToThreshold)),
2886
+ maxSymbolsPerRun: Math.max(0, Math.floor(maxSymbolsPerRun)),
2887
+ // canonicalBodyLines: a negative/zero value must NOT clamp to 0 because
2888
+ // extractBodyText treats <= 0 as unlimited — sending full symbol bodies to
2889
+ // the embedding provider instead of the bounded excerpt (defeats the
2890
+ // privacy/cost cap). Fall back to the default instead (#1680).
2891
+ canonicalBodyLines: canonicalBodyLines >= 1 ? Math.floor(canonicalBodyLines) : DEFAULT_CANONICAL_BODY_LINES
2892
+ };
2893
+ }
2894
+
2895
+ // src/semantic/canonical-text.ts
2896
+ import { createHash as createHash2 } from "crypto";
2897
+ function collapseWhitespace(text) {
2898
+ return text.replace(/([{}()<>\[\],;:?!=+\-*/%&|^~])/g, " $1 ").replace(/\s+/g, " ").trim();
2899
+ }
2900
+ function splitSignatureBody(normalized) {
2901
+ const braceIdx = normalized.indexOf("{");
2902
+ if (braceIdx >= 0) {
2903
+ const signature = normalized.slice(0, braceIdx + 1).trim();
2904
+ const body = normalized.slice(braceIdx + 1).trim();
2905
+ return { signature, body };
2906
+ }
2907
+ const arrowIdx = normalized.indexOf("=>");
2908
+ const arrowNormIdx = normalized.indexOf("= >");
2909
+ if (arrowIdx >= 0) {
2910
+ const signature = normalized.slice(0, arrowIdx + 2).trim();
2911
+ const body = normalized.slice(arrowIdx + 2).trim();
2912
+ return { signature, body };
2913
+ }
2914
+ if (arrowNormIdx >= 0) {
2915
+ const bodyStart = arrowNormIdx + 3;
2916
+ const signature = normalized.slice(0, bodyStart).trim();
2917
+ const body = normalized.slice(bodyStart).trim();
2918
+ return { signature, body };
2919
+ }
2920
+ return { signature: normalized, body: "" };
2921
+ }
2922
+ function extractSignatureLine(rawText, _kind) {
2923
+ const normalized = collapseWhitespace(rawText);
2924
+ return splitSignatureBody(normalized).signature;
2925
+ }
2926
+ function extractBodyText(rawText, maxBodyLines) {
2927
+ const normalized = collapseWhitespace(rawText);
2928
+ const body = splitSignatureBody(normalized).body;
2929
+ if (maxBodyLines <= 0 || body.length === 0) return body;
2930
+ const tokens = body.split(/\s+/);
2931
+ return tokens.slice(0, maxBodyLines).join(" ");
2932
+ }
2933
+ function buildCanonicalText(input) {
2934
+ const { symbol, rawText, docComment, maxBodyLines } = input;
2935
+ const budget = maxBodyLines ?? DEFAULT_CANONICAL_BODY_LINES;
2936
+ const signature = extractSignatureLine(rawText, symbol.kind);
2937
+ const body = extractBodyText(rawText, budget);
2938
+ const doc = docComment ? collapseWhitespace(docComment) : "";
2939
+ const parts = [
2940
+ `KIND:${symbol.kind}`,
2941
+ `QNAME:${symbol.qualifiedName}`,
2942
+ `SIG:${signature}`
2943
+ ];
2944
+ if (doc.length > 0) parts.push(`DOC:${doc}`);
2945
+ parts.push(`BODY:${body}`);
2946
+ return parts.join("\n");
2947
+ }
2948
+ function canonicalTextHash(canonicalText) {
2949
+ return createHash2("sha256").update(canonicalText, "utf8").digest("hex");
2950
+ }
2951
+ function buildCanonicalTextAndHash(input) {
2952
+ const text = buildCanonicalText(input);
2953
+ return { text, hash: canonicalTextHash(text) };
2954
+ }
2955
+
2956
+ // src/semantic/minhash.ts
2957
+ import { createHash as createHash3 } from "crypto";
2958
+ function seedFor(index) {
2959
+ const hex = createHash3("sha256").update(`remnic:minhash:seed:${index}`, "utf8").digest("hex").slice(0, 16);
2960
+ return BigInt(`0x${hex}`);
2961
+ }
2962
+ var MINHASH_SEEDS = Object.freeze(
2963
+ Array.from({ length: MINHASH_NUM_PERMUTATIONS }, (_, i) => seedFor(i))
2964
+ );
2965
+ function hash64(data) {
2966
+ const buf = createHash3("sha256").update(data, "utf8").digest();
2967
+ let h = 0n;
2968
+ for (let i = 0; i < 8; i++) {
2969
+ h = h << 8n | BigInt(buf[i]);
2970
+ }
2971
+ return h;
2972
+ }
2973
+ var MINHASH_MODULUS = (1n << 61n) - 1n;
2974
+ function tokenizeForShingling(body) {
2975
+ return body.toLowerCase().split(/[^a-z0-9_]+/).filter((t) => t.length > 0);
2976
+ }
2977
+ function shingleSet(tokens) {
2978
+ if (tokens.length < MINHASH_SHINGLE_WIDTH) {
2979
+ return new Set(tokens.length > 0 ? [tokens.join(" ")] : []);
2980
+ }
2981
+ const out = /* @__PURE__ */ new Set();
2982
+ for (let i = 0; i <= tokens.length - MINHASH_SHINGLE_WIDTH; i++) {
2983
+ out.add(tokens.slice(i, i + MINHASH_SHINGLE_WIDTH).join(" "));
2984
+ }
2985
+ return out;
2986
+ }
2987
+ function minHashSignature(shingles) {
2988
+ const sig = [];
2989
+ if (shingles.size === 0) {
2990
+ for (let i = 0; i < MINHASH_NUM_PERMUTATIONS; i++) sig.push(MINHASH_MODULUS);
2991
+ return sig;
2992
+ }
2993
+ const shingleArr = Array.from(shingles);
2994
+ for (let p = 0; p < MINHASH_NUM_PERMUTATIONS; p++) {
2995
+ const a = MINHASH_SEEDS[p];
2996
+ const b = MINHASH_SEEDS[(p * 2 + 1) % MINHASH_SEEDS.length];
2997
+ let min = MINHASH_MODULUS;
2998
+ for (const s of shingleArr) {
2999
+ const h = (a * hash64(s) + b) % MINHASH_MODULUS;
3000
+ if (h < min) min = h;
3001
+ }
3002
+ sig.push(min);
3003
+ }
3004
+ return sig;
3005
+ }
3006
+ function lshBandKeys(signature) {
3007
+ const keys = [];
3008
+ for (let b = 0; b < LSH_NUM_BANDS; b++) {
3009
+ const start = b * LSH_ROWS_PER_BAND;
3010
+ const end = start + LSH_ROWS_PER_BAND;
3011
+ keys.push(signature.slice(start, end).map((v) => v.toString(16)).join("|"));
3012
+ }
3013
+ return keys;
3014
+ }
3015
+ var MinHasher = class {
3016
+ /** band key → set of node ids in that band. */
3017
+ buckets = /* @__PURE__ */ new Map();
3018
+ /** node id → signature (for Jaccard estimation on candidate pairs). */
3019
+ signatures = /* @__PURE__ */ new Map();
3020
+ /** node id → qualified name (for readable candidate output). */
3021
+ qnames = /* @__PURE__ */ new Map();
3022
+ /**
3023
+ * Add a symbol body to the LSH index. Idempotent — re-adding the same
3024
+ * (nodeId, body) is a no-op.
3025
+ */
3026
+ add(entry) {
3027
+ if (this.signatures.has(entry.nodeId)) return;
3028
+ const tokens = tokenizeForShingling(entry.body);
3029
+ const shingles = shingleSet(tokens);
3030
+ if (shingles.size === 0) return;
3031
+ const sig = minHashSignature(shingles);
3032
+ this.signatures.set(entry.nodeId, sig);
3033
+ this.qnames.set(entry.nodeId, entry.qualifiedName);
3034
+ for (const key of lshBandKeys(sig)) {
3035
+ let bucket = this.buckets.get(key);
3036
+ if (!bucket) {
3037
+ bucket = /* @__PURE__ */ new Set();
3038
+ this.buckets.set(key, bucket);
3039
+ }
3040
+ bucket.add(entry.nodeId);
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Find all candidate pairs (pairs sharing at least one LSH band) with
3045
+ * their estimated Jaccard similarity. Returns a stable-sorted array
3046
+ * (by aNodeId then bNodeId) so the determinism test can compare runs
3047
+ * byte-for-byte.
3048
+ */
3049
+ findCandidates() {
3050
+ const pairSet = /* @__PURE__ */ new Set();
3051
+ const pairs = [];
3052
+ for (const bucket of this.buckets.values()) {
3053
+ if (bucket.size < 2) continue;
3054
+ const ids = Array.from(bucket).sort();
3055
+ for (let i = 0; i < ids.length; i++) {
3056
+ for (let j = i + 1; j < ids.length; j++) {
3057
+ const a = ids[i];
3058
+ const b = ids[j];
3059
+ const key = `${a}\0${b}`;
3060
+ if (pairSet.has(key)) continue;
3061
+ pairSet.add(key);
3062
+ const sigA = this.signatures.get(a);
3063
+ const sigB = this.signatures.get(b);
3064
+ let matches = 0;
3065
+ for (let k = 0; k < sigA.length; k++) {
3066
+ if (sigA[k] === sigB[k]) matches++;
3067
+ }
3068
+ pairs.push({
3069
+ aNodeId: a,
3070
+ bNodeId: b,
3071
+ aQualifiedName: this.qnames.get(a) ?? a,
3072
+ bQualifiedName: this.qnames.get(b) ?? b,
3073
+ jaccard: matches / sigA.length
3074
+ });
3075
+ }
3076
+ }
3077
+ }
3078
+ pairs.sort((x, y) => {
3079
+ if (x.aNodeId !== y.aNodeId) return x.aNodeId < y.aNodeId ? -1 : 1;
3080
+ return x.bNodeId < y.bNodeId ? -1 : x.bNodeId > y.bNodeId ? 1 : 0;
3081
+ });
3082
+ return pairs;
3083
+ }
3084
+ };
3085
+ function createMinHasher() {
3086
+ return new MinHasher();
3087
+ }
3088
+ function cosineSimilarity(a, b) {
3089
+ const len = Math.min(a.length, b.length);
3090
+ let dot = 0;
3091
+ let normA = 0;
3092
+ let normB = 0;
3093
+ for (let i = 0; i < len; i++) {
3094
+ const av = a[i];
3095
+ const bv = b[i];
3096
+ dot += av * bv;
3097
+ normA += av * av;
3098
+ normB += bv * bv;
3099
+ }
3100
+ if (normA === 0 || normB === 0) return 0;
3101
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
3102
+ }
3103
+
3104
+ // src/semantic/vectors.ts
3105
+ import { readFile } from "fs/promises";
3106
+ import path5 from "path";
3107
+ import { normalizeHostEmbeddingVector } from "@remnic/core/host-embedding-provider";
3108
+ function modelIdFor(provider) {
3109
+ return provider.model ?? provider.id;
3110
+ }
3111
+ async function indexSymbolVectors(input) {
3112
+ const { store, provider, repoRoot, config, signal } = input;
3113
+ if (store.isClosed) {
3114
+ return { ok: false, code: "store_closed" };
3115
+ }
3116
+ if (!config.enabled) {
3117
+ return { ok: false, code: "semantic_disabled" };
3118
+ }
3119
+ if (!provider) {
3120
+ return { ok: false, code: "provider_unavailable" };
3121
+ }
3122
+ const modelId = modelIdFor(provider);
3123
+ let nodes;
3124
+ try {
3125
+ nodes = store.readNodesForSemantic();
3126
+ } catch {
3127
+ return { ok: false, code: "db_error" };
3128
+ }
3129
+ const limit = config.maxSymbolsPerRun;
3130
+ let embedded = 0;
3131
+ let cached = 0;
3132
+ let skipped = 0;
3133
+ let embedAttempts = 0;
3134
+ for (const node of nodes) {
3135
+ if (signal?.aborted) break;
3136
+ if (limit > 0 && embedAttempts >= limit) break;
3137
+ const absolutePath = path5.resolve(repoRoot, node.filePath);
3138
+ let bytes;
3139
+ try {
3140
+ bytes = await readFile(absolutePath);
3141
+ } catch {
3142
+ skipped += 1;
3143
+ continue;
3144
+ }
3145
+ const start = Math.max(0, node.startByte);
3146
+ const end = Math.min(bytes.length, node.endByte);
3147
+ if (start > end) {
3148
+ skipped += 1;
3149
+ continue;
3150
+ }
3151
+ const rawText = bytes.subarray(start, end).toString("utf8");
3152
+ const { text: canonicalText, hash } = buildCanonicalTextAndHash({
3153
+ symbol: {
3154
+ kind: node.kind,
3155
+ name: node.qualifiedName.split(/[.#:]/).pop() ?? node.qualifiedName,
3156
+ qualifiedName: node.qualifiedName,
3157
+ span: { startByte: node.startByte, endByte: node.endByte }
3158
+ },
3159
+ rawText,
3160
+ maxBodyLines: config.canonicalBodyLines
3161
+ });
3162
+ const cachedRow = store.readSymbolVector(node.nodeId, modelId);
3163
+ if (cachedRow && cachedRow.contentHash === hash && (provider.dimensions === void 0 || cachedRow.dims === provider.dimensions)) {
3164
+ cached += 1;
3165
+ continue;
3166
+ }
3167
+ embedAttempts += 1;
3168
+ let raw;
3169
+ try {
3170
+ raw = await provider.embed(canonicalText, {
3171
+ signal,
3172
+ inputType: "document"
3173
+ });
3174
+ } catch {
3175
+ if (cachedRow && cachedRow.contentHash !== hash) {
3176
+ await store.deleteSymbolVectors([node.nodeId]);
3177
+ }
3178
+ skipped += 1;
3179
+ continue;
3180
+ }
3181
+ const vec = normalizeHostEmbeddingVector(raw);
3182
+ if (!vec || vec.length === 0) {
3183
+ if (cachedRow && cachedRow.contentHash !== hash) {
3184
+ await store.deleteSymbolVectors([node.nodeId]);
3185
+ }
3186
+ skipped += 1;
3187
+ continue;
3188
+ }
3189
+ const float32 = new Float32Array(vec);
3190
+ const persisted = await store.writeSymbolVector({
3191
+ nodeId: node.nodeId,
3192
+ modelId,
3193
+ contentHash: hash,
3194
+ dims: float32.length,
3195
+ vector: float32
3196
+ });
3197
+ if (persisted) {
3198
+ embedded += 1;
3199
+ } else {
3200
+ skipped += 1;
3201
+ }
3202
+ }
3203
+ return { ok: true, embedded, cached, skipped };
3204
+ }
3205
+
3206
+ // src/semantic/similarity.ts
3207
+ import { readFileSync as fsReadFileSync } from "fs";
3208
+ import path6 from "path";
3209
+ var fs = { readFileSync: fsReadFileSync };
3210
+ var CONFIRM_OPERATOR = ">=";
3211
+ var MINHASH_JACCARD_GATE = 0.5;
3212
+ function computeSimilarTo(input) {
3213
+ const { store, provider, config } = input;
3214
+ if (!config.enabled) {
3215
+ return { ok: false, code: "semantic_disabled" };
3216
+ }
3217
+ if (!input.bodies && !input.repoRoot) {
3218
+ return {
3219
+ ok: false,
3220
+ code: "repo_root_unset",
3221
+ message: "computeSimilarTo needs either 'bodies' or 'repoRoot' to read source text"
3222
+ };
3223
+ }
3224
+ if (store.isClosed) {
3225
+ return { ok: false, code: "store_closed" };
3226
+ }
3227
+ const bodies = input.bodies ?? readBodiesFromStore(store, input.repoRoot);
3228
+ const modelId = provider ? modelIdFor(provider) : void 0;
3229
+ const vectors = input.vectors ?? (modelId ? readVectorsMap(store, modelId) : /* @__PURE__ */ new Map());
3230
+ const hasher = createMinHasher();
3231
+ for (const [nodeId, entry] of bodies) {
3232
+ hasher.add({ nodeId, qualifiedName: entry.qualifiedName, body: entry.body });
3233
+ }
3234
+ const candidates = hasher.findCandidates();
3235
+ const edges = [];
3236
+ let confirmed = 0;
3237
+ let minhashOnly = 0;
3238
+ for (const c of candidates) {
3239
+ const va = vectors.get(c.aNodeId);
3240
+ const vb = vectors.get(c.bNodeId);
3241
+ if (va && vb && va.length === vb.length) {
3242
+ const cos = cosineSimilarity(va, vb);
3243
+ if (cos >= config.similarToThreshold) {
3244
+ edges.push({
3245
+ srcNodeId: c.aNodeId,
3246
+ dstNodeId: c.bNodeId,
3247
+ srcQualifiedName: c.aQualifiedName,
3248
+ dstQualifiedName: c.bQualifiedName,
3249
+ confidence: cos,
3250
+ confirmed: true
3251
+ });
3252
+ confirmed += 1;
3253
+ }
3254
+ } else if (!provider) {
3255
+ if (c.jaccard >= MINHASH_JACCARD_GATE) {
3256
+ edges.push({
3257
+ srcNodeId: c.aNodeId,
3258
+ dstNodeId: c.bNodeId,
3259
+ srcQualifiedName: c.aQualifiedName,
3260
+ dstQualifiedName: c.bQualifiedName,
3261
+ confidence: MINHASH_ONLY_CONFIDENCE,
3262
+ confirmed: false
3263
+ });
3264
+ minhashOnly += 1;
3265
+ }
3266
+ }
3267
+ }
3268
+ edges.sort((a, b) => {
3269
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
3270
+ if (a.srcQualifiedName !== b.srcQualifiedName) return a.srcQualifiedName < b.srcQualifiedName ? -1 : 1;
3271
+ return a.dstQualifiedName < b.dstQualifiedName ? -1 : a.dstQualifiedName > b.dstQualifiedName ? 1 : 0;
3272
+ });
3273
+ return { ok: true, edges, candidates: candidates.length, confirmed, minhashOnly };
3274
+ }
3275
+ function similarEdgesToEdgeIR(edges) {
3276
+ return edges.map((e) => ({
3277
+ srcQualifiedName: e.srcQualifiedName,
3278
+ dstQualifiedName: e.dstQualifiedName,
3279
+ type: SIMILAR_TO_EDGE_TYPE,
3280
+ confidence: e.confidence,
3281
+ provenance: SEMANTIC_PROVENANCE,
3282
+ srcNodeId: e.srcNodeId,
3283
+ dstNodeId: e.dstNodeId
3284
+ }));
3285
+ }
3286
+ function readBodiesFromStore(store, repoRoot) {
3287
+ const out = /* @__PURE__ */ new Map();
3288
+ for (const node of store.readNodesForSemantic()) {
3289
+ let rawText = "";
3290
+ if (repoRoot) {
3291
+ try {
3292
+ const abs = path6.resolve(repoRoot, node.filePath);
3293
+ const bytes = fs.readFileSync(abs);
3294
+ const start = Math.max(0, node.startByte);
3295
+ const end = Math.min(bytes.length, node.endByte);
3296
+ if (start <= end) rawText = bytes.subarray(start, end).toString("utf8");
3297
+ } catch {
3298
+ }
3299
+ }
3300
+ const body = extractBodyText(rawText, 0);
3301
+ out.set(node.nodeId, { qualifiedName: node.qualifiedName, body });
3302
+ }
3303
+ return out;
3304
+ }
3305
+ function readVectorsMap(store, modelId) {
3306
+ const out = /* @__PURE__ */ new Map();
3307
+ for (const row of store.readAllSymbolVectors(modelId)) {
3308
+ out.set(row.nodeId, row.vector);
3309
+ }
3310
+ return out;
3311
+ }
3312
+ function estimateJaccard(bodyA, bodyB) {
3313
+ const sa = shingleSet(tokenizeForShingling(bodyA));
3314
+ const sb = shingleSet(tokenizeForShingling(bodyB));
3315
+ if (sa.size === 0 && sb.size === 0) return 1;
3316
+ let inter = 0;
3317
+ for (const s of sa) if (sb.has(s)) inter += 1;
3318
+ const union = sa.size + sb.size - inter;
3319
+ return union === 0 ? 0 : inter / union;
3320
+ }
3321
+
3322
+ // src/semantic/semantic-query.ts
3323
+ import {
3324
+ EmbeddingProviderUnavailableError,
3325
+ EmbeddingTimeoutError
3326
+ } from "@remnic/core/embedding-fallback";
3327
+ import { normalizeHostEmbeddingVector as normalizeHostEmbeddingVector2 } from "@remnic/core/host-embedding-provider";
3328
+ var DEFAULT_SEMANTIC_QUERY_LIMIT = 10;
3329
+ async function semanticQuery(input) {
3330
+ const { store, provider, repoRoot, config, query, signal } = input;
3331
+ if (store.isClosed) {
3332
+ return { ok: false, code: "store_closed" };
3333
+ }
3334
+ if (!config.enabled) {
3335
+ return { ok: false, code: "semantic_disabled" };
3336
+ }
3337
+ if (query.length === 0) {
3338
+ return { ok: false, code: "invalid_query", message: "query must be non-empty" };
3339
+ }
3340
+ if (!provider) {
3341
+ return { ok: false, code: "provider_unavailable" };
3342
+ }
3343
+ let raw;
3344
+ try {
3345
+ raw = await provider.embed(query, { signal, inputType: "query" });
3346
+ } catch (error) {
3347
+ if (error instanceof EmbeddingTimeoutError) {
3348
+ return { ok: false, code: "provider_timeout" };
3349
+ }
3350
+ if (error instanceof EmbeddingProviderUnavailableError) {
3351
+ return { ok: false, code: "provider_unavailable" };
3352
+ }
3353
+ return { ok: false, code: "provider_unavailable" };
3354
+ }
3355
+ const queryVec = normalizeHostEmbeddingVector2(raw);
3356
+ if (!queryVec || queryVec.length === 0) {
3357
+ return { ok: false, code: "malformed_vector" };
3358
+ }
3359
+ const queryF32 = new Float32Array(queryVec);
3360
+ const modelId = modelIdFor(provider);
3361
+ let rows;
3362
+ try {
3363
+ rows = store.readAllSymbolVectors(modelId);
3364
+ } catch {
3365
+ return { ok: false, code: "db_error" };
3366
+ }
3367
+ if (rows.length === 0) {
3368
+ return { ok: false, code: "no_vectors" };
3369
+ }
3370
+ const rawLimit = input.limit;
3371
+ const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.max(1, Math.floor(rawLimit)) : DEFAULT_SEMANTIC_QUERY_LIMIT;
3372
+ const queryDims = queryF32.length;
3373
+ const scored = rows.filter((r) => r.dims === queryDims).map((r) => ({
3374
+ nodeId: r.nodeId,
3375
+ qualifiedName: r.qualifiedName,
3376
+ filePath: r.filePath,
3377
+ kind: r.kind,
3378
+ dims: r.dims,
3379
+ score: cosineSimilarity(queryF32, r.vector)
3380
+ })).filter((r) => Number.isFinite(r.score)).sort((a, b) => b.score - a.score).slice(0, limit);
3381
+ const hits = [];
3382
+ for (const h of scored) {
3383
+ const neighbors = store.readNeighborsByNodeId(h.nodeId);
3384
+ let snippet = "";
3385
+ try {
3386
+ const snippetResult = await store.snippetFor({ nodeId: h.nodeId, repoRoot });
3387
+ if (snippetResult.ok) snippet = snippetResult.text;
3388
+ } catch {
3389
+ }
3390
+ hits.push({
3391
+ qualifiedName: h.qualifiedName,
3392
+ filePath: h.filePath,
3393
+ kind: h.kind,
3394
+ score: h.score,
3395
+ snippet,
3396
+ callers: neighbors.callers,
3397
+ callees: neighbors.callees
3398
+ });
3399
+ }
3400
+ return { ok: true, hits };
3401
+ }
3402
+
3403
+ // src/index.ts
3404
+ var ENGINE_VERSION = CODING_GRAPH_ENGINE_VERSION2;
3405
+ var CodingGraphError = class extends Error {
3406
+ code;
3407
+ engineVersion;
3408
+ constructor(code, message, engineVersion = ENGINE_VERSION) {
3409
+ super(message);
3410
+ this.name = "CodingGraphError";
3411
+ this.code = code;
3412
+ this.engineVersion = engineVersion;
3413
+ }
3414
+ };
3415
+ export {
3416
+ BLAST_RADIUS_EDGE_TYPES,
3417
+ CODING_GRAPH_ENGINE_VERSION2 as CODING_GRAPH_ENGINE_VERSION,
3418
+ CODING_GRAPH_SCHEMA_VERSION,
3419
+ CONFIRM_OPERATOR,
3420
+ CodingGraphError,
3421
+ DEAD_CODE_EXCLUSION,
3422
+ DEFAULT_BLAST_RADIUS_DEPTH,
3423
+ DEFAULT_CANONICAL_BODY_LINES,
3424
+ DEFAULT_CO_CHANGE_CONFIG,
3425
+ DEFAULT_LSP_CONFIG,
3426
+ DEFAULT_LSP_MAX_REQUESTS_PER_RUN,
3427
+ DEFAULT_LSP_TIMEOUT_MS,
3428
+ DEFAULT_MAX_SYMBOLS_PER_RUN,
3429
+ DEFAULT_SEMANTIC_QUERY_LIMIT,
3430
+ DEFAULT_SIMILAR_TO_THRESHOLD,
3431
+ DEFAULT_TRAVERSE_PATHS_MAX,
3432
+ EDGE_PROVENANCE_VALUES,
3433
+ ENGINE_VERSION,
3434
+ FAN_IN_ESCALATION_THRESHOLD,
3435
+ GraphStore,
3436
+ LspClient,
3437
+ LspFrameDecoder,
3438
+ MAX_TRAVERSE_PATHS_HOPS,
3439
+ META_KEY_LAST_HEAD,
3440
+ META_KEY_PENDING_PARSE_FAILURES,
3441
+ MINHASH_ONLY_CONFIDENCE,
3442
+ MINHASH_SEEDS,
3443
+ MinHasher,
3444
+ SEMANTIC_PROVENANCE,
3445
+ SIMILAR_TO_EDGE_TYPE,
3446
+ TIER_1_LANGUAGES3 as TIER_1_LANGUAGES,
3447
+ VALID_CYPHER_LABELS,
3448
+ WasmTreeSitterBackend,
3449
+ applyCodingGraphSchema,
3450
+ buildCanonicalText,
3451
+ buildCanonicalTextAndHash,
3452
+ buildLineOffsetMap,
3453
+ byteOffsetToPosition,
3454
+ byteSpanToLines,
3455
+ canonicalTextHash,
3456
+ classifyRisk,
3457
+ collapseWhitespace,
3458
+ computeBlastRadius,
3459
+ computeSimilarTo,
3460
+ cosineSimilarity,
3461
+ createCodingGraphEngine,
3462
+ createMinHasher,
3463
+ defaultCodingGitInvoker,
3464
+ encodeLspFrame,
3465
+ estimateJaccard,
3466
+ executeAst,
3467
+ executeCypher,
3468
+ executeLspResolution,
3469
+ executeReindex,
3470
+ extractBodyText,
3471
+ extractSignatureLine,
3472
+ findDirectlyAffectedSymbols,
3473
+ formatLspStatusLine,
3474
+ getIndexStatus,
3475
+ getLspStatus,
3476
+ hashContent,
3477
+ indexSymbolVectors,
3478
+ isEdgeProvenance,
3479
+ lshBandKeys,
3480
+ lspDegradation,
3481
+ mapLocationToNode,
3482
+ minHashSignature,
3483
+ mineAndStoreCoChanges,
3484
+ mineCoChangeEdges,
3485
+ modelIdFor,
3486
+ nodeIdFor,
3487
+ parseCypher,
3488
+ parseHunks,
3489
+ parseLogFiles,
3490
+ parseLspConfig,
3491
+ parseNameStatus,
3492
+ pathToUri,
3493
+ planLspUpgrades,
3494
+ planReindex,
3495
+ positionToByteOffset,
3496
+ rangesOverlap,
3497
+ readFileHashes,
3498
+ readLastIndexedHead,
3499
+ readLspEnabledEnv,
3500
+ readSchemaVersion,
3501
+ resolutionResultToStatusMaps,
3502
+ resolveSemanticConfig,
3503
+ semanticQuery,
3504
+ shingleSet,
3505
+ similarEdgesToEdgeIR,
3506
+ tokenizeForShingling,
3507
+ uriToPath
3508
+ };
3509
+ //# sourceMappingURL=index.js.map