@danypops/pi-lector 0.16.0 → 0.17.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.
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  CallHierarchyEntry,
3
3
  Diagnostic,
4
+ DiagnosticContext,
4
5
  DocumentSymbolEntry,
5
6
  Hover,
6
7
  IncomingCall,
@@ -153,8 +154,21 @@ export function formatDiagnosticsCall(args: { path?: unknown }, theme: LectorThe
153
154
  return `${theme.fg("toolTitle", theme.bold(presentationTitle("diagnostics")))} ${theme.fg("accent", path)}`;
154
155
  }
155
156
 
156
- export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | undefined, expanded: boolean, theme: LectorTheme): string {
157
- if (!diagnostics || diagnostics.length === 0) return theme.fg("success", "No diagnostics.");
157
+ /** Describes project confidence separately from native diagnostics, optionally including setup actions. */
158
+ export function describeDiagnosticContext(context: DiagnosticContext | undefined, expanded = true): string {
159
+ const lines = [`Project context: ${context?.confidence ?? "unknown"}`];
160
+ if (expanded) for (const finding of context?.setupFindings ?? []) lines.push(`setup ${finding.kind}: ${finding.action}`);
161
+ return lines.join("\n");
162
+ }
163
+
164
+ export function formatDiagnosticsResult(
165
+ diagnostics: readonly Diagnostic[] | undefined,
166
+ expanded: boolean,
167
+ theme: LectorTheme,
168
+ context?: DiagnosticContext,
169
+ ): string {
170
+ const contextText = context ? `${describeDiagnosticContext(context, expanded)}\n` : "";
171
+ if (!diagnostics || diagnostics.length === 0) return contextText + theme.fg("success", "No diagnostics.");
158
172
 
159
173
  const lines = [
160
174
  theme.fg("muted", `${diagnostics.length} diagnostic${diagnostics.length === 1 ? "" : "s"}:`),
@@ -171,7 +185,7 @@ export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | und
171
185
  moreLine: moreLine(theme),
172
186
  }),
173
187
  ];
174
- return lines.join("\n");
188
+ return contextText + lines.join("\n");
175
189
  }
176
190
 
