@neuralsea/workspace-indexer 0.1.0 → 0.3.2

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/dist/index.js CHANGED
@@ -1,22 +1,427 @@
1
1
  import {
2
2
  DEFAULT_PROFILES,
3
+ GoModuleLinkStrategy,
3
4
  HashEmbeddingsProvider,
5
+ IndexerProgressObservable,
6
+ Neo4jGraphStore,
7
+ NestedRepoLinkStrategy,
8
+ NoopAnnIndex,
9
+ NpmDependencyLinkStrategy,
4
10
  OllamaEmbeddingsProvider,
5
11
  OpenAIEmbeddingsProvider,
6
12
  RepoIndexer,
13
+ VsCodeContributesLanguageLinkStrategy,
7
14
  WorkspaceIndexer,
15
+ WorkspaceLinker,
16
+ WorkspaceStore,
17
+ asProgressSink,
18
+ chunkSource,
19
+ createAnnIndex,
20
+ createNeo4jGraphStore,
8
21
  createVectorIndex,
9
22
  deepMergeProfile,
10
- loadConfigFile
11
- } from "./chunk-QPQCSCBN.js";
23
+ discoverGitRepos,
24
+ languageFromPath,
25
+ linkWorkspaceRepos,
26
+ loadConfigFile,
27
+ mergeIndexerConfig,
28
+ pickRepoOverride,
29
+ stableSymbolId
30
+ } from "./chunk-GGL3XTMV.js";
31
+
32
+ // src/symbolGraph/vscodeProvider.ts
33
+ import path2 from "path";
34
+ import { createRequire } from "module";
35
+
36
+ // src/symbolGraph/strategies.ts
37
+ import path from "path";
38
+ function toPosixRel(repoRoot, absPath) {
39
+ const abs = path.resolve(absPath);
40
+ const root = path.resolve(repoRoot);
41
+ const rel = path.relative(root, abs);
42
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return null;
43
+ return rel.split(path.sep).join("/");
44
+ }
45
+ function fromLspRange(r) {
46
+ return {
47
+ startLine: r.start.line + 1,
48
+ startCharacter: r.start.character + 1,
49
+ endLine: r.end.line + 1,
50
+ endCharacter: r.end.character + 1
51
+ };
52
+ }
53
+ function bestRange(sym) {
54
+ return sym.selectionRange ?? sym.range;
55
+ }
56
+ function makeSymbolId(input, sym) {
57
+ const range = fromLspRange(bestRange(sym));
58
+ return stableSymbolId({
59
+ repoRoot: input.repoRoot,
60
+ path: input.path,
61
+ language: input.language,
62
+ kind: sym.kind,
63
+ name: sym.name,
64
+ range
65
+ });
66
+ }
67
+ function flattenSymbols(input, syms) {
68
+ const symbols = [];
69
+ const edges = [];
70
+ const visit = (s, parentId) => {
71
+ const id = makeSymbolId(input, s);
72
+ const range = fromLspRange(bestRange(s));
73
+ symbols.push({
74
+ id,
75
+ repoRoot: input.repoRoot,
76
+ path: input.path,
77
+ language: input.language,
78
+ name: s.name,
79
+ kind: s.kind,
80
+ range,
81
+ containerName: s.containerName,
82
+ detail: s.detail
83
+ });
84
+ if (parentId) {
85
+ edges.push({
86
+ repoRoot: input.repoRoot,
87
+ fromId: parentId,
88
+ toId: id,
89
+ kind: "contains",
90
+ fromPath: input.path,
91
+ toPath: input.path
92
+ });
93
+ }
94
+ for (const c of s.children ?? []) visit(c, id);
95
+ };
96
+ for (const s of syms) visit(s);
97
+ return { symbols, containsEdges: edges };
98
+ }
99
+ function cacheKeyFor(doc, input) {
100
+ const hv = input.contentHash ? `h:${input.contentHash}` : doc.version !== void 0 ? `v:${doc.version}` : "v:0";
101
+ return `${doc.fsPath}::${hv}`;
102
+ }
103
+ var SymbolGraphIndexer = class {
104
+ constructor(lsp, opts = {}) {
105
+ this.lsp = lsp;
106
+ this.opts = opts;
107
+ }
108
+ cache = /* @__PURE__ */ new Map();
109
+ async indexDocument(input, cancel = {}) {
110
+ const doc = await this.lsp.openTextDocument(path.join(input.repoRoot, input.path.split("/").join(path.sep)));
111
+ const key = cacheKeyFor(doc, input);
112
+ const cached = this.cache.get(key);
113
+ if (cached) {
114
+ const contains = cached.containsEdges;
115
+ if ((this.opts.mode ?? "staged") === "full") {
116
+ const extra = await this.expandDocumentEdges(input, cancel);
117
+ return { symbols: cached.symbols, edges: [...contains, ...extra] };
118
+ }
119
+ return { symbols: cached.symbols, edges: contains };
120
+ }
121
+ if (cancel.signal?.aborted) return { symbols: [], edges: [] };
122
+ const syms = await this.lsp.documentSymbols(doc);
123
+ const { symbols, containsEdges } = flattenSymbols(input, syms);
124
+ this.cache.set(key, {
125
+ key,
126
+ repoRoot: input.repoRoot,
127
+ path: input.path,
128
+ language: input.language,
129
+ symbols,
130
+ containsEdges,
131
+ edgeCache: /* @__PURE__ */ new Map()
132
+ });
133
+ if ((this.opts.mode ?? "staged") === "full") {
134
+ const extra = await this.expandDocumentEdges(input, cancel);
135
+ return { symbols, edges: [...containsEdges, ...extra] };
136
+ }
137
+ return { symbols, edges: containsEdges };
138
+ }
139
+ /**
140
+ * Compute cross-file edges for a document (definition/reference/implementation/typeDefinition).
141
+ * Intended for staged indexing and on-demand expansion during retrieval.
142
+ */
143
+ async expandDocumentEdges(input, cancel = {}) {
144
+ const doc = await this.lsp.openTextDocument(path.join(input.repoRoot, input.path.split("/").join(path.sep)));
145
+ const key = cacheKeyFor(doc, input);
146
+ const cached = this.cache.get(key);
147
+ if (!cached) {
148
+ await this.indexDocument(input, cancel);
149
+ }
150
+ const cached2 = this.cache.get(key);
151
+ if (!cached2) return [];
152
+ const maxEdges = this.opts.maxEdgesPerDocument ?? 200;
153
+ const out = [];
154
+ let emitted = 0;
155
+ const symbols = cached2.symbols;
156
+ for (const s of symbols) {
157
+ if (cancel.signal?.aborted) break;
158
+ if (s.kind === "file" || s.kind === "module" || s.kind === "namespace") continue;
159
+ const pos = { line: s.range.startLine - 1, character: s.range.startCharacter - 1 };
160
+ for (const kind of ["definition", "typeDefinition", "implementation", "reference"]) {
161
+ if (cancel.signal?.aborted) break;
162
+ if (emitted >= maxEdges) break;
163
+ const cacheKey = `${kind}:${s.id}`;
164
+ const existing = cached2.edgeCache.get(cacheKey);
165
+ if (existing && existing.length > 0) {
166
+ for (const e of existing) {
167
+ out.push(e);
168
+ emitted++;
169
+ if (emitted >= maxEdges) break;
170
+ }
171
+ continue;
172
+ }
173
+ let targets = [];
174
+ try {
175
+ targets = await this.lsp.resolveEdges(doc, pos, kind);
176
+ } catch {
177
+ targets = [];
178
+ }
179
+ const edges = [];
180
+ for (const t of targets) {
181
+ if (cancel.signal?.aborted) break;
182
+ if (emitted >= maxEdges) break;
183
+ const toPath = toPosixRel(input.repoRoot, t.fsPath);
184
+ if (!toPath) continue;
185
+ const toId = await this.bestEffortTargetSymbolId(input, t.fsPath, t.range, cancel);
186
+ edges.push({
187
+ repoRoot: input.repoRoot,
188
+ fromId: s.id,
189
+ toId,
190
+ kind,
191
+ fromPath: input.path,
192
+ toPath
193
+ });
194
+ emitted++;
195
+ }
196
+ cached2.edgeCache.set(cacheKey, edges);
197
+ out.push(...edges);
198
+ }
199
+ if (emitted >= maxEdges) break;
200
+ }
201
+ return out;
202
+ }
203
+ async bestEffortTargetSymbolId(input, targetFsPath, targetRange, cancel) {
204
+ const toPath = toPosixRel(input.repoRoot, targetFsPath) ?? "";
205
+ const fallback = stableSymbolId({
206
+ repoRoot: input.repoRoot,
207
+ path: toPath,
208
+ language: input.language,
209
+ kind: "unknown",
210
+ name: `@${targetRange.start.line + 1}:${targetRange.start.character + 1}`,
211
+ range: fromLspRange(targetRange)
212
+ });
213
+ if (!toPath) return fallback;
214
+ if (cancel.signal?.aborted) return fallback;
215
+ let doc;
216
+ try {
217
+ doc = await this.lsp.openTextDocument(targetFsPath);
218
+ } catch {
219
+ return fallback;
220
+ }
221
+ if (cancel.signal?.aborted) return fallback;
222
+ let syms = [];
223
+ try {
224
+ syms = await this.lsp.documentSymbols(doc);
225
+ } catch {
226
+ return fallback;
227
+ }
228
+ const best = findSymbolAt(syms, targetRange.start);
229
+ if (!best) return fallback;
230
+ const language = doc.languageId || input.language;
231
+ const range = fromLspRange(bestRange(best));
232
+ return stableSymbolId({
233
+ repoRoot: input.repoRoot,
234
+ path: toPath,
235
+ language,
236
+ kind: best.kind,
237
+ name: best.name,
238
+ range
239
+ });
240
+ }
241
+ };
242
+ function rangeContains(r, p) {
243
+ const le = (a, b) => a.line < b.line || a.line === b.line && a.character <= b.character;
244
+ return le(r.start, p) && le(p, r.end);
245
+ }
246
+ function findSymbolAt(syms, pos) {
247
+ let best = null;
248
+ let bestSpan = Number.POSITIVE_INFINITY;
249
+ const visit = (s) => {
250
+ const r = s.range;
251
+ if (!rangeContains(r, pos)) return;
252
+ const span = (r.end.line - r.start.line) * 1e6 + (r.end.character - r.start.character);
253
+ if (span < bestSpan) {
254
+ bestSpan = span;
255
+ best = s;
256
+ }
257
+ for (const c of s.children ?? []) visit(c);
258
+ };
259
+ for (const s of syms) visit(s);
260
+ return best;
261
+ }
262
+
263
+ // src/symbolGraph/vscodeProvider.ts
264
+ function fromPosixPath(posixRelPath) {
265
+ return posixRelPath.split("/").join(path2.sep);
266
+ }
267
+ function toPosixPath(p) {
268
+ return p.split(path2.sep).join("/");
269
+ }
270
+ function kindFromVscode(vscode, k) {
271
+ const SK = vscode?.SymbolKind;
272
+ if (!SK) return "unknown";
273
+ if (k === SK.File) return "file";
274
+ if (k === SK.Module) return "module";
275
+ if (k === SK.Namespace) return "namespace";
276
+ if (k === SK.Package) return "package";
277
+ if (k === SK.Class) return "class";
278
+ if (k === SK.Interface) return "interface";
279
+ if (k === SK.Enum) return "enum";
280
+ if (k === SK.TypeParameter) return "type";
281
+ if (k === SK.Function) return "function";
282
+ if (k === SK.Method) return "method";
283
+ if (k === SK.Property) return "property";
284
+ if (k === SK.Field) return "field";
285
+ if (k === SK.Variable) return "variable";
286
+ if (k === SK.Constant) return "constant";
287
+ if (k === SK.Constructor) return "constructor";
288
+ if (k === SK.Parameter) return "parameter";
289
+ return "unknown";
290
+ }
291
+ function fromLocationLike(loc) {
292
+ if (!loc) return [];
293
+ if (Array.isArray(loc)) return loc.flatMap(fromLocationLike);
294
+ if (loc?.uri && loc?.range) return [{ uri: loc.uri, range: loc.range }];
295
+ if (loc?.targetUri && (loc?.targetSelectionRange || loc?.targetRange)) {
296
+ return [{ uri: loc.targetUri, range: loc.targetSelectionRange ?? loc.targetRange }];
297
+ }
298
+ return [];
299
+ }
300
+ function toLspRange(r) {
301
+ return { start: { line: r.start.line, character: r.start.character }, end: { line: r.end.line, character: r.end.character } };
302
+ }
303
+ function toLspSymbols(vscode, res) {
304
+ if (!Array.isArray(res)) return [];
305
+ if (res.length === 0) return [];
306
+ if (res[0]?.location) {
307
+ return res.map((s) => ({
308
+ name: s.name,
309
+ kind: kindFromVscode(vscode, s.kind),
310
+ range: toLspRange(s.location.range),
311
+ selectionRange: toLspRange(s.location.range),
312
+ containerName: s.containerName,
313
+ detail: s.detail,
314
+ children: []
315
+ }));
316
+ }
317
+ const visit = (d) => ({
318
+ name: d.name,
319
+ kind: kindFromVscode(vscode, d.kind),
320
+ range: toLspRange(d.range),
321
+ selectionRange: d.selectionRange ? toLspRange(d.selectionRange) : void 0,
322
+ detail: d.detail,
323
+ containerName: d.containerName,
324
+ children: Array.isArray(d.children) ? d.children.map(visit) : []
325
+ });
326
+ return res.map(visit);
327
+ }
328
+ function toIndexInput(doc, input) {
329
+ const rel = toPosixPath(path2.relative(path2.resolve(input.repoRoot), path2.resolve(doc.fsPath)));
330
+ return { ...input, path: rel || input.path };
331
+ }
332
+ function makeVscodeFacade(vscode) {
333
+ const cmdForKind = {
334
+ definition: "vscode.executeDefinitionProvider",
335
+ reference: "vscode.executeReferenceProvider",
336
+ implementation: "vscode.executeImplementationProvider",
337
+ typeDefinition: "vscode.executeTypeDefinitionProvider"
338
+ };
339
+ return {
340
+ async openTextDocument(absPath) {
341
+ const uri = vscode.Uri.file(absPath);
342
+ const doc = await vscode.workspace.openTextDocument(uri);
343
+ return {
344
+ uri: String(doc.uri.toString()),
345
+ fsPath: doc.uri.fsPath,
346
+ languageId: doc.languageId ?? "text",
347
+ version: doc.version
348
+ };
349
+ },
350
+ async documentSymbols(doc) {
351
+ const uri = vscode.Uri.file(doc.fsPath);
352
+ const res = await vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", uri);
353
+ return toLspSymbols(vscode, res);
354
+ },
355
+ async resolveEdges(doc, pos, kind) {
356
+ const uri = vscode.Uri.file(doc.fsPath);
357
+ const command = cmdForKind[kind];
358
+ const res = await vscode.commands.executeCommand(command, uri, new vscode.Position(pos.line, pos.character));
359
+ const locs = fromLocationLike(res);
360
+ return locs.map((l) => ({
361
+ uri: String(l.uri.toString()),
362
+ fsPath: l.uri.fsPath,
363
+ range: toLspRange(l.range)
364
+ }));
365
+ }
366
+ };
367
+ }
368
+ async function createVSCodeSymbolGraphProvider(opts) {
369
+ let vscode;
370
+ try {
371
+ const require2 = createRequire(import.meta.url);
372
+ vscode = require2("vscode");
373
+ } catch {
374
+ return null;
375
+ }
376
+ const languages = new Set(
377
+ opts?.languages ?? ["typescript", "javascript", "python", "go", "isl", "ctl", "ltl", "colour-algebra"]
378
+ );
379
+ const lsp = makeVscodeFacade(vscode);
380
+ const indexer = new SymbolGraphIndexer(lsp, { mode: opts?.mode ?? "staged", maxEdgesPerDocument: opts?.maxEdgesPerDocument ?? 200 });
381
+ return {
382
+ id: "vscode.symbolGraph",
383
+ supports: (language) => languages.has(language),
384
+ async indexDocument(input) {
385
+ const absPath = path2.join(input.repoRoot, fromPosixPath(input.path));
386
+ const doc = await lsp.openTextDocument(absPath);
387
+ return await indexer.indexDocument(toIndexInput(doc, input), { signal: input.signal });
388
+ },
389
+ async expandDocumentEdges(input, o) {
390
+ const absPath = path2.join(input.repoRoot, fromPosixPath(input.path));
391
+ const doc = await lsp.openTextDocument(absPath);
392
+ return await indexer.expandDocumentEdges(toIndexInput(doc, input), { signal: o?.signal });
393
+ }
394
+ };
395
+ }
12
396
  export {
13
397
  DEFAULT_PROFILES,
398
+ GoModuleLinkStrategy,
14
399
  HashEmbeddingsProvider,
400
+ IndexerProgressObservable,
401
+ Neo4jGraphStore,
402
+ NestedRepoLinkStrategy,
403
+ NoopAnnIndex,
404
+ NpmDependencyLinkStrategy,
15
405
  OllamaEmbeddingsProvider,
16
406
  OpenAIEmbeddingsProvider,
17
407
  RepoIndexer,
408
+ SymbolGraphIndexer,
409
+ VsCodeContributesLanguageLinkStrategy,
18
410
  WorkspaceIndexer,
411
+ WorkspaceLinker,
412
+ WorkspaceStore,
413
+ asProgressSink,
414
+ chunkSource,
415
+ createAnnIndex,
416
+ createNeo4jGraphStore,
417
+ createVSCodeSymbolGraphProvider,
19
418
  createVectorIndex,
20
419
  deepMergeProfile,
21
- loadConfigFile
420
+ discoverGitRepos,
421
+ languageFromPath,
422
+ linkWorkspaceRepos,
423
+ loadConfigFile,
424
+ mergeIndexerConfig,
425
+ pickRepoOverride,
426
+ stableSymbolId
22
427
  };
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "@neuralsea/workspace-indexer",
3
- "version": "0.1.0",
3
+ "version": "0.3.2",
4
4
  "description": "Local-first multi-repo workspace indexer (semantic embeddings + git-aware incremental updates + hybrid retrieval profiles) for AI agents.",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
