@danypops/lector 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +11 -7
  2. package/package.json +5 -1
  3. package/src/adapters/fallback-code-intelligence-index.ts +73 -0
  4. package/src/adapters/find-source-files.ts +1 -1
  5. package/src/adapters/git-repo-fetcher.ts +154 -37
  6. package/src/adapters/lsp/discover-seed-file.ts +17 -9
  7. package/src/adapters/lsp/json-rpc-stream.ts +52 -2
  8. package/src/adapters/lsp/language-server-process.ts +54 -10
  9. package/src/adapters/lsp/lsp-symbol-index.ts +167 -41
  10. package/src/adapters/normalize-npm-repository.ts +77 -0
  11. package/src/adapters/npm-lockfile-version-resolver.ts +519 -0
  12. package/src/adapters/npm-package-source-resolver.ts +322 -0
  13. package/src/adapters/npm-registry-client.ts +304 -0
  14. package/src/adapters/polyglot-code-intelligence-index.ts +135 -0
  15. package/src/adapters/sqlite-symbol-graph.ts +68 -3
  16. package/src/adapters/tree-sitter/typescript-tree-sitter-symbol-index.ts +61 -11
  17. package/src/adapters/typescript-compiler-symbol-index.ts +129 -0
  18. package/src/cli.ts +97 -23
  19. package/src/daemon.ts +4 -0
  20. package/src/domain/find-workspace-symbols.ts +4 -3
  21. package/src/domain/installed-package-version.ts +66 -0
  22. package/src/domain/intelligence-provenance.ts +27 -0
  23. package/src/domain/language-server-descriptor.ts +22 -0
  24. package/src/domain/npm-package-metadata.ts +26 -0
  25. package/src/domain/package-source.ts +143 -0
  26. package/src/domain/repo-fetch-result.ts +33 -1
  27. package/src/domain/resolve-package-source.ts +204 -0
  28. package/src/domain/symbol-graph-generation.ts +5 -0
  29. package/src/domain/symbol-query.ts +16 -0
  30. package/src/domain/workspace-symbol.ts +13 -0
  31. package/src/index.ts +68 -4
  32. package/src/ports/installed-package-version-resolver-port.ts +5 -0
  33. package/src/ports/npm-registry-port.ts +5 -0
  34. package/src/ports/package-source-resolver-port.ts +5 -0
  35. package/src/ports/repo-fetcher-port.ts +2 -2
  36. package/src/ports/symbol-index-port.ts +4 -2
  37. package/src/service.ts +152 -43
package/src/service.ts CHANGED
@@ -1,15 +1,22 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { stat } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
+ import { FallbackCodeIntelligenceIndex } from "./adapters/fallback-code-intelligence-index.ts";
4
5
  import { InMemorySearchCache } from "./adapters/in-memory-search-cache.ts";
5
6
  import { InMemorySymbolGraph } from "./adapters/in-memory-symbol-graph.ts";
6
7
  import { LocalFilesystemWorkspace } from "./adapters/local-filesystem-workspace.ts";
7
8
  import { LocalGit } from "./adapters/local-git.ts";
8
- import { discoverWorkspaceDescriptor } from "./adapters/lsp/discover-seed-file.ts";
9
+ import { discoverWorkspaceDescriptor, discoverWorkspaceDescriptors } from "./adapters/lsp/discover-seed-file.ts";
9
10
  import { LspSymbolIndex } from "./adapters/lsp/lsp-symbol-index.ts";
11
+ import { NpmLockfileVersionResolver } from "./adapters/npm-lockfile-version-resolver.ts";
12
+ import { NpmPackageSourceResolver } from "./adapters/npm-package-source-resolver.ts";
13
+ import { NpmRegistryClient } from "./adapters/npm-registry-client.ts";
14
+ import { PolyglotCodeIntelligenceIndex } from "./adapters/polyglot-code-intelligence-index.ts";
10
15
  import { ReadOnlyWorkspace } from "./adapters/read-only-workspace.ts";
11
16
  import { RipgrepTextSearch } from "./adapters/ripgrep-text-search.ts";
12
17
  import { deriveSourceManifest } from "./adapters/source-manifest.ts";