177
191
  function formatPathCall(toolName: string, args: { path?: unknown }, theme: LectorTheme, qualifier = ""): string {
@@ -4,7 +4,6 @@ import { resolve } from "node:path";
4
4
  import type {
5
5
  CachedRepositoryPage,
6
6
  ContentHash,
7
- Diagnostic,
8
7
  DocumentSymbolEntry,
9
8
  EditOutcome,
10
9
  FindFilesResult,
@@ -53,6 +52,7 @@ import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch/rend
53
52
  import { createLectorCodeIntelligenceOperations } from "./code-intelligence/operations.ts";
54
53
  import {
55
54
  type CallHierarchyToolDetails,
55
+ describeDiagnosticContext,
56
56
  formatCallHierarchyCall,
57
57
  formatCallHierarchyResult,
58
58
  formatCodeActionApplyCall,
@@ -324,8 +324,11 @@ export default function (pi: ExtensionAPI) {
324
324
  });
325
325
 
326
326
  let cachingOverlay: CachingOverlay | undefined;
327
+ let disposeWorkspaceObserver: (() => void) | undefined;
327
328
  pi.on("session_shutdown", (_event, ctx) => {
328
329
  sessionGeneration++;
330
+ disposeWorkspaceObserver?.();
331
+ disposeWorkspaceObserver = undefined;
329
332
  cacheStatesByRoot.clear();
330
333
  monitoringRoots.clear();
331
334
  lastInjectedSummary = undefined;
@@ -343,7 +346,11 @@ export default function (pi: ExtensionAPI) {
343
346
  monitoringRoots.clear();
344
347
  lastInjectedSummary = undefined;
345
348
  uiContext = ctx;
346
- setNewWorkspaceObserver((root) => startMonitoringRoot(root, ctx));
349
+ disposeWorkspaceObserver?.();
350
+ const observerGeneration = sessionGeneration;
351
+ disposeWorkspaceObserver = setNewWorkspaceObserver((root) => {
352
+ if (sessionGeneration === observerGeneration) startMonitoringRoot(root, ctx);
353
+ });
347
354
  if (ctx.hasUI) {
348
355
  // The persistent widget counterpart to the single-line "lector-cache" status above.
349
356
  // Ownership is the Pi session, not cwd: one session may legitimately touch several roots.
@@ -437,7 +444,7 @@ export default function (pi: ExtensionAPI) {
437
444
  name: "localize_context",
438
445
  label: "Localize Context",
439
446
  description:
440
- "Localize a natural-language coding task to a bounded, ranked set of workspace symbols. Combines lexical source matches with the persisted call/reference/containment graph, returns compact signatures and explicit score reasons, and reports incomplete or unavailable graph coverage. The daemon does not invoke an LLM. `directory` selects the project explicitly.",
447
+ "Localize a natural-language coding task to a bounded, ranked set of workspace symbols. Combines lexical source matches with the persisted call/reference/containment graph, returns compact signatures and explicit score reasons, and reports incomplete or unavailable graph coverage. Returns workspace-relative paths, excluding dependency and escaped symlink targets; inspect a package through its separately registered workspace. Checks generation hashes against bounded current source snapshots; stale or unverified declarations are excluded and completeness is reduced. The daemon does not invoke an LLM. `directory` selects the project explicitly.",
441
448
  promptSnippet: "Localize a coding task to ranked symbols and compact graph-backed context",
442
449
  promptGuidelines: [
443
450
  "Use localize_context near the start of an unfamiliar implementation or debugging task to get a bounded candidate set before reading files one by one.",
@@ -771,7 +778,8 @@ export default function (pi: ExtensionAPI) {
771
778
  registerLectorTool({
772
779
  name: "diagnostics",
773
780
  label: "Diagnostics",
774
- description: "List every error and warning a language server currently knows about for one file, as of its last analysis.",
781
+ description:
782
+ "List native errors and warnings with bounded project-context confidence, separate setup findings, synchronized source hash and reported document versions. Server readiness is not compiler verification; unreported configuration stays unknown.",
775
783
  promptSnippet: "List current errors/warnings for one file",
776
784
  promptGuidelines: ["Use diagnostics after an edit to check for new type errors in one specific file, instead of running a full project build."],
777
785
  parameters: Type.Object({ path: Type.String({ description: "Absolute or cwd-relative path to the file" }) }),
@@ -782,7 +790,10 @@ export default function (pi: ExtensionAPI) {
782
790
  details.diagnostics.length === 0
783
791
  ? "No diagnostics."
784
792
  : details.diagnostics.map((d) => `${d.severity} ${d.range.path}:${d.range.start.line}:${d.range.start.character} -- ${d.message}`).join("\n");
785
- return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
793
+ return {
794
+ content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${describeDiagnosticContext(details.context)}\n${text}` }],
795
+ details,
796
+ };
786
797
  },
787
798
  renderCall(args, theme, context) {
788
799
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -798,9 +809,9 @@ export default function (pi: ExtensionAPI) {
798
809
  .join("\n");
799
810
  return new Text(theme.fg("error", errorText || "diagnostics failed"), 0, 0);
800
811
  }
801
- const details = result.details as { diagnostics?: readonly Diagnostic[]; provenance?: IntelligenceProvenance } | undefined;
812
+ const details = result.details as OperationOutputs["workspace.diagnostics"] | undefined;
802
813
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
803
- text.setText(renderIntelligenceSource(formatDiagnosticsResult(details?.diagnostics, expanded, theme), details?.provenance, theme));
814
+ text.setText(renderIntelligenceSource(formatDiagnosticsResult(details?.diagnostics, expanded, theme, details?.context), details?.provenance, theme));
804
815
  return text;
805
816
  },
806
817
  });
@@ -61,8 +61,13 @@ const retryingClient: RetryingClient<LectorClient> = createRetryingClient(() =>
61
61
  */
62
62
  let onNewWorkspace: ((root: string) => void) | undefined;
63
63
 
64
- export function setNewWorkspaceObserver(observer: ((root: string) => void) | undefined): void {
65
- onNewWorkspace = observer;
64
+ /** Registers a workspace observer and returns an idempotent disposer that preserves later registrations. */
65
+ export function setNewWorkspaceObserver(observer: ((root: string) => void) | undefined): () => void {
66
+ const registration = observer ? (root: string) => observer(root) : undefined;
67
+ onNewWorkspace = registration;
68
+ return () => {
69
+ if (onNewWorkspace === registration) onNewWorkspace = undefined;
70
+ };
66
71
  }
67
72
 
68
73
  export interface RetryingLectorClient {
@@ -1,14 +1,18 @@
1
- import type { MutationHistoryEntry } from "@danypops/lector";
1
+ import type { MutationHistoryEntry, MutationTransactionLookupOutcome } from "@danypops/lector";
2
2
  import { withWorkspace, workspaceForPath } from "../lector-client.ts";
3
3
  import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
4
4
  import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
5
5
 
6
6
  const MAX_INTERNAL_HISTORY_LOOKUP_RESULTS = 2_000;
7
7
 
8
- export interface MutationTransactionRevertOutcome {
9
- readonly transactionId: string;
10
- readonly reverted: readonly { readonly path: string; readonly newHash: string | null }[];
11
- }
8
+ export type MutationTransactionRevertOutcome =
9
+ | {
10
+ readonly status: "reverted";
11
+ readonly transactionId: string;
12
+ readonly reverted: readonly { readonly path: string; readonly newHash: string | null }[];
13
+ }
14
+ | { readonly status: "stale"; readonly transactionId: string; readonly stalePaths: readonly string[] }
15
+ | Extract<MutationTransactionLookupOutcome, { readonly status: "evicted" | "wrong-workspace" | "unknown" }>;
12
16
 
13
17
  /** Match MUTATION_HISTORY_READ_PERMISSIONS/MUTATION_HISTORY_WRITE_PERMISSIONS' own declared values server-side (mutation-history/operation-registration.ts). */
14
18
  const MUTATION_HISTORY_READ_PERMISSIONS = ["workspace:read"];
@@ -32,24 +36,14 @@ async function listResolvedHistory(
32
36
  maxResults: number,
33
37
  call: LectorVehicleCall,
34
38
  ): Promise<readonly MutationHistoryEntry[]> {
35
- const relativePath = toWorkspaceRelativePath(root, absolutePath);
36
- // Single-file edits historically record the caller's workspace-relative path, while LSP
37
- // WorkspaceEdits record canonical absolute paths. Query both identities until the daemon's
38
- // stored-history migration can normalize old entries, then deduplicate by immutable entry id.
39
- const paths = relativePath === absolutePath ? [absolutePath] : [relativePath, absolutePath];
40
- const pages = await Promise.all(
41
- paths.map((path) =>
42
- invokeLectorVehicleOperation<{ entries: readonly MutationHistoryEntry[] }>(
43
- "workspace.mutationHistory",
44
- { workspaceId, path, maxResults },
45
- MUTATION_HISTORY_READ_PERMISSIONS,
46
- call,
47
- ),
48
- ),
39
+ const path = toWorkspaceRelativePath(root, absolutePath);
40
+ const page = await invokeLectorVehicleOperation<{ entries: readonly MutationHistoryEntry[] }>(
41
+ "workspace.mutationHistory",
42
+ { workspaceId, path, maxResults },
43
+ MUTATION_HISTORY_READ_PERMISSIONS,
44
+ call,
49
45
  );
50
- const byId = new Map<string, MutationHistoryEntry>();
51
- for (const page of pages) for (const entry of page.entries) byId.set(entry.id, entry);
52
- return [...byId.values()].sort((a, b) => b.timestamp - a.timestamp).slice(0, maxResults);
46
+ return page.entries;
53
47
  }
54
48
 
55
49
  export function createMutationHistoryOperations(): MutationHistoryOperations {
@@ -84,13 +78,23 @@ export function createMutationHistoryOperations(): MutationHistoryOperations {
84
78
  revertTransaction(absolutePath, transactionId, call) {
85
79
  return withWorkspace(
86
80
  () => workspaceForPath(absolutePath),
87
- ({ workspaceId }) =>
88
- invokeLectorVehicleOperation<MutationTransactionRevertOutcome>(
81
+ async ({ workspaceId }) => {
82
+ const lookup = await invokeLectorVehicleOperation<MutationTransactionLookupOutcome>(
83
+ "workspace.mutationTransaction",
84
+ { workspaceId, transactionId },
85
+ MUTATION_HISTORY_READ_PERMISSIONS,
86
+ call,
87
+ );
88
+ if (lookup.status === "stale") return { status: "stale", transactionId, stalePaths: lookup.stalePaths };
89
+ if (lookup.status !== "ready") return lookup;
90
+ const reverted = await invokeLectorVehicleOperation<Omit<Extract<MutationTransactionRevertOutcome, { status: "reverted" }>, "status">>(
89
91
  "workspace.revertMutationTransaction",
90
92
  { workspaceId, transactionId },
91
93
  MUTATION_HISTORY_WRITE_PERMISSIONS,
92
94
  call,
93
- ),
95
+ );
96
+ return { status: "reverted", ...reverted };
97
+ },
94
98
  );
95
99
  },
96
100
  };
@@ -12,9 +12,23 @@ export function formatMutationHistoryList(entries: readonly MutationHistoryEntry
12
12
  }
13
13
 
14
14
  export function formatMutationTransactionRevert(originalTransactionId: string, outcome: MutationTransactionRevertOutcome): string {
15
- const lines = [
16
- `${originalTransactionId} reverted atomically; revert recorded as transaction ${outcome.transactionId}`,
17
- ...outcome.reverted.map((entry) => `${entry.path} -> ${entry.newHash ?? "(deleted)"}`),
18
- ];
19
- return lines.join("\n");
15
+ switch (outcome.status) {
16
+ case "reverted":
17
+ return [
18
+ `${originalTransactionId} reverted atomically; revert recorded as transaction ${outcome.transactionId}`,
19
+ ...outcome.reverted.map((entry) => `${entry.path} -> ${entry.newHash ?? "(deleted)"}`),
20
+ ].join("\n");
21
+ case "stale":
22
+ return `${originalTransactionId} is stale at ${outcome.stalePaths.length} path(s); no files were reverted\n${outcome.stalePaths.join("\n")}`;
23
+ case "evicted":
24
+ return `${originalTransactionId} cannot be reverted because its bounded process-local history was evicted`;
25
+ case "wrong-workspace":
26
+ return `${originalTransactionId} belongs to a different registered workspace; use a path from that workspace`;
27
+ case "unknown":
28
+ return `${originalTransactionId} is unknown; mutation history is process-local and is lost after daemon restart`;
29
+ default: {
30
+ const exhaustive: never = outcome;
31
+ return exhaustive;
32
+ }
33
+ }
20
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.16.0",
3
+ "version": "0.17.2",
4
4
  "description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  "@danypops/vehicle-client-pi": "^0.45.0"
23
23
  },
24
24
  "dependencies": {
25
- "@danypops/lector": "^0.22.0",
25
+ "@danypops/lector": "^0.23.1",
26
26
  "@danypops/vehicle-client": "^0.10.8",
27
27
  "@danypops/vehicle-core": "^0.19.1",
28
28
  "malevich-tui-components": "^0.32.1",