@danypops/lector 0.1.9 → 0.1.10

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 CHANGED
@@ -64,7 +64,7 @@ Three independent `SymbolIndexPort` implementations use one result DTO with expl
64
64
  - **TypeScript compiler** extracts bounded structural declarations from cold, malformed, or partially configured projects. It does not claim cross-file identity.
65
65
  - **tree-sitter** extracts bounded structural declarations and caches them by content hash. It does not claim type or language-server identity.
66
66
 
67
- File-position operations dispatch by extension and seed a cold server with the requested file. Workspace symbol search and graph population detect every enabled language, merge results deterministically, and retain per-symbol and per-backend provenance; one failed backend produces an explicit partial result instead of hiding successful languages. Bash remains explicit-file-only until its vulnerable upstream dependency chain is removed; generic YAML is explicit-file-only because lockfiles and data files do not establish a workspace language.
67
+ File-position operations dispatch by extension and seed a cold server with the requested file. Workspace symbol search and graph population detect every enabled language, merge results deterministically, and retain per-symbol and per-backend provenance. Backend and file-level semantic failures produce bounded partial results without discarding successful languages or files. Bash remains explicit-file-only until its vulnerable upstream dependency chain is removed; generic YAML is explicit-file-only because lockfiles and data files do not establish a workspace language.
68
68
 
69
69
  Symbol results report `fidelity`, `backend`, `authority`, `freshness`, `limitations`, `truncated`, and composite `sources`/`completeness` when applicable. Language-server messages, pending requests, opened files, file bytes, settling, parser files, parser nodes, parser bytes, language indexes, and returned symbols are bounded.
70
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/lector",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Platform-neutral filesystem & code-intelligence capability: core, service, and driven adapters",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -97,6 +97,10 @@ export class PolyglotCodeIntelligenceIndex implements SymbolIndexPort, CodeIntel
97
97
  };
98
98
  }
99
99
 
100
+ provenanceForPath(path: string): IntelligenceProvenance {
101
+ return this.indexForPath(path).provenance;
102
+ }
103
+
100
104
  goToDefinition(at: WorkspaceLocation) {
101
105
  return this.indexForPath(at.path).goToDefinition(at);
102
106
  }
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { type Migration, openSqliteWithPragmas } from "@danypops/daemon-kit/storage";
3
3
  import type { IntelligenceProvenance } from "../domain/intelligence-provenance.ts";
4
+ import type { PopulateSymbolGraphResult, SymbolGraphPopulationFailure } from "../domain/populate-symbol-graph.ts";
4
5
  import type { SymbolGraphGeneration } from "../domain/symbol-graph-generation.ts";
5
6
  import type { SymbolNodeId } from "../domain/symbol-node-id.ts";
6
7
  import type { SymbolEdgeKind, SymbolGraphPort, SymbolNode } from "../ports/symbol-graph-port.ts";
@@ -32,6 +33,10 @@ const MIGRATIONS: Migration[] = [
32
33
  version: 4,
33
34
  up: (db) => db.exec("ALTER TABLE symbol_graph_generation ADD COLUMN sources_json TEXT"),
34
35
  },
36
+ {
37
+ version: 5,
38
+ up: (db) => db.exec("ALTER TABLE symbol_graph_generation ADD COLUMN result_json TEXT"),
39
+ },
35
40
  ];
36
41
 
37
42
  interface NodeRow {
@@ -53,6 +58,7 @@ interface GenerationRow {
53
58
  edges_added: number;
54
59
  provenance_json: string | null;
55
60
  sources_json: string | null;
61
+ result_json: string | null;
56
62
  }
57
63
 
58
64
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -103,6 +109,69 @@ function parseSources(json: string | null): readonly IntelligenceProvenance[] |
103
109
  return sources.every((source) => source !== undefined) ? sources : undefined;
104
110
  }
105
111
 