18
+ import { TreeSitterSymbolIndex } from "./adapters/tree-sitter/typescript-tree-sitter-symbol-index.ts";
19
+ import { TypeScriptCompilerSymbolIndex } from "./adapters/typescript-compiler-symbol-index.ts";
13
20
  import { BoundedJobExecutor, type JobSnapshot } from "./domain/bounded-job-executor.ts";
14
21
  import type { CallHierarchyEntry, IncomingCall, OutgoingCall } from "./domain/call-hierarchy.ts";
15
22
  import type { Diagnostic } from "./domain/diagnostic.ts";
@@ -27,8 +34,10 @@ import { goToImplementation as goToImplementationQuery } from "./domain/go-to-im
27
34
  import type { Hover } from "./domain/hover.ts";
28
35
  import { hoverAt } from "./domain/hover-at.ts";
29
36
  import { incomingCalls as incomingCallsQuery } from "./domain/incoming-calls.ts";
37
+ import type { IntelligenceProvenance } from "./domain/intelligence-provenance.ts";
30
38
  import { descriptorForPath, LANGUAGE_SERVER_DESCRIPTORS, type LanguageServerDescriptor } from "./domain/language-server-descriptor.ts";
31
39
  import { outgoingCalls as outgoingCallsQuery } from "./domain/outgoing-calls.ts";
40
+ import type { PackageSourceBounds, PackageSourceOperationResult, PackageSourceRequest } from "./domain/package-source.ts";
32
41
  import { type PopulateSymbolGraphResult, populateSymbolGraph as populateSymbolGraphQuery } from "./domain/populate-symbol-graph.ts";
33
42
  import { prepareCallHierarchy as prepareCallHierarchyQuery } from "./domain/prepare-call-hierarchy.ts";
34
43
  import { raceWorkspaceQuery } from "./domain/race-workspace-query.ts";
@@ -36,16 +45,19 @@ import { type RawRead, rawRead, WorkspaceEntryNotFound } from "./domain/raw-read
36
45
  import { reachableSymbolsFrom } from "./domain/reachable-symbols-from.ts";
37
46
  import type { RepoFetchResult } from "./domain/repo-fetch-result.ts";
38
47
  import type { RepoReference } from "./domain/repo-reference.ts";
48
+ import { resolvePackageSource } from "./domain/resolve-package-source.ts";
39
49
  import { searchText as searchTextQuery } from "./domain/search-text.ts";
40
50
  import { symbolEdgesFrom } from "./domain/symbol-edges-from.ts";
41
51
  import { symbolEdgesTo } from "./domain/symbol-edges-to.ts";
42
52
  import type { WorkspaceCacheStatus } from "./domain/symbol-graph-generation.ts";
43
53
  import { deriveSymbolNodeId } from "./domain/symbol-node-id.ts";
54
+ import { assertBoundedSymbolQuery } from "./domain/symbol-query.ts";
44
55
  import type { TextSearchResult } from "./domain/text-search-result.ts";
45
56
  import type { WorkspaceQueryOutcome } from "./domain/workspace-query-outcome.ts";
46
- import type { WorkspaceLocation, WorkspaceSymbol } from "./domain/workspace-symbol.ts";
57
+ import type { SymbolSearchResult, WorkspaceLocation } from "./domain/workspace-symbol.ts";
47
58
  import type { CodeIntelligencePort } from "./ports/code-intelligence-port.ts";
48
59
  import type { GitPort } from "./ports/git-port.ts";
60
+ import type { PackageSourceResolverPort } from "./ports/package-source-resolver-port.ts";
49
61
  import type { RepoFetcherPort } from "./ports/repo-fetcher-port.ts";
50
62
  import type { SearchCachePort } from "./ports/search-cache-port.ts";
51
63
  import type { SymbolEdgeKind, SymbolGraphPort, SymbolNode } from "./ports/symbol-graph-port.ts";
@@ -128,6 +140,13 @@ export class RepoFetcherNotConfigured extends Error {
128
140
  }
129
141
  }
130
142
 
