@mrclrchtr/supi-code-intelligence 4.0.0 → 4.2.0

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 (68) hide show
  1. package/README.md +8 -3
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  6. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  7. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  8. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -5
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +2 -2
  22. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +5 -5
  23. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -5
  24. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  25. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  26. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  27. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  28. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  29. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  30. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  31. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  32. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  33. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +2 -1
  34. package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +19 -0
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +1 -0
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-c-family.ts +230 -0
  37. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +186 -0
  38. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-jvm.ts +311 -0
  39. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-polyglot.ts +218 -0
  40. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-scripting.ts +307 -0
  41. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +9 -5
  42. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +6 -2
  43. package/package.json +6 -5
  44. package/src/analysis/search/pattern.ts +2 -2
  45. package/src/analysis/target/symbol.ts +5 -2
  46. package/src/app/app.ts +4 -1
  47. package/src/extension.ts +6 -84
  48. package/src/headless.ts +41 -0
  49. package/src/session/orientation/collect.ts +134 -180
  50. package/src/session/orientation/context-sections.ts +14 -48
  51. package/src/session/orientation-types.ts +14 -4
  52. package/src/session/orientation-workflow.ts +20 -48
  53. package/src/substrate/lsp/lifecycle.ts +25 -39
  54. package/src/substrate/lsp/state.ts +3 -0
  55. package/src/substrate/workspace-provider-host.ts +129 -0
  56. package/src/tool/graph/execute.ts +1 -1
  57. package/src/tool/graph/markdown.ts +222 -0
  58. package/src/tool/orientation/markdown.ts +66 -11
  59. package/src/tool/register.ts +10 -0
  60. package/src/ui/markdown/overview-data.ts +3 -15
  61. package/src/ui/markdown/overview.ts +4 -10
  62. package/src/ui/markdown/types.ts +0 -1
  63. package/src/substrate/tree-sitter/lifecycle.ts +0 -52
  64. package/src/tool/graph/calls-md.ts +0 -41
  65. package/src/tool/graph/implementations-md.ts +0 -57
  66. package/src/tool/graph/markdown-base.ts +0 -88
  67. package/src/tool/graph/provider-location-md.ts +0 -9
  68. package/src/tool/graph/references-md.ts +0 -41
@@ -16,11 +16,16 @@ export interface OrientationWorkflowInput {
16
16
  readonly maxResults?: number;
17
17
  }
18
18
 
19
- /** Presentation-neutral block collected for an Orientation result. */
20
- export type OrientationBlock =
21
- | { readonly kind: "heading"; readonly level: 1 | 2 | 3; readonly text: string }
19
+ /**
20
+ * Presentation-neutral content atom a collector emits for one Orientation
21
+ * section. Collectors never build headings, section notes, or document
22
+ * framing — the markdown adapter owns all of that. `subheading` renders as an
23
+ * h3 within a section (only instruction-file paths use it).
24
+ */
25
+ export type OrientationItem =
22
26
  | { readonly kind: "paragraph"; readonly text: string }
23
27
  | { readonly kind: "list-item"; readonly text: string }