112
+ function isNonNegativeInteger(value: unknown): value is number {
113
+ return Number.isSafeInteger(value) && typeof value === "number" && value >= 0;
114
+ }
115
+
116
+ function asPopulationFailure(value: unknown): SymbolGraphPopulationFailure | undefined {
117
+ if (!isRecord(value)) return undefined;
118
+ const provenance = asProvenance(value.provenance);
119
+ if (
120
+ typeof value.path !== "string" ||
121
+ value.path.length > 4096 ||
122
+ (value.operation !== "document-symbols" && value.operation !== "outgoing-calls") ||
123
+ typeof value.code !== "string" ||
124
+ value.code.length > 100 ||
125
+ typeof value.message !== "string" ||
126
+ value.message.length > 500 ||
127
+ !provenance
128
+ ) {
129
+ return undefined;
130
+ }
131
+ return { path: value.path, operation: value.operation, code: value.code, message: value.message, provenance };
132
+ }
133
+
134
+ function parsePopulationResult(json: string | null): PopulateSymbolGraphResult | undefined {
135
+ const value = parseJson(json);
136
+ if (!isRecord(value) || (value.completeness !== "complete" && value.completeness !== "partial") || !Array.isArray(value.failures)) return undefined;
137
+ if (
138
+ !isNonNegativeInteger(value.filesAttempted) ||
139
+ !isNonNegativeInteger(value.filesProcessed) ||
140
+ !isNonNegativeInteger(value.filesFailed) ||
141
+ !isNonNegativeInteger(value.symbolsProcessed) ||
142
+ !isNonNegativeInteger(value.nodesAdded) ||
143
+ !isNonNegativeInteger(value.edgesAdded) ||
144
+ !isNonNegativeInteger(value.failureCount) ||
145
+ typeof value.failuresTruncated !== "boolean" ||
146
+ value.failures.length > 100
147
+ ) {
148
+ return undefined;
149
+ }
150
+ const failures = value.failures.map(asPopulationFailure);
151
+ if (
152
+ !failures.every((failure) => failure !== undefined) ||
153
+ value.filesProcessed > value.filesAttempted ||
154
+ value.filesFailed > value.filesAttempted ||
155
+ failures.length > value.failureCount ||
156
+ (value.completeness === "complete") !== (value.failureCount === 0) ||
157
+ value.failuresTruncated !== value.failureCount > failures.length
158
+ ) {
159
+ return undefined;
160
+ }
161
+ return {
162
+ completeness: value.completeness,
163
+ filesAttempted: value.filesAttempted,
164
+ filesProcessed: value.filesProcessed,
165
+ filesFailed: value.filesFailed,
166
+ symbolsProcessed: value.symbolsProcessed,
167
+ nodesAdded: value.nodesAdded,
168
+ edgesAdded: value.edgesAdded,
169
+ failureCount: value.failureCount,
170
+ failures,
171
+ failuresTruncated: value.failuresTruncated,
172
+ };
173
+ }
174
+
106
175
  /**
107
176
  * SQLite-backed SymbolGraphPort; survives a daemon restart pointed at the
108
177
  * same database file. reachableFrom uses `WITH RECURSIVE` rather than an
@@ -172,12 +241,26 @@ export class SqliteSymbolGraph implements SymbolGraphPort {
172
241
  async getGeneration(): Promise<SymbolGraphGeneration | undefined> {
173
242
  const row = this.db
174
243
  .query(
175
- "SELECT source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json, sources_json FROM symbol_graph_generation WHERE singleton = 1",
244
+ "SELECT source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json, sources_json, result_json FROM symbol_graph_generation WHERE singleton = 1",
176
245
  )
177
246
  .get() as GenerationRow | null;
178
247
  if (!row) return undefined;
179
248
  const provenance = parseProvenance(row.provenance_json);
180
249
  const sources = parseSources(row.sources_json);
250
+ const result =
251
+ parsePopulationResult(row.result_json) ??
252
+ ({
253
+ completeness: "complete",
254
+ filesAttempted: row.files_processed,
255
+ filesProcessed: row.files_processed,
256
+ filesFailed: 0,
257
+ symbolsProcessed: row.symbols_processed,
258
+ nodesAdded: row.nodes_added,
259
+ edgesAdded: row.edges_added,
260
+ failureCount: 0,
261
+ failures: [],
262
+ failuresTruncated: false,
263
+ } satisfies PopulateSymbolGraphResult);
181
264
  return {
182
265
  sourceFingerprint: row.source_fingerprint,
183
266
  maxFiles: row.max_files,
@@ -185,19 +268,14 @@ export class SqliteSymbolGraph implements SymbolGraphPort {
185
268
  completedAt: row.completed_at,
186
269
  ...(provenance ? { provenance } : {}),
187
270
  ...(sources ? { sources } : {}),
188
- result: {
189
- filesProcessed: row.files_processed,
190
- symbolsProcessed: row.symbols_processed,
191
- nodesAdded: row.nodes_added,
192
- edgesAdded: row.edges_added,
193
- },
271
+ result,
194
272
  };
195
273
  }
196
274
 
197
275
  async setGeneration(generation: SymbolGraphGeneration): Promise<void> {
198
276
  this.db
199
277
  .query(
200
- "INSERT INTO symbol_graph_generation (singleton, source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json, sources_json) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET source_fingerprint = excluded.source_fingerprint, max_files = excluded.max_files, max_symbols_per_file = excluded.max_symbols_per_file, completed_at = excluded.completed_at, files_processed = excluded.files_processed, symbols_processed = excluded.symbols_processed, nodes_added = excluded.nodes_added, edges_added = excluded.edges_added, provenance_json = excluded.provenance_json, sources_json = excluded.sources_json",
278
+ "INSERT INTO symbol_graph_generation (singleton, source_fingerprint, max_files, max_symbols_per_file, completed_at, files_processed, symbols_processed, nodes_added, edges_added, provenance_json, sources_json, result_json) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(singleton) DO UPDATE SET source_fingerprint = excluded.source_fingerprint, max_files = excluded.max_files, max_symbols_per_file = excluded.max_symbols_per_file, completed_at = excluded.completed_at, files_processed = excluded.files_processed, symbols_processed = excluded.symbols_processed, nodes_added = excluded.nodes_added, edges_added = excluded.edges_added, provenance_json = excluded.provenance_json, sources_json = excluded.sources_json, result_json = excluded.result_json",
201
279
  )
202
280
  .run(
203
281
  generation.sourceFingerprint,
@@ -210,6 +288,7 @@ export class SqliteSymbolGraph implements SymbolGraphPort {
210
288
  generation.result.edgesAdded,
211
289
  generation.provenance ? JSON.stringify(generation.provenance) : null,
212
290
  generation.sources ? JSON.stringify(generation.sources) : null,
291
+ JSON.stringify(generation.result),
213
292
  );
214
293
  }
215
294
 
package/src/cli.ts CHANGED
@@ -12,6 +12,7 @@ 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
14
  import { DEFAULT_PACKAGE_SOURCE_BOUNDS, type PackageSourceOperationResult } from "./domain/package-source.ts";
15
+ import type { PopulateSymbolGraphResult } from "./domain/populate-symbol-graph.ts";
15
16
  import type { SymbolSearchResult } from "./domain/workspace-symbol.ts";
16
17
  import type { WorkspacePort } from "./ports/workspace-port.ts";
17
18
  import type { WorkspaceId } from "./service.ts";
@@ -361,11 +362,19 @@ function requiredIntFlag(flags: string[], flag: string): number {
361
362
  return parsed;
362
363
  }
363
364
 
364
- function formatJobSnapshot(job: JobSnapshot<{ filesProcessed: number; symbolsProcessed: number; nodesAdded: number; edgesAdded: number }>): string {
365
+ function formatPopulationResult(result: PopulateSymbolGraphResult): string {
366
+ const counts = `${result.filesProcessed}/${result.filesAttempted} files, ${result.symbolsProcessed} symbols, ${result.nodesAdded} nodes, ${result.edgesAdded} edges`;
367
+ if (result.completeness === "complete") return counts;
368
+ const first = result.failures[0];
369
+ const failure = first ? `; first failure: ${first.path} [${first.code} via ${first.provenance.backend}] ${first.message}` : "";
370
+ return `partial -- ${counts}, ${result.filesFailed} failed files (${result.failureCount} failed operations)${failure}`;
371
+ }
372
+
373
+ function formatJobSnapshot(job: JobSnapshot<PopulateSymbolGraphResult>): string {
365
374
  if (job.status === "queued") return `${job.id}: queued (${job.operation}); poll with: lector job status ${job.id}`;
366
375
  if (job.status === "running") return `${job.id}: still running (${job.operation}); poll with: lector job status ${job.id}`;
367
376
  if (job.status === "failed") return `${job.id}: failed [${job.error.code}] -- ${job.error.message}`;
368
- return `${job.id}: succeeded -- ${job.result.filesProcessed} files, ${job.result.symbolsProcessed} symbols, ${job.result.nodesAdded} nodes, ${job.result.edgesAdded} edges`;
377
+ return `${job.id}: succeeded -- ${formatPopulationResult(job.result)}`;
369
378
  }
370
379
 
371
380
  async function runWorkspacePopulateSymbolGraph(workspaceId: string | undefined, flags: string[]): Promise<void> {
@@ -385,11 +394,7 @@ async function runWorkspacePopulateSymbolGraph(workspaceId: string | undefined,
385
394
  return;
386
395
  }
387
396
  const result = await client.call("workspace.populateSymbolGraph", { workspaceId, maxFiles, maxSymbolsPerFile });
388
- console.log(
389
- hasFlag(flags, "--json")
390
- ? JSON.stringify(result)
391
- : `${result.filesProcessed} files, ${result.symbolsProcessed} symbols, ${result.nodesAdded} nodes, ${result.edgesAdded} edges`,
392
- );
397
+ console.log(hasFlag(flags, "--json") ? JSON.stringify(result) : formatPopulationResult(result));
393
398
  }
394
399
 
395
400
  async function runJobStatus(jobId: string | undefined, flags: string[]): Promise<void> {
@@ -411,6 +416,7 @@ async function runWorkspaceCacheStatus(workspaceId: string | undefined, flags: s
411
416
  }
412
417
  if (status.status === "not-cached") console.log(`not cached -- ${status.reason}`);
413
418
  else if (status.status === "caching") console.log(`caching -- job ${status.jobId}`);
419
+ else if (status.status === "partial") console.log(`partially cached -- ${formatPopulationResult(status.generation.result)}`);
414
420
  else console.log(`cached -- completed ${new Date(status.generation.completedAt).toISOString()}`);
415
421
  }
416
422
 
@@ -1,17 +1,60 @@
1
1
  import type { CodeIntelligencePort } from "../ports/code-intelligence-port.ts";
2
2
  import type { SymbolGraphPort, SymbolNode } from "../ports/symbol-graph-port.ts";
3
+ import type { OutgoingCall } from "./call-hierarchy.ts";
3
4
  import type { DocumentSymbolEntry } from "./document-symbol.ts";
5
+ import type { IntelligenceProvenance } from "./intelligence-provenance.ts";
4
6
  import { deriveSymbolNodeId } from "./symbol-node-id.ts";
5
7
  import type { WorkspaceLocation } from "./workspace-symbol.ts";
6
8
 
7
9
  const CALLABLE_KINDS = new Set(["function", "method", "constructor"]);
10
+ const MAX_RECORDED_FAILURES = 100;
11
+ const MAX_FAILURE_MESSAGE_LENGTH = 500;
12
+
13
+ export interface SymbolGraphPopulationFailure {
14
+ readonly path: string;
15
+ readonly operation: "document-symbols" | "outgoing-calls";
16
+ readonly code: string;
17
+ readonly message: string;
18
+ readonly provenance: IntelligenceProvenance;
19
+ }
8
20
 
9
21
  export interface PopulateSymbolGraphResult {
22
+ readonly completeness: "complete" | "partial";
23
+ readonly filesAttempted: number;
10
24
  readonly filesProcessed: number;
25
+ readonly filesFailed: number;
11
26
  readonly symbolsProcessed: number;
12
27
  /** addNode calls made, not necessarily new nodes -- a symbol reached from multiple edges is upserted once per encounter within a run, deduped in-memory. */