143
+ export class PackageSourceResolverNotConfigured extends Error {
144
+ constructor() {
145
+ super("package.resolveSource requires a service constructed with repository fetching");
146
+ this.name = "PackageSourceResolverNotConfigured";
147
+ }
148
+ }
149
+
131
150
  export class UnsupportedJobOperation extends Error {
132
151
  constructor(readonly operation: string) {
133
152
  super(`operation "${operation}" cannot run as a background job; supported operations: workspace.populateSymbolGraph`);
@@ -185,6 +204,7 @@ export type OperationName =
185
204
  | "workspace.gitLog"
186
205
  | "workspace.gitDiff"
187
206
  | "repo.fetch"
207
+ | "package.resolveSource"
188
208
  | "workspace.searchText"
189
209
  | "search.symbols"
190
210
  | "search.text"
@@ -215,6 +235,7 @@ export const OPERATION_NAMES: readonly OperationName[] = [
215
235
  "workspace.gitLog",
216
236
  "workspace.gitDiff",
217
237
  "repo.fetch",
238
+ "package.resolveSource",
218
239
  "workspace.searchText",
219
240
  "search.symbols",
220
241
  "search.text",
@@ -234,7 +255,7 @@ export interface OperationInputs {
234
255
  "workspace.rawRead": { workspaceId: WorkspaceId; path: string };
235
256
  "workspace.exactEdit": { workspaceId: WorkspaceId } & ExpectedHashEdit;
236
257
  "workspace.registerPath": { path: string };
237
- "workspace.findSymbols": { workspaceId: WorkspaceId; query: string; seedFile?: string };
258
+ "workspace.findSymbols": { workspaceId: WorkspaceId; query: string; seedFile?: string; maxResults?: number };
238
259
  "workspace.goToDefinition": WorkspacePosition;
239
260
  "workspace.goToImplementation": WorkspacePosition;
240
261
  "workspace.findReferences": WorkspacePosition & { includeDeclaration: boolean };
@@ -254,6 +275,7 @@ export interface OperationInputs {
254
275
  "workspace.gitLog": { workspaceId: WorkspaceId; maxCount: number };
255
276
  "workspace.gitDiff": { workspaceId: WorkspaceId; ref?: string; maxBytes: number };
256
277
  "repo.fetch": RepoReference;
278
+ "package.resolveSource": { request: PackageSourceRequest; bounds: PackageSourceBounds };
257
279
  "workspace.searchText": { workspaceId: WorkspaceId; query: string; maxMatches: number; maxBytes: number };
258
280
  /**
259
281
  * No single workspaceId -- fans out across several at once, unlike every other findSymbols-
@@ -275,20 +297,22 @@ export interface OperationInputs {
275
297
  "job.status": { jobId: string };
276
298
  }
277
299
 
300
+ type Provenanced<T> = T & { readonly provenance: IntelligenceProvenance };
301
+
278
302
  export interface OperationOutputs {
279
303
  "workspace.rawRead": RawRead;
280
304
  "workspace.exactEdit": EditOutcome;
281
305
  "workspace.registerPath": { workspaceId: WorkspaceId; created: boolean };
282
- "workspace.findSymbols": { symbols: readonly WorkspaceSymbol[] };
283
- "workspace.goToDefinition": { locations: readonly WorkspaceLocation[] };
284
- "workspace.goToImplementation": { locations: readonly WorkspaceLocation[] };
285
- "workspace.findReferences": { locations: readonly WorkspaceLocation[] };
286
- "workspace.hover": { hover: Hover | undefined };
287
- "workspace.documentSymbols": { symbols: readonly DocumentSymbolEntry[] };
288
- "workspace.diagnostics": { diagnostics: readonly Diagnostic[] };
289
- "workspace.prepareCallHierarchy": { items: readonly CallHierarchyEntry[] };
290
- "workspace.incomingCalls": { calls: readonly IncomingCall[] };
291
- "workspace.outgoingCalls": { calls: readonly OutgoingCall[] };
306
+ "workspace.findSymbols": SymbolSearchResult;
307
+ "workspace.goToDefinition": Provenanced<{ locations: readonly WorkspaceLocation[] }>;
308
+ "workspace.goToImplementation": Provenanced<{ locations: readonly WorkspaceLocation[] }>;
309
+ "workspace.findReferences": Provenanced<{ locations: readonly WorkspaceLocation[] }>;
310
+ "workspace.hover": Provenanced<{ hover: Hover | undefined }>;
311
+ "workspace.documentSymbols": Provenanced<{ symbols: readonly DocumentSymbolEntry[] }>;
312
+ "workspace.diagnostics": Provenanced<{ diagnostics: readonly Diagnostic[] }>;
313
+ "workspace.prepareCallHierarchy": Provenanced<{ items: readonly CallHierarchyEntry[] }>;
314
+ "workspace.incomingCalls": Provenanced<{ calls: readonly IncomingCall[] }>;
315
+ "workspace.outgoingCalls": Provenanced<{ calls: readonly OutgoingCall[] }>;
292
316
  "workspace.populateSymbolGraph": { filesProcessed: number; symbolsProcessed: number; nodesAdded: number; edgesAdded: number };
293
317
  "workspace.reachableFrom": { symbols: readonly SymbolNode[] };
294
318
  "workspace.symbolEdgesFrom": { symbols: readonly SymbolNode[] };
@@ -299,8 +323,9 @@ export interface OperationOutputs {
299
323
  "workspace.gitLog": { entries: readonly GitLogEntry[] };
300
324
  "workspace.gitDiff": GitDiffResult;
301
325
  "repo.fetch": RepoFetchResult & { workspaceId: WorkspaceId };
326
+ "package.resolveSource": PackageSourceOperationResult;
302
327
  "workspace.searchText": TextSearchResult;
303
- "search.symbols": { results: readonly WorkspaceQueryOutcome<{ symbols: readonly WorkspaceSymbol[] }>[] };
328
+ "search.symbols": { results: readonly WorkspaceQueryOutcome<SymbolSearchResult>[] };
304
329
  "search.text": { results: readonly WorkspaceQueryOutcome<TextSearchResult>[] };
305
330
  "job.submit": { job: JobSnapshot<PopulateSymbolGraphResult> };
306
331
  "job.status": { job: JobSnapshot<PopulateSymbolGraphResult> };
@@ -366,6 +391,8 @@ export interface LectorServiceOptions {
366
391
  createGitPort?: (rootPath: string) => GitPort;
367
392
  /** Factory for the port backing repo.fetch. No default -- unlike createSymbolGraph's safe in-memory fallback, fetching a real external repo always needs a real disk location only a host (daemon.ts) can supply. Called once at construction and reused, not per-call. */
368
393
  createRepoFetcher?: () => RepoFetcherPort;
394
+ /** Override package-source resolution. With a repo fetcher configured, the default composes npm lockfiles, registry metadata, and exact Git fetching. */
395
+ createPackageSourceResolver?: () => PackageSourceResolverPort;
369
396
  /** Factory for the port backing workspace.searchText. Defaults to RipgrepTextSearch -- cheap to construct, no disk dependency, safe like createGitPort's default. Called once at construction and reused. */
370
397
  createTextSearch?: () => TextSearchPort;
371
398
  /** Factory for workspace.searchText's result cache. Defaults to an in-memory-only InMemorySearchCache -- safe, no disk dependency. A host wanting the disk-backed tier too (surviving a restart) supplies a TieredSearchCache here. Called once at construction and reused. */
@@ -382,7 +409,7 @@ function resolveWorkspace(registry: MutableRegistry, workspaceId: WorkspaceId):
382
409
 
383
410
  /** True when a warm SymbolIndexPort is also a real CodeIntelligencePort (currently: any LspSymbolIndex, never TreeSitterSymbolIndex). */
384
411
  function supportsCodeIntelligence(index: SymbolIndexPort): index is SymbolIndexPort & CodeIntelligencePort {
385
- return typeof (index as Partial<CodeIntelligencePort>).goToDefinition === "function";
412
+ return "goToDefinition" in index && typeof index.goToDefinition === "function";
386
413
  }
387
414
 
388
415
  type OperationHandlers = {
@@ -418,6 +445,7 @@ async function registerPath(registry: MutableRegistry, input: OperationInputs["w
418
445
  */
419
446
  const DEFAULT_CROSS_WORKSPACE_TIMEOUT_MS = 3000;
420
447
  const MAX_INITIAL_JOB_WAIT_MS = 30_000;
448
+ const MAX_SYMBOL_RESULTS = 5_000;
421
449
  const MAX_SOURCE_MANIFEST_BYTES = 50 * 1024 * 1024;
422
450
 
423
451
  /**
@@ -455,7 +483,11 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
455
483
  const symbolIndexes = new Map<string, { index: ClosableSymbolIndex; workspaceId: WorkspaceId; lastUsedAt: number }>();
456
484
  const createSymbolIndex =
457
485
  options.createSymbolIndex ??
458
- ((rootPath: string, descriptor: LanguageServerDescriptor, seedFile?: string) => new LspSymbolIndex(rootPath, descriptor, seedFile));
486
+ ((rootPath: string, descriptor: LanguageServerDescriptor, seedFile?: string) => {
487
+ const semantic = new LspSymbolIndex(rootPath, descriptor, seedFile);
488
+ if (descriptor.languageId !== "typescript") return semantic;
489
+ return new FallbackCodeIntelligenceIndex(semantic, [new TypeScriptCompilerSymbolIndex(rootPath), new TreeSitterSymbolIndex(rootPath)]);
490
+ });
459
491
  function symbolIndexKey(workspaceId: WorkspaceId, languageId: string): string {
460
492
  return `${workspaceId}:${languageId}`;
461
493
  }
@@ -481,6 +513,11 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
481
513
  // every time, wastefully, and would risk losing the in-memory LRU's recency ordering
482
514
  // between calls for no benefit (the index itself is what makes rehydration correct at all).
483
515
  const repoFetcher = options.createRepoFetcher?.();
516
+ const packageSourceResolver =
517
+ options.createPackageSourceResolver?.() ??
518
+ (repoFetcher
519
+ ? new NpmPackageSourceResolver({ versions: new NpmLockfileVersionResolver(), registry: new NpmRegistryClient(), repositories: repoFetcher })
520
+ : undefined);
484
521
  const textSearch = options.createTextSearch?.() ?? new RipgrepTextSearch();
485
522
  const searchCache = options.createSearchCache?.() ?? new InMemorySearchCache();
486
523
 
@@ -521,6 +558,28 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
521
558
  return { workspaceId, ...result };
522
559
  }
523
560
 
561
+ async function packageSourceHandler(
562
+ registry: MutableRegistry,
563
+ input: OperationInputs["package.resolveSource"],
564
+ ): Promise<OperationOutputs["package.resolveSource"]> {
565
+ if (!packageSourceResolver) throw new PackageSourceResolverNotConfigured();
566
+ const outcome = await resolvePackageSource(packageSourceResolver, input.request, input.bounds);
567
+ if (outcome.status !== "verified") return { outcome, workspaceId: null };
568
+ const absolutePath = resolve(outcome.workspace.cachePath);
569
+ let sourceStats: Awaited<ReturnType<typeof stat>>;
570
+ try {
571
+ sourceStats = await stat(absolutePath);
572
+ } catch {
573
+ throw new InvalidWorkspaceRoot(absolutePath, "verified package source does not exist or is not accessible");
574
+ }
575
+ if (!sourceStats.isDirectory()) throw new InvalidWorkspaceRoot(absolutePath, "verified package source is not a directory");
576
+ const workspaceId = deriveWorkspaceId(absolutePath);
577
+ if (!registry.has(workspaceId)) {
578
+ registry.set(workspaceId, { port: new ReadOnlyWorkspace(new LocalFilesystemWorkspace(absolutePath)), rootPath: absolutePath, origin: "remote" });
579
+ }
580
+ return { outcome, workspaceId };
581
+ }
582
+
524
583
  async function searchTextHandler(
525
584
  registry: MutableRegistry,
526
585
  input: OperationInputs["workspace.searchText"],
@@ -619,6 +678,18 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
619
678
  return { descriptor: discovered.descriptor, seedFile: discovered.seedFile };
620
679
  }
621
680
 
681
+ function ensureLanguageIndex(workspaceId: WorkspaceId, rootPath: string, descriptor: LanguageServerDescriptor, seedFile?: string): ClosableSymbolIndex {
682
+ const key = symbolIndexKey(workspaceId, descriptor.languageId);
683
+ let entryIndex = symbolIndexes.get(key);
684
+ if (!entryIndex) {
685
+ entryIndex = { index: createSymbolIndex(rootPath, descriptor, seedFile), workspaceId, lastUsedAt: Date.now() };
686
+ symbolIndexes.set(key, entryIndex);
687
+ } else {
688
+ entryIndex.lastUsedAt = Date.now();
689
+ }
690
+ return entryIndex.index;
691
+ }
692
+
622
693
  async function ensureWarmIndex(input: {
623
694
  workspaceId: WorkspaceId;
624
695
  path?: string;
@@ -627,18 +698,45 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
627
698
  const entry = registry.get(input.workspaceId);
628
699
  if (!entry) throw new UnknownWorkspace(input.workspaceId);
629
700
  if (!entry.rootPath) throw new SymbolQueryUnavailable(input.workspaceId);
630
- const rootPath = entry.rootPath;
701
+ const { descriptor, seedFile } = resolveDescriptor(entry.rootPath, input);
702
+ return { index: ensureLanguageIndex(input.workspaceId, entry.rootPath, descriptor, seedFile), descriptor };
703
+ }
631
704
 
632
- const { descriptor, seedFile } = resolveDescriptor(rootPath, input);
633
- const key = symbolIndexKey(input.workspaceId, descriptor.languageId);
634
- let entryIndex = symbolIndexes.get(key);
635
- if (!entryIndex) {
636
- entryIndex = { index: createSymbolIndex(rootPath, descriptor, seedFile), workspaceId: input.workspaceId, lastUsedAt: Date.now() };
637
- symbolIndexes.set(key, entryIndex);
705
+ function ensureWorkspaceIndex(
706
+ workspaceId: WorkspaceId,
707
+ preferredSeedFile?: string,
708
+ ): {
709
+ index: SymbolIndexPort;
710
+ descriptors: readonly LanguageServerDescriptor[];
711
+ sources: readonly IntelligenceProvenance[];
712
+ } {
713
+ const entry = registry.get(workspaceId);
714
+ if (!entry) throw new UnknownWorkspace(workspaceId);
715
+ if (!entry.rootPath) throw new SymbolQueryUnavailable(workspaceId);
716
+ const rootPath = entry.rootPath;
717
+ const preferredDescriptor = preferredSeedFile ? descriptorForPath(preferredSeedFile) : undefined;
718
+ if (preferredSeedFile && !preferredDescriptor) throw new UnsupportedLanguage(preferredSeedFile);
719
+ const discovered = [...discoverWorkspaceDescriptors(rootPath, LANGUAGE_SERVER_DESCRIPTORS)];
720
+ if (preferredDescriptor && preferredSeedFile && !discovered.some(({ descriptor }) => descriptor.languageId === preferredDescriptor.languageId)) {
721
+ discovered.push({ descriptor: preferredDescriptor, seedFile: preferredSeedFile });
722
+ }
723
+ if (discovered.length === 0) throw new UnsupportedLanguage(rootPath);
724
+ const indexes = discovered.map(({ descriptor, seedFile }) => ({
725
+ descriptor,
726
+ index: ensureLanguageIndex(workspaceId, rootPath, descriptor, preferredDescriptor?.languageId === descriptor.languageId ? preferredSeedFile : seedFile),
727
+ }));
728
+ const first = indexes[0];
729
+ let index: SymbolIndexPort;
730
+ if (indexes.length === 1 && first) {
731
+ index = first.index;
638
732
  } else {
639
- entryIndex.lastUsedAt = Date.now();
733
+ index = new PolyglotCodeIntelligenceIndex(indexes);
640
734
  }
641
- return { index: entryIndex.index, descriptor };
735
+ return { index, descriptors: discovered.map(({ descriptor }) => descriptor), sources: indexes.map(({ index: source }) => source.provenance) };
736
+ }
737
+
738
+ function workspaceSourceExtensions(descriptors: readonly LanguageServerDescriptor[]): readonly string[] {
739
+ return Array.from(new Set(descriptors.flatMap((descriptor) => descriptor.extensions)));
642
740
  }
643
741
 
644
742
  /** Never spawns -- a caller deciding whether to enrich a result with LSP-backed info must not pay a cold-start cost just to check. With a path, checks that file's own language; without one, whether anything is warm for the workspace at all. */
@@ -660,9 +758,13 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
660
758
  }
661
759
 
662
760
  async function findSymbols(_registry: MutableRegistry, input: OperationInputs["workspace.findSymbols"]): Promise<OperationOutputs["workspace.findSymbols"]> {
663
- const { index } = await ensureWarmIndex(input);
664
- const symbols = await findWorkspaceSymbols(index, input.query);
665
- return { symbols };
761
+ assertBoundedSymbolQuery(input.query);
762
+ const maxResults = input.maxResults ?? 1_000;
763
+ if (!Number.isSafeInteger(maxResults) || maxResults < 1 || maxResults > MAX_SYMBOL_RESULTS) {
764
+ throw new TypeError(`maxResults must be a positive safe integer no greater than ${MAX_SYMBOL_RESULTS}`);
765
+ }
766
+ const { index } = ensureWorkspaceIndex(input.workspaceId, input.seedFile);
767
+ return findWorkspaceSymbols(index, input.query, { maxResults });
666
768
  }
667
769
 
668
770
  async function requireCodeIntelligence(input: {
@@ -681,7 +783,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
681
783
  ): Promise<OperationOutputs["workspace.goToDefinition"]> {
682
784
  const { index } = await requireCodeIntelligence(input);
683
785
  const locations = await goToDefinitionQuery(index, { path: input.path, line: input.line, character: input.character });
684
- return { locations };
786
+ return { locations, provenance: index.provenance };
685
787
  }
686
788
 
687
789
  async function goToImplementation(
@@ -690,7 +792,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
690
792
  ): Promise<OperationOutputs["workspace.goToImplementation"]> {
691
793
  const { index } = await requireCodeIntelligence(input);
692
794
  const locations = await goToImplementationQuery(index, { path: input.path, line: input.line, character: input.character });
693
- return { locations };
795
+ return { locations, provenance: index.provenance };
694
796
  }
695
797
 
696
798
  async function findReferences(
@@ -699,13 +801,13 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
699
801
  ): Promise<OperationOutputs["workspace.findReferences"]> {
700
802
  const { index } = await requireCodeIntelligence(input);
701
803
  const locations = await findReferencesQuery(index, { path: input.path, line: input.line, character: input.character }, input.includeDeclaration);
702
- return { locations };
804
+ return { locations, provenance: index.provenance };
703
805
  }
704
806
 
705
807
  async function hover(_registry: MutableRegistry, input: OperationInputs["workspace.hover"]): Promise<OperationOutputs["workspace.hover"]> {
706
808
  const { index } = await requireCodeIntelligence(input);
707
809
  const hover = await hoverAt(index, { path: input.path, line: input.line, character: input.character });
708
- return { hover };
810
+ return { hover, provenance: index.provenance };
709
811
  }
710
812
 
711
813
  async function documentSymbolsHandler(
@@ -714,7 +816,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
714
816
  ): Promise<OperationOutputs["workspace.documentSymbols"]> {
715
817
  const { index } = await requireCodeIntelligence(input);
716
818
  const symbols = await documentSymbolsQuery(index, input.path);
717
- return { symbols };
819
+ return { symbols, provenance: index.provenance };
718
820
  }
719
821
 
720
822
  async function diagnosticsHandler(
@@ -723,7 +825,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
723
825
  ): Promise<OperationOutputs["workspace.diagnostics"]> {
724
826
  const { index } = await requireCodeIntelligence(input);
725
827
  const diagnostics = await diagnosticsQuery(index, input.path);
726
- return { diagnostics };
828
+ return { diagnostics, provenance: index.provenance };
727
829
  }
728
830
 
729
831
  async function prepareCallHierarchyHandler(
@@ -732,7 +834,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
732
834
  ): Promise<OperationOutputs["workspace.prepareCallHierarchy"]> {
733
835
  const { index } = await requireCodeIntelligence(input);
734
836
  const items = await prepareCallHierarchyQuery(index, { path: input.path, line: input.line, character: input.character });
735
- return { items };
837
+ return { items, provenance: index.provenance };
736
838
  }
737
839
 
738
840
  async function incomingCallsHandler(
@@ -741,7 +843,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
741
843
  ): Promise<OperationOutputs["workspace.incomingCalls"]> {
742
844
  const { index } = await requireCodeIntelligence(input);
743
845
  const calls = await incomingCallsQuery(index, { path: input.path, line: input.line, character: input.character });
744
- return { calls };
846
+ return { calls, provenance: index.provenance };
745
847
  }
746
848
 
747
849
  async function outgoingCallsHandler(
@@ -750,27 +852,31 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
750
852
  ): Promise<OperationOutputs["workspace.outgoingCalls"]> {
751
853
  const { index } = await requireCodeIntelligence(input);
752
854
  const calls = await outgoingCallsQuery(index, { path: input.path, line: input.line, character: input.character });
753
- return { calls };
855
+ return { calls, provenance: index.provenance };
754
856
  }
755
857
 
756
858
  async function populateSymbolGraphHandler(
757
859
  _registry: MutableRegistry,
758
860
  input: OperationInputs["workspace.populateSymbolGraph"],
759
861
  ): Promise<OperationOutputs["workspace.populateSymbolGraph"]> {
760
- const { index, descriptor } = await requireCodeIntelligence(input);
862
+ const workspaceIndex = ensureWorkspaceIndex(input.workspaceId);
863
+ if (!supportsCodeIntelligence(workspaceIndex.index)) throw new CodeIntelligenceUnavailable(input.workspaceId);
761
864
  const entry = registry.get(input.workspaceId);
762
865
  if (!entry?.rootPath) throw new SymbolQueryUnavailable(input.workspaceId);
763
866
  const rootPath = entry.rootPath;
764
- const before = await deriveSourceManifest(rootPath, descriptor.extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES);
867
+ const extensions = workspaceSourceExtensions(workspaceIndex.descriptors);
868
+ const before = await deriveSourceManifest(rootPath, extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES);
765
869
  const graph = ensureSymbolGraph(input.workspaceId);
766
- const result = await populateSymbolGraphQuery(index, graph, before.absoluteFiles, input.maxSymbolsPerFile);
767
- const after = await deriveSourceManifest(rootPath, descriptor.extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES);
870
+ const result = await populateSymbolGraphQuery(workspaceIndex.index, graph, before.absoluteFiles, input.maxSymbolsPerFile);
871
+ const after = await deriveSourceManifest(rootPath, extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES);
768
872
  if (after.fingerprint !== before.fingerprint) throw new WorkspaceChangedDuringPopulation(input.workspaceId);
769
873
  await graph.setGeneration({
770
874
  sourceFingerprint: after.fingerprint,
771
875
  maxFiles: input.maxFiles,
772
876
  maxSymbolsPerFile: input.maxSymbolsPerFile,
773
877
  completedAt: Date.now(),
878
+ provenance: workspaceIndex.index.provenance,
879
+ sources: workspaceIndex.sources,
774
880
  result,
775
881
  });
776
882
  return result;
@@ -795,10 +901,12 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
795
901
  if (generation.maxFiles !== input.maxFiles || generation.maxSymbolsPerFile !== input.maxSymbolsPerFile) {
796
902
  return { status: "not-cached", reason: "bounds-changed" };
797
903
  }
798
- const { descriptor } = resolveDescriptor(entry.rootPath, {});
904
+ const discovered = discoverWorkspaceDescriptors(entry.rootPath, LANGUAGE_SERVER_DESCRIPTORS);
905
+ if (discovered.length === 0) return { status: "not-cached", reason: "source-changed" };
906
+ const extensions = workspaceSourceExtensions(discovered.map(({ descriptor }) => descriptor));
799
907
  let currentFingerprint: string;
800
908
  try {
801
- currentFingerprint = (await deriveSourceManifest(entry.rootPath, descriptor.extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES)).fingerprint;
909
+ currentFingerprint = (await deriveSourceManifest(entry.rootPath, extensions, input.maxFiles, MAX_SOURCE_MANIFEST_BYTES)).fingerprint;
802
910
  } catch {
803
911
  return { status: "not-cached", reason: "source-changed" };
804
912
  }
@@ -920,6 +1028,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
920
1028
  "workspace.gitLog": gitLogHandler,
921
1029
  "workspace.gitDiff": gitDiffHandler,
922
1030
  "repo.fetch": repoFetchHandler,
1031
+ "package.resolveSource": packageSourceHandler,
923
1032
  "workspace.searchText": searchTextHandler,
924
1033
  "search.symbols": crossFindSymbols,
925
1034
  "search.text": crossSearchText,