@remnic/coding-graph 9.3.759
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/dist/chunk-5I2DBHOQ.js +1042 -0
- package/dist/chunk-5I2DBHOQ.js.map +1 -0
- package/dist/chunk-CPYJACC5.js +1838 -0
- package/dist/chunk-CPYJACC5.js.map +1 -0
- package/dist/chunk-ZVCMIM4T.js +216 -0
- package/dist/chunk-ZVCMIM4T.js.map +1 -0
- package/dist/cypher/query-parser.d.ts +253 -0
- package/dist/cypher/query-parser.js +17 -0
- package/dist/cypher/query-parser.js.map +1 -0
- package/dist/graph-schema.d.ts +84 -0
- package/dist/graph-schema.js +17 -0
- package/dist/graph-schema.js.map +1 -0
- package/dist/graph-store.d.ts +938 -0
- package/dist/graph-store.js +16 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/index.d.ts +1953 -0
- package/dist/index.js +3509 -0
- package/dist/index.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +79 -0
- package/src/co-change.test.ts +175 -0
- package/src/co-change.ts +167 -0
- package/src/cypher/query-parser.test.ts +1107 -0
- package/src/cypher/query-parser.ts +1692 -0
- package/src/detect-changes.test.ts +533 -0
- package/src/detect-changes.ts +367 -0
- package/src/engine/emit.ts +556 -0
- package/src/engine/engine.test.ts +1417 -0
- package/src/engine/engine.ts +182 -0
- package/src/engine/extractors.ts +486 -0
- package/src/engine/fixtures.ts +364 -0
- package/src/engine/language-sniff.ts +56 -0
- package/src/engine/parser-backend.ts +206 -0
- package/src/engine/utf16-offsets.ts +68 -0
- package/src/git-invoker.test.ts +116 -0
- package/src/git-invoker.ts +426 -0
- package/src/graph-schema.test.ts +541 -0
- package/src/graph-schema.ts +383 -0
- package/src/graph-store-pr2.test.ts +1879 -0
- package/src/graph-store.test.ts +1420 -0
- package/src/graph-store.ts +3489 -0
- package/src/index-status.test.ts +303 -0
- package/src/index-status.ts +135 -0
- package/src/index.ts +384 -0
- package/src/lsp/byte-position.ts +173 -0
- package/src/lsp/characterization.test.ts +174 -0
- package/src/lsp/client.test.ts +275 -0
- package/src/lsp/client.ts +484 -0
- package/src/lsp/config.ts +219 -0
- package/src/lsp/degradation.ts +86 -0
- package/src/lsp/fixtures/fake-server.mjs +198 -0
- package/src/lsp/framing.test.ts +180 -0
- package/src/lsp/framing.ts +177 -0
- package/src/lsp/resolution.test.ts +497 -0
- package/src/lsp/resolution.ts +483 -0
- package/src/lsp/status.ts +140 -0
- package/src/lsp/types.ts +167 -0
- package/src/reindex.test.ts +1038 -0
- package/src/reindex.ts +908 -0
- package/src/row-types.ts +45 -0
- package/src/semantic/canonical-text.test.ts +150 -0
- package/src/semantic/canonical-text.ts +219 -0
- package/src/semantic/config.ts +235 -0
- package/src/semantic/index.ts +78 -0
- package/src/semantic/minhash.test.ts +197 -0
- package/src/semantic/minhash.ts +261 -0
- package/src/semantic/semantic-query.ts +173 -0
- package/src/semantic/semantic.test.ts +1315 -0
- package/src/semantic/similarity.ts +268 -0
- package/src/semantic/types.ts +145 -0
- package/src/semantic/vectors.ts +235 -0
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
|