13
28
  readonly nodesAdded: number;
14
29
  readonly edgesAdded: number;
30
+ readonly failureCount: number;
31
+ readonly failures: readonly SymbolGraphPopulationFailure[];
32
+ readonly failuresTruncated: boolean;
33
+ }
34
+
35
+ const UNKNOWN_PROVENANCE: IntelligenceProvenance = {
36
+ fidelity: "semantic",
37
+ backend: "unavailable",
38
+ languageId: "unknown",
39
+ authority: "language-server",
40
+ freshness: "live-process",
41
+ limitations: ["source provenance was unavailable"],
42
+ };
43
+
44
+ function boundedFailure(
45
+ index: CodeIntelligencePort,
46
+ path: string,
47
+ operation: SymbolGraphPopulationFailure["operation"],
48
+ error: unknown,
49
+ ): SymbolGraphPopulationFailure {
50
+ const errorName = error instanceof Error ? error.name : undefined;
51
+ return {
52
+ path,
53
+ operation,
54
+ code: errorName && errorName !== "Error" ? errorName : "CodeIntelligenceFileError",
55
+ message: (error instanceof Error ? error.message : String(error)).slice(0, MAX_FAILURE_MESSAGE_LENGTH),
56
+ provenance: index.provenanceForPath?.(path) ?? index.provenance ?? UNKNOWN_PROVENANCE,
57
+ };
15
58
  }