10
11
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
12
14
  }
13
15
  },
14
16
  "bin": {
@@ -20,10 +22,13 @@
20
22
  "LICENSE"
21
23
  ],
22
24
  "scripts": {
23
- "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
24
- "dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
25
+ "build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts --clean",
26
+ "dev": "tsup src/index.ts src/cli.ts --format esm,cjs --dts --watch",
25
27
  "lint": "node -e \"console.log('Add eslint if desired')\"",
26
- "test": "node --test dist/test.js || echo \"(optional)\""
28
+ "test": "node scripts/run-tests.mjs test",
29
+ "test:unit": "node scripts/run-tests.mjs test/unit",
30
+ "test:integration": "node scripts/run-tests.mjs test/integration",
31
+ "test:smoke": "node scripts/run-tests.mjs test/smoke"
27
32
  },
28
33
  "dependencies": {
29
34
  "better-sqlite3": "^12.5.0",
@@ -36,6 +41,7 @@
36
41
  "@types/better-sqlite3": "^7.6.13",
37
42
  "@types/node": "^25.0.3",
38
43
  "@types/yargs": "^17.0.35",
44
+ "neo4j-driver": "^5.28.1",
39
45
  "tsup": "^8.5.1",
40
46
  "typescript": "^5.9.3"
41
47
  },