28
+ | { readonly kind: "subheading"; readonly text: string }
24
29
  | {
25
30
  readonly kind: "code";
26
31
  readonly language: string | null;
@@ -51,6 +56,8 @@ export interface OrientationSectionData {
51
56
  readonly confidence: ConfidenceMode;
52
57
  readonly provenance: readonly OrientationProvenance[];
53
58
  readonly evidenceLists: readonly EvidenceListMetadata[];
59
+ /** Content atoms for this section; the adapter frames them under the title + note. */
60
+ readonly items: readonly OrientationItem[];
54
61
  }
55
62
 
56
63
  export interface OrientationCandidate {
@@ -67,7 +74,10 @@ export interface OrientationCandidate {
67
74
 
68
75
  /** Immutable facts returned by the session before markdown or TUI rendering. */
69
76
  export interface OrientationResultData {
70
- readonly blocks: readonly OrientationBlock[];
77
+ /** Document title rendered as the leading h1 (e.g. "Workspace Orientation", "Code Orientation"). */
78
+ readonly title: string;
79
+ /** Leading paragraphs rendered before the title (target summary / resolution notes). */
80
+ readonly notes: readonly string[];
71
81
  readonly sections: readonly OrientationSectionData[];
72
82
  readonly confidence: ConfidenceMode;
73
83
  readonly focusTarget: string | null;
@@ -14,10 +14,9 @@ import { loadCodeIntelligenceConfig } from "../config.ts";
14
14
  import type { CapabilityAdapter } from "./capability-adapter.ts";
15
15
  import { parseOrientationWorkflowInput } from "./input/workflows.ts";
16
16
  import { executeOrientation } from "./orientation/collect.ts";
17
- import { formatSectionNote } from "./orientation/context-sections.ts";
18
17
  import type {
19
- OrientationBlock,
20
18
  OrientationFocusInput,
19
+ OrientationItem,
21
20
  OrientationResultData,
22
21
  OrientationSectionData,
23
22
  OrientationWorkflowInput,
@@ -217,6 +216,20 @@ function addInstructionFiles(
217
216
  const collected = collectInstructionFiles(matches);
218
217
  if (!collected) return result;
219
218
  deps.markInstructionDirsSurfaced(collected.metadata.files.map((file) => file.directory));
219
+ const items: OrientationItem[] = [];
220
+ for (const file of collected.files) {
221
+ items.push(
222
+ { kind: "subheading", text: file.path },
223
+ { kind: "code", language: null, lines: file.content.split("\n") },
224
+ );
225
+ if (file.truncated) {
226
+ items.push({
227
+ kind: "paragraph",
228
+ text: `Instruction file truncated to ${file.shownLines} of ${file.totalLines} lines. Read ${file.path} for the full file.`,
229
+ });
230
+ }
231
+ items.push({ kind: "blank" });
232
+ }
220
233
  const section: OrientationSectionData = {
221
234
  key: "instructions",
222
235
  title: "Instructions",
@@ -233,10 +246,10 @@ function addInstructionFiles(
233
246
  partialReason: null,
234
247
  },
235
248
  ],
249
+ items,
236
250
  };
237
251
  return {
238
252
  ...result,
239
- blocks: insertInstructionBlocks(result.blocks, collected.files, section),
240
253
  sections: [section, ...result.sections],
241
254
  renderedSections: ["instructions", ...result.renderedSections],
242
255
  instructions: collected.metadata,
@@ -258,50 +271,9 @@ function addTargetSummary(
258
271
  cwd: string,
259
272
  ): OrientationResultData {
260
273
  const name = entry.name ? ` ${entry.name}` : "";
261
- const summary: OrientationBlock[] = [
262
- ...notes.map((note): OrientationBlock => ({ kind: "paragraph", text: `Note: ${note}` })),
263
- {
264
- kind: "paragraph",
265
- text: `Resolved target${name}: ${relative(cwd, entry.file) || entry.file}:${entry.displayLine}:${entry.displayCharacter} — Target ID: ${entry.targetId}`,
266
- },
267
- { kind: "blank" },
274
+ const summaryNotes = [
275
+ ...notes.map((note) => `Note: ${note}`),
276
+ `Resolved target${name}: ${relative(cwd, entry.file) || entry.file}:${entry.displayLine}:${entry.displayCharacter} — Target ID: ${entry.targetId}`,
268
277
  ];
269
- return { ...result, blocks: [...summary, ...result.blocks], target: { ...entry } };
270
- }
271
-
272
- function insertInstructionBlocks(
273
- existing: readonly OrientationBlock[],
274
- files: readonly {
275
- path: string;
276
- content: string;
277
- truncated: boolean;
278
- shownLines: number;
279
- totalLines: number;
280
- }[],
281
- section: OrientationSectionData,
282
- ): OrientationBlock[] {
283
- const blocks: OrientationBlock[] = [
284
- { kind: "heading", level: 2, text: "Instructions" },
285
- { kind: "paragraph", text: formatSectionNote(section) },
286
- { kind: "blank" },
287
- ];
288
- for (const file of files) {
289
- blocks.push(
290
- { kind: "heading", level: 3, text: file.path },
291
- { kind: "code", language: null, lines: file.content.split("\n") },
292
- );
293
- if (file.truncated) {
294
- blocks.push({
295
- kind: "paragraph",
296
- text: `Instruction file truncated to ${file.shownLines} of ${file.totalLines} lines. Read ${file.path} for the full file.`,
297
- });
298
- }
299
- blocks.push({ kind: "blank" });
300
- }
301
-
302
- const insertion = existing.findIndex(
303
- (block, index) => index > 0 && block.kind === "heading" && block.level === 2,
304
- );
305
- if (insertion < 0) return [...existing, ...blocks];
306
- return [...existing.slice(0, insertion), ...blocks, ...existing.slice(insertion)];
278
+ return { ...result, notes: [...summaryNotes, ...result.notes], target: { ...entry } };
307
279
  }
@@ -1,53 +1,39 @@
1
- // LSP session lifecycle uses LspRuntimeController from @mrclrchtr/supi-lsp/api.
2
- //
3
- // Registers session_start and session_shutdown handlers that manage
4
- // the LspRuntimeController lifecycle.
1
+ // LSP lifecycle backed by the process-shared Workspace provider host.
5
2
 
6
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
- import { getDefaultWorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
8
- import { LspRuntimeController, scanWorkspaceSentinels } from "@mrclrchtr/supi-lsp/api";
9
4
  import { unregisterLspFooterContribution } from "../../ui/footer.ts";
5
+ import { acquireWorkspaceProviderHost } from "../workspace-provider-host.ts";
10
6
  import type { LspAdapterState } from "./state.ts";
11
7
 
12
- export function registerLspSessionLifecycle(pi: ExtensionAPI, state: LspAdapterState): void {
8
+ /** Acquire providers once per workspace and release them after the final Pi session ends. */
9
+ export function registerLspSessionLifecycle(
10
+ pi: ExtensionAPI,
11
+ state: LspAdapterState,
12
+ onStarted?: (ctx: ExtensionContext) => void | Promise<void>,
13
+ ): void {
13
14
  pi.on("session_start", async (_event, ctx: ExtensionContext) => {
14
- const cwd = ctx.cwd;
15
- const runtime = getDefaultWorkspaceRuntime();
16
-
17
- if (state.controller) {
18
- await state.controller.shutdown();
19
- state.controller = null;
20
- }
15
+ await state.providerLease?.release();
16
+ state.providerLease = null;
17
+ state.controller = null;
21
18
  state.lspActive = false;
19
+ state.sentinelSnapshot = new Map();
22
20
 
23
- // Project-local LSP configuration may define custom commands; defer
24
- // until the project is trusted so an untrusted repository cannot control
25
- // spawned processes.
26
- if (!ctx.isProjectTrusted()) {
27
- state.controller = null;
28
- state.lspActive = false;
29
- return;
30
- }
31
-
32
- const controller = new LspRuntimeController(cwd, runtime);
33
- const result = await controller.start();
34
-
35
- if (result.kind === "ready") {
36
- state.controller = controller;
37
- state.lspActive = true;
38
- state.sentinelSnapshot = scanWorkspaceSentinels(cwd);
39
- } else {
40
- state.controller = null;
41
- state.lspActive = false;
42
- }
21
+ const lease = await acquireWorkspaceProviderHost(ctx.cwd, {
22
+ projectTrusted: ctx.isProjectTrusted(),
23
+ });
24
+ state.providerLease = lease;
25
+ state.controller = lease.lspController;
26
+ state.lspActive = lease.lspController?.kind === "ready";
27
+ state.sentinelSnapshot = lease.sentinelSnapshot;
28
+ await onStarted?.(ctx);
43
29
  });
44
30
 
45
- pi.on("session_shutdown", async (_event, _ctx: ExtensionContext) => {
46
- if (state.controller) {
47
- await state.controller.shutdown();
48
- state.controller = null;
49
- }
31
+ pi.on("session_shutdown", async () => {
32
+ await state.providerLease?.release();
33
+ state.providerLease = null;
34
+ state.controller = null;
50
35
  state.lspActive = false;
36
+ state.sentinelSnapshot = new Map();
51
37
  unregisterLspFooterContribution();
52
38
  });
53
39
  }
@@ -4,6 +4,7 @@
4
4
  // through LspRuntimeController from @mrclrchtr/supi-lsp/api.
5
5
 
6
6
  import type { LspRuntimeController } from "@mrclrchtr/supi-lsp/api";
7
+ import type { WorkspaceProviderHostLease } from "../workspace-provider-host.ts";
7
8
 
8
9
  /** LSP status overlay UI state (handle + close). */
9
10
  export interface LspInspectorState {
@@ -19,6 +20,7 @@ export interface LspInspectorState {
19
20
  */
20
21
  export interface LspAdapterState {
21
22
  controller: LspRuntimeController | null;
23
+ providerLease: WorkspaceProviderHostLease | null;
22
24
  inspector: LspInspectorState;
23
25
  lspActive: boolean;
24
26
  /** Snapshot of workspace sentinel files (package.json, tsconfig, lockfiles) for change detection. */
@@ -28,6 +30,7 @@ export interface LspAdapterState {
28
30
  export function createLspAdapterState(): LspAdapterState {
29
31
  return {
30
32
  controller: null,
33
+ providerLease: null,
31
34
  inspector: { handle: null, close: null },
32
35
  lspActive: false,
33
36
  sentinelSnapshot: new Map(),
@@ -0,0 +1,129 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { getDefaultWorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
4
+ import { LspRuntimeController, scanWorkspaceSentinels } from "@mrclrchtr/supi-lsp/api";
5
+ import { TreeSitterRuntimeController } from "@mrclrchtr/supi-tree-sitter/api";
6
+
7
+ const HOSTS = Symbol.for("supi-code-intelligence/workspace-provider-hosts");
8
+ const SHUTDOWN_GRACE_MS = 2_000;
9
+
10
+ type HostRegistry = Map<string, WorkspaceProviderHost>;
11
+
12
+ /** One acquired reference to a process-shared Workspace provider host. */
13
+ export interface WorkspaceProviderHostLease {
14
+ cwd: string;
15
+ lspController: LspRuntimeController | null;
16
+ sentinelSnapshot: Map<string, number>;
17
+ release(): Promise<void>;
18
+ }
19
+
20
+ function registry(): HostRegistry {
21
+ const global = globalThis as typeof globalThis & { [HOSTS]?: HostRegistry };
22
+ global[HOSTS] ??= new Map();
23
+ return global[HOSTS];
24
+ }
25
+
26
+ function canonicalWorkspace(cwd: string): string {
27
+ try {
28
+ return realpathSync(cwd);
29
+ } catch {
30
+ return resolve(cwd);
31
+ }
32
+ }
33
+
34
+ function settleWithin(operation: Promise<void>): Promise<void> {
35
+ return Promise.race([
36
+ operation.catch(() => undefined),
37
+ new Promise<void>((resolveGrace) => {
38
+ const timeout = setTimeout(resolveGrace, SHUTDOWN_GRACE_MS);
39
+ timeout.unref?.();
40
+ }),
41
+ ]);
42
+ }
43
+
44
+ class WorkspaceProviderHost {
45
+ readonly cwd: string;
46
+ #leases = 0;
47
+ #lsp: LspRuntimeController | null = null;
48
+ #tree: TreeSitterRuntimeController | null = null;
49
+ #treeStarted = false;
50
+ #lspStarted = false;
51
+ #settledStart = Promise.resolve();
52
+ #closed = false;
53
+
54
+ constructor(cwd: string) {
55
+ this.cwd = cwd;
56
+ }
57
+
58
+ async acquire(projectTrusted: boolean): Promise<WorkspaceProviderHostLease> {
59
+ if (this.#closed) throw new Error("Workspace provider host is closed.");
60
+ // Count this pending acquirer before awaiting startup so the final active lease cannot shut it down.
61
+ this.#leases++;
62
+ this.#settledStart = this.#settledStart
63
+ .then(() => this.#startMissing(projectTrusted))
64
+ .catch(() => undefined);
65
+ await this.#settledStart;
66
+ let released = false;
67
+ return {
68
+ cwd: this.cwd,
69
+ lspController: projectTrusted ? this.#lsp : null,
70
+ sentinelSnapshot:
71
+ projectTrusted && this.#lsp?.kind === "ready"
72
+ ? scanWorkspaceSentinels(this.cwd)
73
+ : new Map(),
74
+ release: async () => {
75
+ if (released) return;
76
+ released = true;
77
+ this.#leases--;
78
+ if (this.#leases === 0) {
79
+ registry().delete(this.cwd);
80
+ await this.#shutdown();
81
+ }
82
+ },
83
+ };
84
+ }
85
+
86
+ async #startMissing(projectTrusted: boolean): Promise<void> {
87
+ const runtime = getDefaultWorkspaceRuntime();
88
+ if (!this.#treeStarted) {
89
+ this.#treeStarted = true;
90
+ this.#tree = new TreeSitterRuntimeController(this.cwd, runtime);
91
+ await this.#tree.start().catch(() => undefined);
92
+ }
93
+ if (!projectTrusted || this.#lspStarted) return;
94
+ this.#lspStarted = true;
95
+ this.#lsp = new LspRuntimeController(this.cwd, runtime);
96
+ await this.#lsp.start().catch(() => undefined);
97
+ }
98
+
99
+ async #shutdown(): Promise<void> {
100
+ if (this.#closed) return;
101
+ this.#closed = true;
102
+ await Promise.all([
103
+ ...(this.#lsp ? [settleWithin(this.#lsp.shutdown())] : []),
104
+ ...(this.#tree ? [settleWithin(this.#tree.shutdown())] : []),
105
+ ]);
106
+ this.#lsp = null;
107
+ this.#tree = null;
108
+ }
109
+ }
110
+
111
+ /** Acquire shared semantic and structural providers for one canonical workspace. */
112
+ export async function acquireWorkspaceProviderHost(
113
+ cwd: string,
114
+ options: { projectTrusted: boolean },
115
+ ): Promise<WorkspaceProviderHostLease> {
116
+ const workspace = canonicalWorkspace(cwd);
117
+ const hosts = registry();
118
+ let host = hosts.get(workspace);
119
+ if (!host) {
120
+ host = new WorkspaceProviderHost(workspace);
121
+ hosts.set(workspace, host);
122
+ }
123
+ return host.acquire(options.projectTrusted);
124
+ }
125
+
126
+ /** Clear process-shared hosts between isolated tests. */
127
+ export function resetWorkspaceProviderHostsForTests(): void {
128
+ registry().clear();
129
+ }
@@ -6,7 +6,7 @@ import type { CodeIntelResult, CodeIntelToolExecCtx } from "../../types/index.ts
6
6
  import { searchErrorResult } from "../infra/error-results.ts";
7
7
  import { toWorkflowControl } from "../infra/workflow-control.ts";
8
8
  import { assembleGraphResult } from "../result/graph.ts";
9
- import { renderGraphResult } from "./markdown-base.ts";
9
+ import { renderGraphResult } from "./markdown.ts";
10
10
 
11
11
  export type GraphRelation = RequestedGraphRelation;
12
12
 
@@ -0,0 +1,222 @@
1
+ /** Markdown adapter for assembled code_graph evidence. */
2
+
3
+ import {
4
+ type EvidenceList,
5
+ type EvidenceListMetadata,
6
+ renderEvidenceListMetadataDisclosure,
7
+ } from "../../analysis/evidence.ts";
8
+ import { renderReadNextSection } from "../../analysis/read-next.ts";
9
+ import {
10
+ compactLineRanges,
11
+ formatAssembledReferenceList,
12
+ } from "../../analysis/references/semantic-refs.ts";
13
+ import type {
14
+ CallEntry,
15
+ CalleeScope,
16
+ ImplementationEntry,
17
+ ReferenceEntry,
18
+ } from "../../analysis/relations/types.ts";
19
+ import { toDisplayPath } from "../../analysis/search/paths.ts";
20
+ import type { GraphResultAssembly } from "../result/graph.ts";
21
+
22
+ /** Render an assembled graph result. */
23
+ export function renderGraphResult(assembly: GraphResultAssembly): string {
24
+ const lines = [
25
+ `# Graph of \`${assembly.displayName}\``,
26
+ "",
27
+ `_File: \`${assembly.resolvedDisplayFile}\`_`,
28
+ "",
29
+ ];
30
+
31
+ const available = assembly.sections.filter((section) => section.kind === "ok");
32
+ if (available.length > 0) {
33
+ lines.push(
34
+ available
35
+ .map(
36
+ (section) =>
37
+ `**${section.rel}**: ${sectionCount(section)} result${sectionCount(section) === 1 ? "" : "s"}`,
38
+ )
39
+ .join(" | "),
40
+ "",
41
+ );
42
+ }
43
+
44
+ const unavailable = assembly.sections.filter((section) => section.kind === "unavailable");
45
+ if (unavailable.length > 0) {
46
+ lines.push(
47
+ `_Unavailable: ${unavailable.map((section) => `\`${section.rel}\``).join(", ")}_`,
48
+ "",
49
+ );
50
+ }
51
+
52
+ for (const section of assembly.sections) {
53
+ if (section.kind === "unavailable") {
54
+ lines.push(`**${section.rel}**: ${section.message}`, "");
55
+ continue;
56
+ }
57
+
58
+ switch (section.rel) {
59
+ case "references": {
60
+ const rendered = renderReferencesResult({
61
+ symbolName: assembly.displayName,
62
+ references: section.evidence,
63
+ externalCount: section.data.externalCount,
64
+ confidence: section.data.confidence,
65
+ cwd: assembly.cwd,
66
+ });
67
+ lines.push(rendered.content, "");
68
+ break;
69
+ }
70
+ case "callees": {
71
+ const rendered = renderCallsResult(
72
+ section.data.enclosingScope,
73
+ section.evidence,
74
+ assembly.resolvedDisplayFile,
75
+ section.data.depth,
76
+ );
77
+ lines.push(rendered.content, "");
78
+ break;
79
+ }
80
+ case "implements": {
81
+ const rendered = renderImplementationsResult({
82
+ implementations: section.evidence,
83
+ externalCount: section.data.externalCount,
84
+ cwd: assembly.cwd,
85
+ targetName: assembly.displayName,
86
+ });
87
+ lines.push(rendered.content, "");
88
+ break;
89
+ }
90
+ }
91
+ }
92
+
93
+ lines.push(...renderReadNextSection(available.flatMap((section) => section.readNext)));
94
+ return lines.join("\n");
95
+ }
96
+
97
+ function sectionCount(
98
+ section: Extract<GraphResultAssembly["sections"][number], { kind: "ok" }>,
99
+ ): number {
100
+ return section.evidence.metadata.totalCount ?? section.evidence.metadata.shownCount;
101
+ }
102
+
103
+ function renderReferencesResult(options: {
104
+ symbolName: string;
105
+ references: EvidenceList<ReferenceEntry>;
106
+ externalCount: number;
107
+ confidence: string;
108
+ cwd: string;
109
+ }): { content: string; evidenceList: EvidenceListMetadata | null } {
110
+ const total = options.references.metadata.totalCount ?? options.references.metadata.shownCount;
111
+ const lines = [
112
+ `# References of \`${options.symbolName}\``,
113
+ "",
114
+ `**${total} reference${total === 1 ? "" : "s"}** (${options.confidence})`,
115
+ ];
116
+ if (options.externalCount > 0) {
117
+ lines.push(
118
+ `_+${options.externalCount} external reference${options.externalCount === 1 ? "" : "s"}_`,
119
+ );
120
+ }
121
+ const invalidDisclosure = renderInvalidProviderLocations(options.references.metadata);
122
+ if (invalidDisclosure) lines.push(invalidDisclosure);
123
+ lines.push("");
124
+
125
+ const displayEvidence = {
126
+ key: options.references.key,
127
+ items: options.references.items.map((reference) => ({
128
+ file: toDisplayPath(options.cwd, reference.file),
129
+ line: reference.line,
130
+ })),
131
+ metadata: options.references.metadata,
132
+ };
133
+ const evidenceList = formatAssembledReferenceList(lines, displayEvidence);
134
+ return { content: lines.join("\n"), evidenceList };
135
+ }
136
+
137
+ function renderCallsResult(
138
+ enclosingScope: CalleeScope,
139
+ calls: EvidenceList<CallEntry>,
140
+ relPath: string,
141
+ depth: "direct" | "deep" = "direct",
142
+ ): { content: string; evidenceList: EvidenceListMetadata } {
143
+ const lines: string[] = [];
144
+ const depthLabel = depth === "deep" ? "Deep structural calls" : "Direct structural calls";
145
+ const depthNote =
146
+ depth === "deep"
147
+ ? "_Deep: includes calls from nested function/method/callback scopes within the enclosing scope._"
148
+ : "_Structural only: call expressions are reported by source shape, not symbol identity; calls inside nested function/method/callback scopes are excluded from this enclosing scope._";
149
+ const total = calls.metadata.totalCount ?? calls.metadata.shownCount;
150
+
151
+ lines.push(`# ${depthLabel} from \`${enclosingScope.name}\``, "");
152
+ lines.push(
153
+ `**${total} ${depth === "deep" ? "" : "direct "}structural call${total === 1 ? "" : "s"}** from enclosing scope \`${enclosingScope.name}\` (${formatScopeRange(enclosingScope)}) in \`${relPath}\``,
154
+ "",
155
+ depthNote,
156
+ "",
157
+ );
158
+
159
+ for (const call of calls.items) lines.push(`- \`${call.name}\` (L${call.line})`);
160
+ const disclosure = renderEvidenceListMetadataDisclosure(calls.metadata);
161
+ if (disclosure) lines.push(disclosure);
162
+ return { content: lines.join("\n"), evidenceList: calls.metadata };
163
+ }
164
+
165
+ function renderImplementationsResult(options: {
166
+ implementations: EvidenceList<ImplementationEntry>;
167
+ externalCount: number;
168
+ cwd: string;
169
+ targetName?: string;
170
+ }): { content: string; evidenceList: EvidenceListMetadata } {
171
+ const lines: string[] = [
172
+ options.targetName
173
+ ? `# Implementations of \`${options.targetName}\` (semantic)`
174
+ : "# Implementations (semantic)",
175
+ "",
176
+ ];
177
+ const total =
178
+ options.implementations.metadata.totalCount ?? options.implementations.metadata.shownCount;
179
+ if (total > 0) {
180
+ lines.push(`**${total} implementation${total === 1 ? "" : "s"}** in the project`, "");
181
+ for (const [file, locations] of groupByFile(options.implementations.items, options.cwd)) {
182
+ lines.push(`### ${file}`, `- ${compactLineRanges(locations)}`, "");
183
+ }
184
+ const disclosure = renderEvidenceListMetadataDisclosure(options.implementations.metadata);
185
+ if (disclosure) lines.push(disclosure);
186
+ }
187
+
188
+ if (options.externalCount > 0) {
189
+ lines.push(
190
+ `_+${options.externalCount} external location${options.externalCount === 1 ? "" : "s"} (outside this project)_`,
191
+ "",
192
+ );
193
+ }
194
+ const invalidDisclosure = renderInvalidProviderLocations(options.implementations.metadata);
195
+ if (invalidDisclosure) lines.push(invalidDisclosure, "");
196
+
197
+ return { content: lines.join("\n"), evidenceList: options.implementations.metadata };
198
+ }
199
+
200
+ function formatScopeRange(scope: CalleeScope): string {
201
+ if (scope.startLine === scope.endLine) return `L${scope.startLine}`;
202
+ return `L${scope.startLine}–L${scope.endLine}`;
203
+ }
204
+
205
+ function groupByFile(impls: readonly ImplementationEntry[], cwd: string): Map<string, number[]> {
206
+ const byFile = new Map<string, number[]>();
207
+ for (const impl of impls) {
208
+ const displayPath = toDisplayPath(cwd, impl.file);
209
+ const group = byFile.get(displayPath) ?? [];
210
+ group.push(impl.line);
211
+ byFile.set(displayPath, group);
212
+ }
213
+ return byFile;
214
+ }
215
+
216
+ /** Render an explicit disclosure for unusable semantic-provider locations. */
217
+ function renderInvalidProviderLocations(metadata: EvidenceListMetadata): string | null {
218
+ const count = metadata.invalidLocationCount ?? 0;
219
+ if (count <= 0 || metadata.partialReason === null) return null;
220
+ const noun = count === 1 ? "location" : "locations";
221
+ return `_${count} invalid provider ${noun} omitted (${metadata.partialReason})_`;
222
+ }