@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/cli.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { mkdirSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
- import { dirname, join } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { InMemoryWorkspace } from "./adapters/in-memory-workspace.ts";
8
8
  import { LocalFilesystemWorkspace } from "./adapters/local-filesystem-workspace.ts";
@@ -11,6 +11,8 @@ import { LECTOR_PATH_NAMES } from "./constants.ts";
11
11
  import { serveMain } from "./daemon.ts";
12
12
  import type { JobSnapshot } from "./domain/bounded-job-executor.ts";
13
13
  import type { ContentHash } from "./domain/content-hash.ts";
14
+ import { DEFAULT_PACKAGE_SOURCE_BOUNDS, type PackageSourceOperationResult } from "./domain/package-source.ts";
15
+ import type { SymbolSearchResult } from "./domain/workspace-symbol.ts";
14
16
  import type { WorkspacePort } from "./ports/workspace-port.ts";
15
17
  import type { WorkspaceId } from "./service.ts";
16
18
 
@@ -51,6 +53,8 @@ const USAGE = `Usage:
51
53
  lector workspace git-diff <workspace-id> [--ref <ref>] --max-bytes <n> [--json]
52
54
  lector workspace repo-fetch <owner>/<repo>[@ref] [--host <host>] [--json]
53
55
  shallow-clones an external repo into a disk-bounded cache and registers it read-only
56
+ lector package source <project-dir> <package-name> [--version <exact-version>] [--registry <url>] [--json]
57
+ resolves an installed npm package to verified exact repository source and registers it read-only
54
58
  lector workspace search-text <workspace-id> <query> --max-matches <n> --max-bytes <n> [--json]
55
59
  lector search symbols <query> [--workspace <id>]... [--timeout-ms <n>] [--json]
56
60
  lector search text <query> --max-matches <n> --max-bytes <n> [--workspace <id>]... [--timeout-ms <n>] [--json]
@@ -127,15 +131,26 @@ async function runWorkspaceRegister(dir: string | undefined, flags: string[]): P
127
131
  console.log(hasFlag(flags, "--json") ? JSON.stringify(result) : `${result.workspaceId} (${result.created ? "created" : "already registered"})`);
128
132
  }
129
133
 
134
+ function formatSymbolSources(result: SymbolSearchResult): readonly string[] {
135
+ return (result.sources ?? []).map((source) => {
136
+ const identity = `${source.provenance.languageId}: ${source.status} via ${source.provenance.backend}`;
137
+ if (source.status === "failed") return source.error ? `${identity} [${source.error.code}] ${source.error.message}` : identity;
138
+ return `${identity} (${source.symbolCount} symbol${source.symbolCount === 1 ? "" : "s"}${source.truncated ? ", truncated" : ""})`;
139
+ });
140
+ }
141
+
130
142
  async function runWorkspaceSymbols(workspaceId: string | undefined, query: string | undefined, flags: string[]): Promise<void> {
131
143
  if (!workspaceId || !query) fail(USAGE);
132
144
  const seedFile = flagValue(flags, "--seed-file"); // omit to auto-discover one
133
145
  const client = await connectLectorClient();
134
- const { symbols } = await client.call("workspace.findSymbols", { workspaceId, query, seedFile });
146
+ const result = await client.call("workspace.findSymbols", { workspaceId, query, seedFile });
135
147
  if (hasFlag(flags, "--json")) {
136
- console.log(JSON.stringify(symbols));
148
+ console.log(JSON.stringify(result));
137
149
  return;
138
150
  }
151
+ const { symbols, provenance, truncated } = result;
152
+ console.log(`${provenance.fidelity} via ${provenance.backend}${truncated ? " (truncated)" : ""}`);
153
+ for (const source of formatSymbolSources(result)) console.log(source);
139
154
  if (symbols.length === 0) {
140
155
  console.log(`no symbols matched "${query}"`);
141
156
  return;
@@ -154,16 +169,22 @@ function parsePosition(line: string | undefined, character: string | undefined):
154
169
  return { line: parsedLine, character: parsedCharacter };
155
170
  }
156
171
 
172
+ function formatIntelligenceSource(provenance: { fidelity: string; backend: string }): string {
173
+ return `${provenance.fidelity} via ${provenance.backend}`;
174
+ }
175
+
157
176
  async function runWorkspaceDefinition(workspaceId: string | undefined, path: string | undefined, rest: string[]): Promise<void> {
158
177
  if (!workspaceId || !path) fail(USAGE);
159
178
  const [lineArg, characterArg, ...flags] = rest;
160
179
  const { line, character } = parsePosition(lineArg, characterArg);
161
180
  const client = await connectLectorClient();
162
- const { locations } = await client.call("workspace.goToDefinition", { workspaceId, path, line, character });
181
+ const result = await client.call("workspace.goToDefinition", { workspaceId, path, line, character });
163
182
  if (hasFlag(flags, "--json")) {
164
- console.log(JSON.stringify(locations));
183
+ console.log(JSON.stringify(result));
165
184
  return;
166
185
  }
186
+ const { locations, provenance } = result;
187
+ console.log(formatIntelligenceSource(provenance));
167
188
  if (locations.length === 0) {
168
189
  console.log("no definition found");
169
190
  return;
@@ -176,11 +197,13 @@ async function runWorkspaceImplementation(workspaceId: string | undefined, path:
176
197
  const [lineArg, characterArg, ...flags] = rest;
177
198
  const { line, character } = parsePosition(lineArg, characterArg);
178
199
  const client = await connectLectorClient();
179
- const { locations } = await client.call("workspace.goToImplementation", { workspaceId, path, line, character });
200
+ const result = await client.call("workspace.goToImplementation", { workspaceId, path, line, character });
180
201
  if (hasFlag(flags, "--json")) {
181
- console.log(JSON.stringify(locations));
202
+ console.log(JSON.stringify(result));
182
203
  return;
183
204
  }
205
+ const { locations, provenance } = result;
206
+ console.log(formatIntelligenceSource(provenance));
184
207
  if (locations.length === 0) {
185
208
  console.log("no implementation found");
186
209
  return;
@@ -194,11 +217,13 @@ async function runWorkspaceReferences(workspaceId: string | undefined, path: str
194
217
  const { line, character } = parsePosition(lineArg, characterArg);
195
218
  const includeDeclaration = hasFlag(flags, "--include-declaration");
196
219
  const client = await connectLectorClient();
197
- const { locations } = await client.call("workspace.findReferences", { workspaceId, path, line, character, includeDeclaration });
220
+ const result = await client.call("workspace.findReferences", { workspaceId, path, line, character, includeDeclaration });
198
221
  if (hasFlag(flags, "--json")) {
199
- console.log(JSON.stringify(locations));
222
+ console.log(JSON.stringify(result));
200
223
  return;
201
224
  }
225
+ const { locations, provenance } = result;
226
+ console.log(formatIntelligenceSource(provenance));
202
227
  if (locations.length === 0) {
203
228
  console.log("no references found");
204
229
  return;
@@ -211,22 +236,25 @@ async function runWorkspaceHover(workspaceId: string | undefined, path: string |
211
236
  const [lineArg, characterArg, ...flags] = rest;
212
237
  const { line, character } = parsePosition(lineArg, characterArg);
213
238
  const client = await connectLectorClient();
214
- const { hover } = await client.call("workspace.hover", { workspaceId, path, line, character });
239
+ const result = await client.call("workspace.hover", { workspaceId, path, line, character });
215
240
  if (hasFlag(flags, "--json")) {
216
- console.log(JSON.stringify(hover ?? null));
241
+ console.log(JSON.stringify(result));
217
242
  return;
218
243
  }
219
- console.log(hover ? hover.contents : "no hover information available");
244
+ console.log(formatIntelligenceSource(result.provenance));
245
+ console.log(result.hover ? result.hover.contents : "no hover information available");
220
246
  }
221
247
 
222
248
  async function runWorkspaceDocumentSymbols(workspaceId: string | undefined, path: string | undefined, flags: string[]): Promise<void> {
223
249
  if (!workspaceId || !path) fail(USAGE);
224
250
  const client = await connectLectorClient();
225
- const { symbols } = await client.call("workspace.documentSymbols", { workspaceId, path });
251
+ const result = await client.call("workspace.documentSymbols", { workspaceId, path });
226
252
  if (hasFlag(flags, "--json")) {
227
- console.log(JSON.stringify(symbols));
253
+ console.log(JSON.stringify(result));
228
254
  return;
229
255
  }
256
+ const { symbols, provenance } = result;
257
+ console.log(formatIntelligenceSource(provenance));
230
258
  if (symbols.length === 0) {
231
259
  console.log("no symbols found");
232
260
  return;
@@ -241,11 +269,13 @@ async function runWorkspaceDocumentSymbols(workspaceId: string | undefined, path
241
269
  async function runWorkspaceDiagnostics(workspaceId: string | undefined, path: string | undefined, flags: string[]): Promise<void> {
242
270
  if (!workspaceId || !path) fail(USAGE);
243
271
  const client = await connectLectorClient();
244
- const { diagnostics } = await client.call("workspace.diagnostics", { workspaceId, path });
272
+ const result = await client.call("workspace.diagnostics", { workspaceId, path });
245
273
  if (hasFlag(flags, "--json")) {
246
- console.log(JSON.stringify(diagnostics));
274
+ console.log(JSON.stringify(result));
247
275
  return;
248
276
  }
277
+ const { diagnostics, provenance } = result;
278
+ console.log(formatIntelligenceSource(provenance));
249
279
  if (diagnostics.length === 0) {
250
280
  console.log("no diagnostics");
251
281
  return;
@@ -273,11 +303,13 @@ async function runWorkspaceCallHierarchy(
273
303
  const client = await connectLectorClient();
274
304
 
275
305
  if (subcommand === "prepare") {
276
- const { items } = await client.call("workspace.prepareCallHierarchy", { workspaceId, path, line, character });
306
+ const result = await client.call("workspace.prepareCallHierarchy", { workspaceId, path, line, character });
277
307
  if (hasFlag(flags, "--json")) {
278
- console.log(JSON.stringify(items));
308
+ console.log(JSON.stringify(result));
279
309
  return;
280
310
  }
311
+ const { items, provenance } = result;
312
+ console.log(formatIntelligenceSource(provenance));
281
313
  if (items.length === 0) {
282
314
  console.log("no call-hierarchy root at this position");
283
315
  return;
@@ -286,11 +318,13 @@ async function runWorkspaceCallHierarchy(
286
318
  return;
287
319
  }
288
320
  if (subcommand === "incoming") {
289
- const { calls } = await client.call("workspace.incomingCalls", { workspaceId, path, line, character });
321
+ const result = await client.call("workspace.incomingCalls", { workspaceId, path, line, character });
290
322
  if (hasFlag(flags, "--json")) {
291
- console.log(JSON.stringify(calls));
323
+ console.log(JSON.stringify(result));
292
324
  return;
293
325
  }
326
+ const { calls, provenance } = result;
327
+ console.log(formatIntelligenceSource(provenance));
294
328
  if (calls.length === 0) {
295
329
  console.log("no incoming calls found");
296
330
  return;
@@ -299,11 +333,13 @@ async function runWorkspaceCallHierarchy(
299
333
  return;
300
334
  }
301
335
  if (subcommand === "outgoing") {
302
- const { calls } = await client.call("workspace.outgoingCalls", { workspaceId, path, line, character });
336
+ const result = await client.call("workspace.outgoingCalls", { workspaceId, path, line, character });
303
337
  if (hasFlag(flags, "--json")) {
304
- console.log(JSON.stringify(calls));
338
+ console.log(JSON.stringify(result));
305
339
  return;
306
340
  }
341
+ const { calls, provenance } = result;
342
+ console.log(formatIntelligenceSource(provenance));
307
343
  if (calls.length === 0) {
308
344
  console.log("no outgoing calls found");
309
345
  return;
@@ -454,6 +490,38 @@ async function runWorkspaceRepoFetch(spec: string | undefined, flags: string[]):
454
490
  if (result.refFallbackOccurred) console.log(`note: requested ref not found, fell back to the default branch (resolved: ${result.resolvedRef})`);
455
491
  }
456
492
 
493
+ function formatPackageSourceResult(result: PackageSourceOperationResult): string {
494
+ const { outcome } = result;
495
+ if (outcome.status === "verified") {
496
+ return `${result.workspaceId ?? "unregistered"} ${outcome.coordinate.name}@${outcome.coordinate.resolvedVersion} -- ${outcome.workspace.cachePath}\n${outcome.repository.url ?? "local source"}@${outcome.repository.resolvedRef ?? "local"} ${outcome.repository.commit ?? outcome.verification.integrity}`;
497
+ }
498
+ if (outcome.status === "ambiguous") {
499
+ return `ambiguous [${outcome.code}] -- ${outcome.candidates.map((candidate) => `${candidate.version} (${candidate.source})`).join(", ")}${outcome.truncated ? ", …" : ""}`;
500
+ }
501
+ if (outcome.status === "unauthenticated") return `unauthenticated [${outcome.code}] -- configure ${outcome.requiredCredentialNames.join(", ")}`;
502
+ if (outcome.status === "oversized") return `oversized [${outcome.code}] -- ${outcome.resource} exceeded ${outcome.limit}`;
503
+ if (outcome.status === "mismatched") return `mismatched [${outcome.code}] -- expected ${outcome.expected}, got ${outcome.actual}`;
504
+ return `unavailable [${outcome.code}]`;
505
+ }
506
+
507
+ async function runPackageSource(projectDir: string | undefined, packageName: string | undefined, flags: string[]): Promise<void> {
508
+ if (!projectDir || !packageName) fail(USAGE);
509
+ const client = await connectLectorClient();
510
+ const result = await client.call("package.resolveSource", {
511
+ request: {
512
+ projectRoot: resolve(projectDir),
513
+ coordinate: {
514
+ ecosystem: "npm",
515
+ registry: flagValue(flags, "--registry") ?? null,
516
+ name: packageName,
517
+ requestedVersion: flagValue(flags, "--version") ?? null,
518
+ },
519
+ },
520
+ bounds: DEFAULT_PACKAGE_SOURCE_BOUNDS,
521
+ });
522
+ console.log(hasFlag(flags, "--json") ? JSON.stringify(result) : formatPackageSourceResult(result));
523
+ }
524
+
457
525
  async function runWorkspaceSearchText(workspaceId: string | undefined, query: string | undefined, flags: string[]): Promise<void> {
458
526
  if (!workspaceId || !query) fail(USAGE);
459
527
  const maxMatches = requiredIntFlag(flags, "--max-matches");
@@ -655,7 +723,7 @@ WantedBy=default.target
655
723
  }
656
724
 
657
725
  function unitPath(): string {
658
- const configHome = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
726
+ const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
659
727
  return join(configHome, "systemd", "user", LECTOR_PATH_NAMES.systemdUnitName);
660
728
  }
661
729
 
@@ -713,6 +781,12 @@ async function main(): Promise<void> {
713
781
  fail(USAGE);
714
782
  }
715
783
 
784
+ if (command === "package") {
785
+ const [action, projectDir, packageName, ...packageFlags] = rest;
786
+ if (action === "source") return runPackageSource(projectDir, packageName, packageFlags);
787
+ fail(USAGE);
788
+ }
789
+
716
790
  if (command === "workspace") {
717
791
  const [action, ...actionArgs] = rest;
718
792
  if (action === "register") {
package/src/daemon.ts CHANGED
@@ -9,6 +9,7 @@ import { SqliteSearchCache } from "./adapters/sqlite-search-cache.ts";
9
9
  import { SqliteSymbolGraph } from "./adapters/sqlite-symbol-graph.ts";
10
10
  import { TieredSearchCache } from "./adapters/tiered-search-cache.ts";
11
11
  import { resolveLectorPaths } from "./constants.ts";
12
+ import type { PackageSourceResolverPort } from "./ports/package-source-resolver-port.ts";
12
13
  import type { RepoFetcherPort } from "./ports/repo-fetcher-port.ts";
13
14
  import type { WorkspacePort } from "./ports/workspace-port.ts";
14
15
  import { createLectorService, type LectorService, type OperationName, type WorkspaceId } from "./service.ts";
@@ -84,6 +85,8 @@ export interface LectorDaemonOptions {
84
85
  symbolIndexReapIntervalMs?: number;
85
86
  /** Override the repo.fetch backend (tests inject a GitRepoFetcher pointed at a local fixture repo, avoiding live network). Defaults to a real GitRepoFetcher under this daemon's own data directory. */
86
87
  createRepoFetcher?: () => RepoFetcherPort;
88
+ /** Override package source resolution while retaining the authenticated daemon/client seam. */
89
+ createPackageSourceResolver?: () => PackageSourceResolverPort;
87
90
  }
88
91
 
89
92
  function prepare(options: LectorDaemonOptions): {
@@ -110,6 +113,7 @@ function prepare(options: LectorDaemonOptions): {
110
113
  allowDynamicOnly: options.allowDynamicOnly,
111
114
  createSymbolGraph: (workspaceId) => new SqliteSymbolGraph(join(symbolGraphDirectory, `${workspaceId}.db`)),
112
115
  createRepoFetcher: options.createRepoFetcher ?? (() => new GitRepoFetcher(reposDirectory)),
116
+ createPackageSourceResolver: options.createPackageSourceResolver,
113
117
  // The real production shape the SearchCachePort design was for: an in-memory tier for
114
118
  // speed plus a disk-backed tier so repeated searches survive a daemon restart -- a single
115
119
  // SearchCachePort adapter can only be one or the other, service.ts's own safe default is
@@ -1,7 +1,8 @@
1
1
  import type { SymbolIndexPort } from "../ports/symbol-index-port.ts";
2
- import type { WorkspaceSymbol } from "./workspace-symbol.ts";
2
+ import type { SymbolSearchBounds } from "./intelligence-provenance.ts";
3
+ import type { SymbolSearchResult } from "./workspace-symbol.ts";
3
4
 
4
5
  /** Find workspace symbols matching a fuzzy query string. */
5
- export async function findWorkspaceSymbols(index: SymbolIndexPort, query: string): Promise<WorkspaceSymbol[]> {
6
- return index.findSymbols(query);
6
+ export async function findWorkspaceSymbols(index: SymbolIndexPort, query: string, bounds?: SymbolSearchBounds): Promise<SymbolSearchResult> {
7
+ return index.findSymbols(query, bounds);
7
8
  }
@@ -0,0 +1,66 @@
1
+ export type JavaScriptPackageManager = "npm" | "pnpm" | "yarn" | "bun";
2
+
3
+ export interface InstalledPackageVersionRequest {
4
+ readonly projectRoot: string;
5
+ readonly packageName: string;
6
+ readonly requestedVersion: string | null;
7
+ }
8
+
9
+ export interface InstalledPackageVersionBounds {
10
+ readonly maxManifestBytes: number;
11
+ readonly maxManifestEntries: number;
12
+ readonly maxManifestNesting: number;
13
+ readonly maxWorkspaces: number;
14
+ readonly maxDiagnostics: number;
15
+ readonly maxCandidates: number;
16
+ readonly maxEvidencePerVersion: number;
17
+ }
18
+
19
+ export interface InstalledPackageEvidence {
20
+ readonly manager: JavaScriptPackageManager;
21
+ readonly lockfile: string;
22
+ readonly locator: string;
23
+ readonly integrity: string | null;
24
+ readonly workspace: boolean;
25
+ }
26
+
27
+ export interface ResolvedInstalledPackageVersion {
28
+ readonly status: "resolved";
29
+ readonly packageName: string;
30
+ readonly requestedVersion: string | null;
31
+ readonly version: string;
32
+ readonly evidence: readonly InstalledPackageEvidence[];
33
+ readonly evidenceTruncated: boolean;
34
+ }
35
+
36
+ export interface InstalledPackageVersionCandidate {
37
+ readonly version: string;
38
+ readonly evidence: readonly InstalledPackageEvidence[];
39
+ readonly evidenceTruncated: boolean;
40
+ }
41
+
42
+ export interface AmbiguousInstalledPackageVersion {
43
+ readonly status: "ambiguous";
44
+ readonly packageName: string;
45
+ readonly requestedVersion: null;
46
+ readonly candidates: readonly InstalledPackageVersionCandidate[];
47
+ readonly truncated: boolean;
48
+ }
49
+
50
+ export interface UnavailableInstalledPackageVersion {
51
+ readonly status: "unavailable";
52
+ readonly code: "lockfile-not-found" | "package-not-found" | "version-not-found" | "unsupported-lockfile" | "corrupt-lockfile";
53
+ readonly lockfile?: string;
54
+ }
55
+
56
+ export interface OversizedInstalledPackageVersion {
57
+ readonly status: "oversized";
58
+ readonly resource: "manifest-bytes" | "manifest-entries" | "manifest-nesting" | "workspaces" | "diagnostics";
59
+ readonly limit: number;
60
+ }
61
+
62
+ export type InstalledPackageVersionOutcome =
63
+ | ResolvedInstalledPackageVersion
64
+ | AmbiguousInstalledPackageVersion
65
+ | UnavailableInstalledPackageVersion
66
+ | OversizedInstalledPackageVersion;
@@ -0,0 +1,27 @@
1
+ export type IntelligenceFidelity = "semantic" | "structural";
2
+
3
+ export interface IntelligenceProvenance {
4
+ readonly fidelity: IntelligenceFidelity;
5
+ readonly backend: string;
6
+ readonly languageId: string;
7
+ readonly authority: "language-server" | "parser" | "compiler";
8
+ readonly freshness: "live-process" | "content-hash" | "filesystem-snapshot";
9
+ readonly limitations: readonly string[];
10
+ }
11
+
12
+ export interface IntelligenceSourceOutcome {
13
+ readonly provenance: IntelligenceProvenance;
14
+ readonly status: "ready" | "failed";
15
+ readonly symbolCount: number;
16
+ readonly truncated?: boolean;
17
+ readonly error?: { readonly code: string; readonly message: string };
18
+ }
19
+
20
+ export interface SymbolSearchBounds {
21
+ readonly maxResults: number;
22
+ }
23
+
24
+ export interface ProvenancedResult<T> {
25
+ readonly result: T;
26
+ readonly provenance: IntelligenceProvenance;
27
+ }
@@ -13,13 +13,18 @@ export type LanguageServerLaunch = { readonly kind: "npm-module"; readonly entry
13
13
 
14
14
  export interface LanguageServerDescriptor {
15
15
  readonly languageId: string;
16
+ readonly backendId: string;
16
17
  readonly extensions: readonly string[];
18
+ /** Per-extension document language ids when one server owns a language family. */
19
+ readonly documentLanguageIds?: Readonly<Record<string, string>>;
17
20
  readonly launch: LanguageServerLaunch;
18
21
  readonly args: readonly string[];
19
22
  /** Checked nearest-first; closest match wins over a more distant one -- a monorepo subproject with its own root marker resolves to itself, not the outer repo. */
20
23
  readonly rootMarkers: readonly string[];
21
24
  /** Tried before the bounded directory scan when picking a file to warm the server with (e.g. a language's usual entry-point names). */
22
25
  readonly commonSeedCandidates: readonly string[];
26
+ /** Excludes unsafe or unusually expensive servers from automatic workspace-wide fan-out while preserving explicit file operations. */
27
+ readonly workspaceDiscovery?: "enabled" | "explicit-only";
23
28
  /** Extra textDocument/workspace capabilities this specific server gates real features behind (e.g. typescript-language-server withholds diagnostics/callHierarchy unless declared). */
24
29
  readonly extraCapabilities?: Record<string, unknown>;
25
30
  /** Milliseconds to wait after opening a file before trusting the server's answers -- no server signals "project loaded". Default 1000ms; rust-analyzer needs more (see RUST_DESCRIPTOR). */
@@ -30,7 +35,16 @@ export const DEFAULT_SETTLE_MS = 1000;
30
35
 
31
36
  export const TYPESCRIPT_DESCRIPTOR: LanguageServerDescriptor = {
32
37
  languageId: "typescript",
38
+ backendId: "typescript-language-server",
33
39
  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
40
+ documentLanguageIds: {
41
+ ".ts": "typescript",
42
+ ".tsx": "typescriptreact",
43
+ ".js": "javascript",
44
+ ".jsx": "javascriptreact",
45
+ ".mjs": "javascript",
46
+ ".cjs": "javascript",
47
+ },
34
48
  launch: { kind: "npm-module", entryModule: "typescript-language-server/lib/cli.mjs" },
35
49
  args: ["--stdio"],
36
50
  rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
@@ -43,6 +57,7 @@ export const TYPESCRIPT_DESCRIPTOR: LanguageServerDescriptor = {
43
57
 
44
58
  export const PYTHON_DESCRIPTOR: LanguageServerDescriptor = {
45
59
  languageId: "python",
60
+ backendId: "pyright",
46
61
  extensions: [".py", ".pyi"],
47
62
  launch: { kind: "npm-module", entryModule: "pyright/langserver.index.js" },
48
63
  args: ["--stdio"],
@@ -52,6 +67,7 @@ export const PYTHON_DESCRIPTOR: LanguageServerDescriptor = {
52
67
 
53
68
  export const GO_DESCRIPTOR: LanguageServerDescriptor = {
54
69
  languageId: "go",
70
+ backendId: "gopls",
55
71
  extensions: [".go"],
56
72
  // gopls ships via `go install`, not npm.
57
73
  launch: { kind: "system-binary", command: "gopls" },
@@ -62,6 +78,7 @@ export const GO_DESCRIPTOR: LanguageServerDescriptor = {
62
78
 
63
79
  export const RUST_DESCRIPTOR: LanguageServerDescriptor = {
64
80
  languageId: "rust",
81
+ backendId: "rust-analyzer",
65
82
  extensions: [".rs"],
66
83
  // rust-analyzer ships via rustup, not npm.
67
84
  launch: { kind: "system-binary", command: "rust-analyzer" },
@@ -73,6 +90,7 @@ export const RUST_DESCRIPTOR: LanguageServerDescriptor = {
73
90
 
74
91
  export const CPP_DESCRIPTOR: LanguageServerDescriptor = {
75
92
  languageId: "cpp",
93
+ backendId: "clangd",
76
94
  extensions: [".c", ".h", ".cc", ".cpp", ".cxx", ".hh", ".hpp", ".hxx"],
77
95
  // clangd ships via LLVM's system packaging, not npm.
78
96
  launch: { kind: "system-binary", command: "clangd" },
@@ -83,7 +101,9 @@ export const CPP_DESCRIPTOR: LanguageServerDescriptor = {
83
101
 
84
102
  export const BASH_DESCRIPTOR: LanguageServerDescriptor = {
85
103
  languageId: "shellscript",
104
+ backendId: "bash-language-server",
86
105
  extensions: [".sh", ".bash"],
106
+ workspaceDiscovery: "explicit-only",
87
107
  launch: { kind: "npm-module", entryModule: "bash-language-server/out/cli.js" },
88
108
  args: ["start"],
89
109
  rootMarkers: [],
@@ -92,7 +112,9 @@ export const BASH_DESCRIPTOR: LanguageServerDescriptor = {
92
112
 
93
113
  export const YAML_DESCRIPTOR: LanguageServerDescriptor = {
94
114
  languageId: "yaml",
115
+ backendId: "yaml-language-server",
95
116
  extensions: [".yaml", ".yml"],
117
+ workspaceDiscovery: "explicit-only",
96
118
  launch: { kind: "npm-module", entryModule: "yaml-language-server/bin/yaml-language-server" },
97
119
  args: ["--stdio"],
98
120
  rootMarkers: [],
@@ -0,0 +1,26 @@
1
+ export interface NpmRegistryVersionRequest {
2
+ readonly registry: string;
3
+ readonly name: string;
4
+ readonly version: string;
5
+ }
6
+
7
+ export interface NpmRegistryBounds {
8
+ readonly maxResponseBytes: number;
9
+ readonly maxRedirects: number;
10
+ readonly maxRetries: number;
11
+ readonly timeoutMs: number;
12
+ }
13
+
14
+ export interface NpmRepositoryMetadata {
15
+ readonly type: string | null;
16
+ readonly url: string;
17
+ readonly directory: string | null;
18
+ }
19
+
20
+ export interface NpmPackageVersionMetadata {
21
+ readonly name: string;
22
+ readonly version: string;
23
+ readonly repository: NpmRepositoryMetadata | null;
24
+ readonly gitHead: string | null;
25
+ readonly integrity: string | null;
26
+ }
@@ -0,0 +1,143 @@
1
+ export type PackageEcosystem = "npm" | "pypi" | "cargo" | "go" | "maven" | "conan" | "vcpkg" | "nuget" | "swiftpm";
2
+
3
+ export interface PackageCoordinateRequest {
4
+ readonly ecosystem: PackageEcosystem;
5
+ readonly registry: string | null;
6
+ readonly name: string;
7
+ readonly requestedVersion: string | null;
8
+ }
9
+
10
+ export interface PackageSourceRequest {
11
+ readonly projectRoot: string;
12
+ readonly coordinate: PackageCoordinateRequest;
13
+ }
14
+
15
+ export interface PackageSourceBounds {
16
+ readonly maxManifestBytes: number;
17
+ readonly maxManifestEntries: number;
18
+ readonly maxManifestNesting: number;
19
+ readonly maxWorkspaces: number;
20
+ readonly maxDiagnostics: number;
21
+ readonly maxRegistryResponseBytes: number;
22
+ readonly maxRedirects: number;
23
+ readonly maxRetries: number;
24
+ readonly maxCloneBytes: number;
25
+ readonly maxCacheBytes: number;
26
+ readonly maxCandidates: number;
27
+ readonly timeoutMs: number;
28
+ }
29
+
30
+ export interface ResolvedPackageCoordinate extends PackageCoordinateRequest {
31
+ readonly resolvedVersion: string;
32
+ }
33
+
34
+ export interface PackageRepositoryIdentity {
35
+ readonly url: string | null;
36
+ readonly requestedRef: string | null;
37
+ readonly resolvedRef: string | null;
38
+ readonly commit: string | null;
39
+ }
40
+
41
+ export interface PackageSourceWorkspace {
42
+ readonly cachePath: string;
43
+ readonly origin: "local" | "fetched";
44
+ readonly readOnly: true;
45
+ }
46
+
47
+ export type PackageSourceVerificationMethod = "lockfile-vcs-pin" | "registry-metadata-and-commit" | "source-artifact-checksum" | "local-content-digest";
48
+
49
+ export interface PackageSourceVerification {
50
+ readonly status: "verified";
51
+ readonly method: PackageSourceVerificationMethod;
52
+ readonly integrity: string;
53
+ }
54
+
55
+ export interface VerifiedPackageSource {
56
+ readonly status: "verified";
57
+ readonly coordinate: ResolvedPackageCoordinate;
58
+ readonly repository: PackageRepositoryIdentity;
59
+ readonly workspace: PackageSourceWorkspace;
60
+ readonly verification: PackageSourceVerification;
61
+ }
62
+
63
+ export type PackageSourceUnavailableCode =
64
+ | "package-not-found"
65
+ | "version-not-found"
66
+ | "source-metadata-missing"
67
+ | "unsupported-ecosystem"
68
+ | "unsupported-manifest"
69
+ | "unverifiable-source";
70
+
71
+ export interface UnavailablePackageSource {
72
+ readonly status: "unavailable";
73
+ readonly code: PackageSourceUnavailableCode;
74
+ }
75
+
76
+ export interface PackageSourceCandidate {
77
+ readonly version: string;
78
+ readonly source: string;
79
+ }
80
+
81
+ export interface AmbiguousPackageSource {
82
+ readonly status: "ambiguous";
83
+ readonly code: "multiple-installed-versions" | "multiple-source-candidates";
84
+ readonly candidates: readonly PackageSourceCandidate[];
85
+ readonly truncated: boolean;
86
+ }
87
+
88
+ export interface UnauthenticatedPackageSource {
89
+ readonly status: "unauthenticated";
90
+ readonly code: "registry-authentication-required" | "repository-authentication-required";
91
+ readonly requiredCredentialNames: readonly string[];
92
+ }
93
+
94
+ export interface OversizedPackageSource {
95
+ readonly status: "oversized";
96
+ readonly code: "manifest-limit-exceeded" | "registry-response-limit-exceeded" | "clone-limit-exceeded" | "cache-limit-exceeded";
97
+ readonly resource:
98
+ | "manifest-bytes"
99
+ | "manifest-entries"
100
+ | "manifest-nesting"
101
+ | "workspaces"
102
+ | "diagnostics"
103
+ | "registry-response-bytes"
104
+ | "clone-bytes"
105
+ | "cache-bytes";
106
+ readonly limit: number;
107
+ readonly observed: number | null;
108
+ }
109
+
110
+ export interface MismatchedPackageSource {
111
+ readonly status: "mismatched";
112
+ readonly code: "coordinate-mismatch" | "repository-ref-mismatch" | "repository-commit-mismatch" | "integrity-mismatch";
113
+ readonly expected: string;
114
+ readonly actual: string;
115
+ }
116
+
117
+ export type PackageSourceOutcome =
118
+ | VerifiedPackageSource
119
+ | UnavailablePackageSource
120
+ | AmbiguousPackageSource
121
+ | UnauthenticatedPackageSource
122
+ | OversizedPackageSource
123
+ | MismatchedPackageSource;
124
+
125
+ export interface PackageSourceOperationResult {
126
+ readonly outcome: PackageSourceOutcome;
127
+ readonly workspaceId: string | null;
128
+ }
129
+
130
+ export const DEFAULT_PACKAGE_SOURCE_BOUNDS: PackageSourceBounds = {
131
+ maxManifestBytes: 16 * 1024 * 1024,
132
+ maxManifestEntries: 100_000,
133
+ maxManifestNesting: 128,
134
+ maxWorkspaces: 10_000,
135
+ maxDiagnostics: 100,
136
+ maxRegistryResponseBytes: 8 * 1024 * 1024,
137
+ maxRedirects: 5,
138
+ maxRetries: 2,
139
+ maxCloneBytes: 512 * 1024 * 1024,
140
+ maxCacheBytes: 5 * 1024 * 1024 * 1024,
141
+ maxCandidates: 20,
142
+ timeoutMs: 60_000,
143
+ };