16
59
 
17
60
  function toLocation(entry: DocumentSymbolEntry): WorkspaceLocation {
@@ -50,7 +93,16 @@ export async function populateSymbolGraph(
50
93
  let symbolsProcessed = 0;
51
94
  let nodesAdded = 0;
52
95
  let edgesAdded = 0;
96
+ let failureCount = 0;
53
97
  const addedNodeIds = new Set<string>();
98
+ const failedFiles = new Set<string>();
99
+ const failures: SymbolGraphPopulationFailure[] = [];
100
+
101
+ function recordFailure(file: string, operation: SymbolGraphPopulationFailure["operation"], error: unknown): void {
102
+ failureCount++;
103
+ failedFiles.add(file);
104
+ if (failures.length < MAX_RECORDED_FAILURES) failures.push(boundedFailure(index, file, operation, error));
105
+ }
54
106
 
55
107
  async function ensureNode(node: SymbolNode): Promise<void> {
56
108
  if (addedNodeIds.has(node.id)) return;
@@ -60,7 +112,13 @@ export async function populateSymbolGraph(
60
112
  }
61
113
 
62
114
  for (const file of files) {
63
- const topLevel = await index.documentSymbols(file);
115
+ let topLevel: DocumentSymbolEntry[];
116
+ try {
117
+ topLevel = await index.documentSymbols(file);
118
+ } catch (error) {
119
+ recordFailure(file, "document-symbols", error);
120
+ continue;
121
+ }
64
122
  const flattened = flattenDocumentSymbols(topLevel).slice(0, maxSymbolsPerFile);
65
123
  filesProcessed++;
66
124
 
@@ -76,7 +134,13 @@ export async function populateSymbolGraph(
76
134
  }
77
135
 
78
136
  if (CALLABLE_KINDS.has(entry.kind)) {
79
- const callees = await index.outgoingCalls(location);
137
+ let callees: OutgoingCall[];
138
+ try {
139
+ callees = await index.outgoingCalls(location);
140
+ } catch (error) {
141
+ recordFailure(file, "outgoing-calls", error);
142
+ continue;
143
+ }
80
144
  for (const call of callees) {
81
145
  const calleeNode: SymbolNode = { id: deriveSymbolNodeId(call.to.location), name: call.to.name, kind: call.to.kind, location: call.to.location };
82
146
  await ensureNode(calleeNode);
@@ -87,5 +151,16 @@ export async function populateSymbolGraph(
87
151
  }
88
152
  }
89
153
 
90
- return { filesProcessed, symbolsProcessed, nodesAdded, edgesAdded };
154
+ return {
155
+ completeness: failureCount === 0 ? "complete" : "partial",
156
+ filesAttempted: files.length,
157
+ filesProcessed,
158
+ filesFailed: failedFiles.size,
159
+ symbolsProcessed,
160
+ nodesAdded,
161
+ edgesAdded,
162
+ failureCount,
163
+ failures,
164
+ failuresTruncated: failureCount > failures.length,
165
+ };
91
166
  }
@@ -16,4 +16,5 @@ export interface SymbolGraphGeneration {
16
16
  export type WorkspaceCacheStatus =
17
17
  | { readonly status: "not-cached"; readonly reason: "no-completed-generation" | "bounds-changed" | "source-changed" }
18
18
  | { readonly status: "caching"; readonly jobId: string }
19
+ | { readonly status: "partial"; readonly generation: SymbolGraphGeneration }
19
20
  | { readonly status: "cached"; readonly generation: SymbolGraphGeneration };
package/src/index.ts CHANGED
@@ -130,7 +130,7 @@ export type {
130
130
  VerifiedPackageSource,
131
131
  } from "./domain/package-source.ts";
132
132
  export { DEFAULT_PACKAGE_SOURCE_BOUNDS } from "./domain/package-source.ts";
133
- export { type PopulateSymbolGraphResult, populateSymbolGraph } from "./domain/populate-symbol-graph.ts";
133
+ export { type PopulateSymbolGraphResult, populateSymbolGraph, type SymbolGraphPopulationFailure } from "./domain/populate-symbol-graph.ts";
134
134
  export { prepareCallHierarchy } from "./domain/prepare-call-hierarchy.ts";
135
135
  export { raceWorkspaceQuery } from "./domain/race-workspace-query.ts";
136
136
  export { type RawRead, rawRead, WorkspaceEntryNotFound } from "./domain/raw-read.ts";
@@ -2,6 +2,7 @@ import type { CallHierarchyEntry, IncomingCall, OutgoingCall } from "../domain/c
2
2
  import type { Diagnostic } from "../domain/diagnostic.ts";
3
3
  import type { DocumentSymbolEntry } from "../domain/document-symbol.ts";
4
4
  import type { Hover } from "../domain/hover.ts";
5
+ import type { IntelligenceProvenance } from "../domain/intelligence-provenance.ts";
5
6
  import type { WorkspaceLocation } from "../domain/workspace-symbol.ts";
6
7
 
7
8
  /**
@@ -17,6 +18,8 @@ import type { WorkspaceLocation } from "../domain/workspace-symbol.ts";
17
18
  * process findSymbols already keeps alive per workspace -- not a second one.
18
19
  */
19
20
  export interface CodeIntelligencePort {
21
+ readonly provenance?: IntelligenceProvenance;
22
+ provenanceForPath?(path: string): IntelligenceProvenance | undefined;
20
23
  /** Where the symbol at `at` is actually declared -- may cross files, may return more than one candidate. */
21
24
  goToDefinition(at: WorkspaceLocation): Promise<WorkspaceLocation[]>;
22
25
  /** Every concrete implementation of the interface/abstract member at `at` -- unlike goToDefinition, crosses a port/interface boundary into its real adapters. */
package/src/service.ts CHANGED
@@ -313,7 +313,7 @@ export interface OperationOutputs {
313
313
  "workspace.prepareCallHierarchy": Provenanced<{ items: readonly CallHierarchyEntry[] }>;
314
314
  "workspace.incomingCalls": Provenanced<{ calls: readonly IncomingCall[] }>;
315
315
  "workspace.outgoingCalls": Provenanced<{ calls: readonly OutgoingCall[] }>;
316
- "workspace.populateSymbolGraph": { filesProcessed: number; symbolsProcessed: number; nodesAdded: number; edgesAdded: number };
316
+ "workspace.populateSymbolGraph": PopulateSymbolGraphResult;
317
317
  "workspace.reachableFrom": { symbols: readonly SymbolNode[] };
318
318
  "workspace.symbolEdgesFrom": { symbols: readonly SymbolNode[] };
319
319
  "workspace.symbolEdgesTo": { symbols: readonly SymbolNode[] };
@@ -911,7 +911,7 @@ export function createLectorService(workspaces: ReadonlyMap<WorkspaceId, Workspa
911
911
  return { status: "not-cached", reason: "source-changed" };
912
912
  }
913
913
  if (currentFingerprint !== generation.sourceFingerprint) return { status: "not-cached", reason: "source-changed" };
914
- return { status: "cached", generation };
914
+ return generation.result.completeness === "partial" ? { status: "partial", generation } : { status: "cached", generation };
915
915
  }
916
916
 
917
917
  function isRecord(value: unknown): value is Record<string, unknown> {