@shanepadgett/tau-agent 0.27.0 → 0.28.1

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 (113) hide show
  1. package/extensions/context/sync.ts +1 -9
  2. package/extensions/context-pruning/prune.ts +2 -2
  3. package/extensions/explore/README.md +13 -21
  4. package/extensions/explore/ast/adapter.ts +97 -0
  5. package/extensions/explore/ast/engine.ts +470 -0
  6. package/extensions/explore/ast/format/ast-search.ts +97 -0
  7. package/extensions/explore/ast/format/composite.ts +26 -0
  8. package/extensions/explore/ast/format/context.ts +66 -0
  9. package/extensions/explore/ast/format/deps.ts +86 -0
  10. package/extensions/explore/ast/format/discover.ts +85 -0
  11. package/extensions/explore/ast/format/impact.ts +105 -0
  12. package/extensions/explore/ast/format/outline.ts +66 -0
  13. package/extensions/explore/ast/format/relationships.ts +81 -0
  14. package/extensions/explore/ast/format/show.ts +38 -0
  15. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  16. package/extensions/explore/ast/grammars/manifest.json +41 -0
  17. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  18. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  19. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  20. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  21. package/extensions/explore/ast/graph/relationships.ts +597 -0
  22. package/extensions/explore/ast/identity.ts +119 -0
  23. package/extensions/explore/ast/ir.ts +136 -0
  24. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  25. package/extensions/explore/ast/languages/csharp.ts +493 -0
  26. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  27. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  28. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  29. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  30. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  31. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  32. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  33. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  34. package/extensions/explore/ast/languages/go.ts +545 -0
  35. package/extensions/explore/ast/languages/java.ts +412 -0
  36. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  37. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  38. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  39. package/extensions/explore/ast/languages/odin.ts +407 -0
  40. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  41. package/extensions/explore/ast/languages/rust.ts +506 -0
  42. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  43. package/extensions/explore/ast/languages/swift.ts +466 -0
  44. package/extensions/explore/ast/languages/tree.ts +220 -0
  45. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  46. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  47. package/extensions/explore/ast/languages/typescript.ts +751 -0
  48. package/extensions/explore/ast/markdown.ts +145 -0
  49. package/extensions/explore/ast/package-surface.ts +42 -0
  50. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  51. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  52. package/extensions/explore/ast/queries/context.ts +406 -0
  53. package/extensions/explore/ast/queries/discover.ts +427 -0
  54. package/extensions/explore/ast/queries/impact.ts +250 -0
  55. package/extensions/explore/ast/queries/outline.ts +203 -0
  56. package/extensions/explore/ast/queries/show.ts +262 -0
  57. package/extensions/explore/ast/query.ts +48 -0
  58. package/extensions/explore/ast/read/hook.ts +125 -0
  59. package/extensions/explore/ast/read/policy.ts +55 -0
  60. package/extensions/explore/ast/registry.ts +120 -0
  61. package/extensions/explore/ast/scan.ts +76 -0
  62. package/extensions/explore/ast/slice.ts +44 -0
  63. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  64. package/extensions/explore/ast/tools/context.ts +126 -0
  65. package/extensions/explore/ast/tools/deps.ts +72 -0
  66. package/extensions/explore/ast/tools/discover.ts +208 -0
  67. package/extensions/explore/ast/tools/impact.ts +135 -0
  68. package/extensions/explore/ast/tools/outline.ts +200 -0
  69. package/extensions/explore/ast/tools/relationships.ts +160 -0
  70. package/extensions/explore/ast/tools/render.ts +121 -0
  71. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  72. package/extensions/explore/ast/tools/show.ts +116 -0
  73. package/extensions/explore/guidance.ts +52 -0
  74. package/extensions/explore/index.ts +82 -92
  75. package/extensions/explore/read/autoread.ts +118 -0
  76. package/extensions/explore/settings.ts +49 -31
  77. package/extensions/explore/traverse.ts +254 -91
  78. package/extensions/handoff/index.ts +1 -1
  79. package/extensions/publish/index.ts +18 -85
  80. package/extensions/publish/publish-ui.ts +0 -4
  81. package/extensions/soul/README.md +7 -4
  82. package/extensions/soul/index.ts +11 -5
  83. package/extensions/soul/prompt.ts +31 -13
  84. package/extensions/soul/settings.ts +7 -2
  85. package/extensions/subagent/agents/context-sync.md +6 -10
  86. package/extensions/subagent/agents/review.md +24 -55
  87. package/extensions/subagent/agents/scout.md +38 -115
  88. package/extensions/subagent/run.ts +1 -1
  89. package/extensions/tau-help/help.md +2 -4
  90. package/package.json +6 -4
  91. package/schemas/tau.schema.json +35 -30
  92. package/{extensions/explore → shared}/autoread.ts +21 -7
  93. package/shared/events.ts +1 -1
  94. package/shared/full-file-knowledge.ts +49 -0
  95. package/extensions/explore/ast-guidance.ts +0 -67
  96. package/extensions/explore/ast-languages.ts +0 -61
  97. package/extensions/explore/ast-tools.ts +0 -1994
  98. package/extensions/explore/ast-worker.ts +0 -1025
  99. package/extensions/explore/find.ts +0 -147
  100. package/extensions/explore/full-file-knowledge.ts +0 -234
  101. package/extensions/explore/grep.ts +0 -749
  102. package/extensions/explore/limits.ts +0 -22
  103. package/extensions/explore/ls.ts +0 -113
  104. package/extensions/explore/orientation-state.ts +0 -280
  105. package/extensions/explore/path-display.ts +0 -41
  106. package/extensions/explore/path-tree.ts +0 -150
  107. package/extensions/explore/read-cache.ts +0 -273
  108. package/extensions/explore/read-snapshots.ts +0 -57
  109. package/extensions/explore/read-stats-panel.ts +0 -176
  110. package/extensions/explore/read-stats.ts +0 -138
  111. package/extensions/explore/read.ts +0 -372
  112. package/extensions/explore/result.ts +0 -29
  113. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -0,0 +1,97 @@
1
+ import type { AstSearchMatch, AstSearchResult } from "../queries/ast-search.ts";
2
+ import type { ScanOutcome } from "../scan.ts";
3
+ import type { TraversalLimit } from "../../traverse.ts";
4
+ import { formatPathForDisplay } from "../../traverse.ts";
5
+
6
+ function lineRange(startLine: number, endLine: number): string {
7
+ return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
8
+ }
9
+
10
+ function formatMatchRows(match: AstSearchMatch): string[] {
11
+ const lines: string[] = [];
12
+ const preview = match.text.length > 0 ? match.text : "(empty)";
13
+ const previewLines = preview.split("\n");
14
+ const first = previewLines[0] ?? "(empty)";
15
+ lines.push(`${lineRange(match.startLine, match.endLine)} ${first}`);
16
+ for (let i = 1; i < previewLines.length; i += 1) {
17
+ lines.push(` ${previewLines[i] ?? ""}`);
18
+ }
19
+ for (const binding of match.bindings) {
20
+ const bindLines = binding.text.split("\n");
21
+ lines.push(` $${binding.name} = ${bindLines[0] ?? ""}`);
22
+ for (let i = 1; i < bindLines.length; i += 1) {
23
+ lines.push(` ${bindLines[i] ?? ""}`);
24
+ }
25
+ }
26
+ if (match.enclosing !== undefined) {
27
+ lines.push(` in ${match.enclosing.kind} ${match.enclosing.name} L${match.enclosing.startLine}`);
28
+ }
29
+ if (match.parseDegraded) {
30
+ lines.push(" warning: parser recovered with errors");
31
+ }
32
+ return lines;
33
+ }
34
+
35
+ /** One complete file group (path header + matches). */
36
+ export function formatAstSearchFile(path: string, matches: readonly AstSearchMatch[], cwd: string): string {
37
+ const lines = [formatPathForDisplay(path, cwd)];
38
+ for (const match of matches) {
39
+ lines.push(...formatMatchRows(match));
40
+ }
41
+ return lines.join("\n");
42
+ }
43
+
44
+ export function formatAstSearchEmpty(): string {
45
+ return "No matches";
46
+ }
47
+
48
+ function limitLabel(limit: TraversalLimit): string {
49
+ switch (limit) {
50
+ case "maxFiles":
51
+ return "file count";
52
+ case "maxSourceBytes":
53
+ return "source bytes";
54
+ case "maxDepth":
55
+ return "depth";
56
+ case "maxElapsedMs":
57
+ return "elapsed time";
58
+ case "cancelled":
59
+ return "cancelled";
60
+ }
61
+ }
62
+
63
+ /** Footers only when something was cut, a budget tripped, or file diagnostics exist. */
64
+ export function formatAstSearchFooters(result: AstSearchResult, cwd: string): string | undefined {
65
+ const limits: string[] = [];
66
+ if (result.resultLimitReached) {
67
+ limits.push(`matches omitted (result limit ${result.resultLimit})`);
68
+ }
69
+ const scan: ScanOutcome | undefined = result.scan;
70
+ if (scan?.limit !== undefined && scan.limit !== "cancelled") {
71
+ limits.push(limitLabel(scan.limit));
72
+ }
73
+ const diagnostics = result.fileErrors.map((err) => `${formatPathForDisplay(err.path, cwd)}: ${err.message}`);
74
+ const parts: string[] = [];
75
+ if (limits.length > 0) parts.push(`limits reached: ${limits.join(", ")}`);
76
+ if (diagnostics.length > 0) parts.push(`file errors: ${diagnostics.join("; ")}`);
77
+ if (parts.length === 0) return undefined;
78
+ return parts.join("\n");
79
+ }
80
+
81
+ /** Group matches by file (path order of first appearance). */
82
+ export function groupAstSearchByFile(
83
+ matches: readonly AstSearchMatch[],
84
+ ): { path: string; matches: AstSearchMatch[] }[] {
85
+ const groups: { path: string; matches: AstSearchMatch[] }[] = [];
86
+ const indexByPath = new Map<string, number>();
87
+ for (const match of matches) {
88
+ const existing = indexByPath.get(match.path);
89
+ if (existing === undefined) {
90
+ indexByPath.set(match.path, groups.length);
91
+ groups.push({ path: match.path, matches: [match] });
92
+ } else {
93
+ groups[existing]?.matches.push(match);
94
+ }
95
+ }
96
+ return groups;
97
+ }
@@ -0,0 +1,26 @@
1
+ export type CompositeBlock = {
2
+ id: string;
3
+ label: string;
4
+ text: string;
5
+ };
6
+
7
+ /** Shared section packing for impact/context complete-block emission. */
8
+ export function compositeSectionBlocks(args: {
9
+ header: string;
10
+ emptyLabel: string;
11
+ sections: readonly { id: string; label: string; text: string }[];
12
+ footers: readonly string[];
13
+ }): CompositeBlock[] {
14
+ const blocks: CompositeBlock[] = [{ id: "header", label: "header", text: args.header }];
15
+ if (args.sections.length === 0) {
16
+ blocks.push({ id: "empty", label: "empty", text: args.emptyLabel });
17
+ } else {
18
+ for (const section of args.sections) {
19
+ blocks.push({ id: section.id, label: section.label, text: section.text });
20
+ }
21
+ }
22
+ if (args.footers.length > 0) {
23
+ blocks.push({ id: "footer", label: "footer", text: args.footers.join("\n") });
24
+ }
25
+ return blocks;
26
+ }
@@ -0,0 +1,66 @@
1
+ import { formatPathForDisplay } from "../../traverse.ts";
2
+ import type { ContextEntry, ContextGroup, ContextResult, ContextTarget } from "../queries/context.ts";
3
+ import { compositeSectionBlocks, type CompositeBlock } from "./composite.ts";
4
+ import { formatCandidateList } from "./relationships.ts";
5
+
6
+ function formatTargetHeader(target: ContextTarget, budget: number, used: number, cwd: string): string {
7
+ const path = formatPathForDisplay(target.path, cwd);
8
+ return `${target.qualifiedName || target.name} ${target.kind} ${path}:${target.startLine} budget=${budget} used=${used}`;
9
+ }
10
+
11
+ function formatEntry(entry: ContextEntry, cwd: string): string {
12
+ const path = formatPathForDisplay(entry.path, cwd);
13
+ const flags = entry.flags.length > 0 ? ` ${entry.flags.join(",")}` : "";
14
+ const head = `${path}:${entry.startLine} ${entry.kind} ${entry.qualifiedName || entry.name} ~${entry.tokens}tok ${entry.view}${flags}`;
15
+ return `${head}\n${entry.text}`;
16
+ }
17
+
18
+ function formatGroup(group: ContextGroup, cwd: string): string {
19
+ const lines = [`${group.label}:`];
20
+ for (const entry of group.entries) {
21
+ lines.push(formatEntry(entry, cwd));
22
+ lines.push("");
23
+ }
24
+ if (lines.at(-1) === "") lines.pop();
25
+ return lines.join("\n");
26
+ }
27
+
28
+ function formatResolved(result: Extract<ContextResult, { kind: "resolved" }>, cwd: string): string {
29
+ const lines = [formatTargetHeader(result.target, result.budget, result.used, cwd)];
30
+ if (result.groups.length === 0) {
31
+ lines.push("No context entries.");
32
+ } else {
33
+ for (const group of result.groups) {
34
+ lines.push("");
35
+ lines.push(formatGroup(group, cwd));
36
+ }
37
+ }
38
+ if (result.truncated) {
39
+ lines.push("");
40
+ lines.push("truncated: budget exhausted or entry skipped");
41
+ }
42
+ return lines.join("\n");
43
+ }
44
+
45
+ export function formatContextResult(result: ContextResult, cwd: string): string {
46
+ if (result.kind === "error") return result.message;
47
+ if (result.kind === "notFound") return "No matching declaration.";
48
+ if (result.kind === "candidates") return formatCandidateList(result.candidates, cwd);
49
+ return formatResolved(result, cwd);
50
+ }
51
+
52
+ export function contextSectionBlocks(
53
+ result: Extract<ContextResult, { kind: "resolved" }>,
54
+ cwd: string,
55
+ ): CompositeBlock[] {
56
+ return compositeSectionBlocks({
57
+ header: formatTargetHeader(result.target, result.budget, result.used, cwd),
58
+ emptyLabel: "No context entries.",
59
+ sections: result.groups.map((group) => ({
60
+ id: group.id,
61
+ label: group.label,
62
+ text: formatGroup(group, cwd),
63
+ })),
64
+ footers: result.truncated ? ["truncated: budget exhausted or entry skipped"] : [],
65
+ });
66
+ }
@@ -0,0 +1,86 @@
1
+ import type { FileDepHit, FileDepQueryResult } from "../graph/file-graph.ts";
2
+ import { formatPathForDisplay } from "../../traverse.ts";
3
+
4
+ function depthPrefix(depth: number, showDepth: boolean): string {
5
+ return showDepth ? `d${depth} ` : "";
6
+ }
7
+
8
+ /** Factor shared directory prefixes into a small indent tree. */
9
+ function formatInternalTree(
10
+ paths: readonly { depth: number; path: string }[],
11
+ cwd: string,
12
+ showDepth: boolean,
13
+ ): string[] {
14
+ if (paths.length === 0) return [];
15
+ const rows = paths.map((hit) => ({
16
+ depth: hit.depth,
17
+ display: formatPathForDisplay(hit.path, cwd),
18
+ }));
19
+
20
+ // Common directory prefix across displays (posix-ish split on /).
21
+ const split = rows.map((row) => row.display.split("/"));
22
+ let common = 0;
23
+ const first = split[0];
24
+ if (first !== undefined && split.length > 1) {
25
+ while (common < first.length - 1) {
26
+ const part = first[common];
27
+ if (part === undefined) break;
28
+ if (!split.every((parts) => parts[common] === part)) break;
29
+ common += 1;
30
+ }
31
+ }
32
+
33
+ const lines: string[] = [];
34
+ if (common > 0 && first !== undefined) {
35
+ lines.push(first.slice(0, common).join("/"));
36
+ }
37
+
38
+ for (let i = 0; i < rows.length; i += 1) {
39
+ const row = rows[i];
40
+ const parts = split[i];
41
+ if (row === undefined || parts === undefined) continue;
42
+ const leaf = common > 0 ? parts.slice(common).join("/") : row.display;
43
+ const indent = common > 0 ? " " : "";
44
+ lines.push(`${indent}${depthPrefix(row.depth, showDepth)}${leaf}`);
45
+ }
46
+ return lines;
47
+ }
48
+
49
+ export function formatDepsResult(result: FileDepQueryResult, cwd: string, emptyLabel: string): string {
50
+ const showDepth = result.depth > 1;
51
+ const internal = result.hits
52
+ .filter((hit): hit is FileDepHit & { path: string } => hit.path !== undefined)
53
+ .map((hit) => ({ depth: hit.depth, path: hit.path }));
54
+ const external = result.hits
55
+ .filter((hit): hit is FileDepHit & { externalId: string } => hit.externalId !== undefined)
56
+ .map((hit) => ({ depth: hit.depth, id: hit.externalId }));
57
+
58
+ if (internal.length === 0 && external.length === 0) {
59
+ return emptyLabel;
60
+ }
61
+
62
+ const lines: string[] = [];
63
+ lines.push(...formatInternalTree(internal, cwd, showDepth));
64
+
65
+ if (external.length > 0) {
66
+ if (lines.length > 0) lines.push("external:");
67
+ else lines.push("external:");
68
+ for (const hit of external) {
69
+ lines.push(` ${depthPrefix(hit.depth, showDepth)}${hit.id}`);
70
+ }
71
+ }
72
+
73
+ if (result.resultLimitReached) {
74
+ lines.push(`omitted (result limit ${result.resultLimit})`);
75
+ }
76
+
77
+ return lines.join("\n");
78
+ }
79
+
80
+ export function formatDepsEmpty(): string {
81
+ return "No file dependencies";
82
+ }
83
+
84
+ export function formatReverseDepsEmpty(): string {
85
+ return "No reverse file dependencies";
86
+ }
@@ -0,0 +1,85 @@
1
+ import type { DiscoverCandidate, DiscoverResult } from "../queries/discover.ts";
2
+ import type { ScanOutcome } from "../scan.ts";
3
+ import type { TraversalLimit } from "../../traverse.ts";
4
+ import { formatPathForDisplay } from "../../traverse.ts";
5
+
6
+ function lineRange(startLine: number, endLine: number): string {
7
+ return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
8
+ }
9
+
10
+ function formatCandidateRows(candidate: DiscoverCandidate): string[] {
11
+ const lines: string[] = [];
12
+ const signature = candidate.signature.length > 0 ? candidate.signature : candidate.name;
13
+ const sigLines = signature.split("\n");
14
+ const first = sigLines[0] ?? candidate.name;
15
+ lines.push(`${lineRange(candidate.startLine, candidate.endLine)} ${candidate.kind}: ${first}`);
16
+ for (let i = 1; i < sigLines.length; i += 1) {
17
+ lines.push(` ${sigLines[i] ?? ""}`);
18
+ }
19
+ if (candidate.access !== undefined) {
20
+ lines.push(` ${candidate.access}`);
21
+ }
22
+ return lines;
23
+ }
24
+
25
+ /** One complete file group (path header + candidates). */
26
+ export function formatDiscoverFile(path: string, candidates: readonly DiscoverCandidate[], cwd: string): string {
27
+ const lines = [formatPathForDisplay(path, cwd)];
28
+ for (const candidate of candidates) {
29
+ lines.push(...formatCandidateRows(candidate));
30
+ }
31
+ return lines.join("\n");
32
+ }
33
+
34
+ export function formatDiscoverEmpty(): string {
35
+ return "No matching declarations";
36
+ }
37
+
38
+ function limitLabel(limit: TraversalLimit): string {
39
+ switch (limit) {
40
+ case "maxFiles":
41
+ return "file count";
42
+ case "maxSourceBytes":
43
+ return "source bytes";
44
+ case "maxDepth":
45
+ return "depth";
46
+ case "maxElapsedMs":
47
+ return "elapsed time";
48
+ case "cancelled":
49
+ return "cancelled";
50
+ }
51
+ }
52
+
53
+ /** Footers only when something was cut or a budget tripped. */
54
+ export function formatDiscoverFooters(result: DiscoverResult): string | undefined {
55
+ const parts: string[] = [];
56
+ if (result.resultLimitReached) {
57
+ parts.push(`candidates omitted (result limit ${result.resultLimit})`);
58
+ }
59
+ if (result.workLimitReached) parts.push("query work");
60
+ if (result.candidateLimitReached) parts.push("query candidates");
61
+ const scan: ScanOutcome | undefined = result.scan;
62
+ if (scan?.limit !== undefined && scan.limit !== "cancelled") {
63
+ parts.push(limitLabel(scan.limit));
64
+ }
65
+ if (parts.length === 0) return undefined;
66
+ return `limits reached: ${parts.join(", ")}`;
67
+ }
68
+
69
+ /** Group candidates by defining file (stable path order from caller). */
70
+ export function groupDiscoverByFile(
71
+ candidates: readonly DiscoverCandidate[],
72
+ ): { path: string; candidates: DiscoverCandidate[] }[] {
73
+ const groups: { path: string; candidates: DiscoverCandidate[] }[] = [];
74
+ const indexByPath = new Map<string, number>();
75
+ for (const candidate of candidates) {
76
+ const existing = indexByPath.get(candidate.path);
77
+ if (existing === undefined) {
78
+ indexByPath.set(candidate.path, groups.length);
79
+ groups.push({ path: candidate.path, candidates: [candidate] });
80
+ } else {
81
+ groups[existing]?.candidates.push(candidate);
82
+ }
83
+ }
84
+ return groups;
85
+ }
@@ -0,0 +1,105 @@
1
+ import { formatPathForDisplay } from "../../traverse.ts";
2
+ import type { ImpactFileRow, ImpactResult, ImpactSection, ImpactSiteRow, ImpactTarget } from "../queries/impact.ts";
3
+ import { compositeSectionBlocks, type CompositeBlock } from "./composite.ts";
4
+ import { formatCandidateList } from "./relationships.ts";
5
+
6
+ function formatTargetHeader(target: ImpactTarget, cwd: string): string {
7
+ const path = formatPathForDisplay(target.path, cwd);
8
+ return `${target.qualifiedName || target.name} ${target.kind} ${path}:${target.startLine}`;
9
+ }
10
+
11
+ function formatSiteRow(row: ImpactSiteRow): string {
12
+ const certainty = row.certainty === "exact" ? "" : ` ${row.certainty}`;
13
+ const preview = row.preview.length > 0 ? ` ${row.preview}` : "";
14
+ const name = row.name.length > 0 ? ` ${row.name}` : "";
15
+ return ` L${row.line} ${row.siteKind}${name}${certainty}${preview}`;
16
+ }
17
+
18
+ function formatFileRow(row: ImpactFileRow, cwd: string, showDepth: boolean): string {
19
+ const depth = showDepth ? `d${row.depth} ` : "";
20
+ if (row.path !== undefined) {
21
+ return ` ${depth}${formatPathForDisplay(row.path, cwd)}`;
22
+ }
23
+ return ` ${depth}${row.externalId ?? "?"}`;
24
+ }
25
+
26
+ function formatSection(section: ImpactSection, cwd: string): string[] {
27
+ const lines = [`${section.label}:`];
28
+ const showDepth = section.id === "transitiveDependents";
29
+ const siteRows = section.rows.filter((row): row is ImpactSiteRow => row.kind === "site");
30
+ const fileRows = section.rows.filter((row): row is ImpactFileRow => row.kind === "file");
31
+
32
+ if (siteRows.length > 0) {
33
+ let currentPath = "";
34
+ for (const row of siteRows) {
35
+ if (row.path !== currentPath) {
36
+ currentPath = row.path;
37
+ lines.push(formatPathForDisplay(row.path, cwd));
38
+ }
39
+ lines.push(formatSiteRow(row));
40
+ }
41
+ }
42
+
43
+ if (fileRows.length > 0) {
44
+ const internal = fileRows.filter((row) => row.path !== undefined);
45
+ const external = fileRows.filter((row) => row.externalId !== undefined);
46
+ for (const row of internal) {
47
+ lines.push(formatFileRow(row, cwd, showDepth));
48
+ }
49
+ if (external.length > 0) {
50
+ lines.push(" external:");
51
+ for (const row of external) {
52
+ lines.push(` ${formatFileRow(row, cwd, showDepth).trimStart()}`);
53
+ }
54
+ }
55
+ }
56
+
57
+ if (section.omitted) {
58
+ lines.push(` omitted (limit ${section.limit})`);
59
+ }
60
+ return lines;
61
+ }
62
+
63
+ function formatResolved(result: Extract<ImpactResult, { kind: "resolved" }>, cwd: string): string {
64
+ const lines = [formatTargetHeader(result.target, cwd)];
65
+ if (result.sections.length === 0) {
66
+ lines.push("No impact edges.");
67
+ } else {
68
+ for (const section of result.sections) {
69
+ lines.push("");
70
+ lines.push(...formatSection(section, cwd));
71
+ }
72
+ }
73
+ for (const note of result.notes) {
74
+ lines.push(note);
75
+ }
76
+ if (result.parseDegraded) {
77
+ lines.push("parser: degraded on at least one file");
78
+ }
79
+ return lines.join("\n");
80
+ }
81
+
82
+ export function formatImpactResult(result: ImpactResult, cwd: string): string {
83
+ if (result.kind === "error") return result.message;
84
+ if (result.kind === "notFound") return "No matching declaration.";
85
+ if (result.kind === "candidates") return formatCandidateList(result.candidates, cwd);
86
+ return formatResolved(result, cwd);
87
+ }
88
+
89
+ export function impactSectionBlocks(
90
+ result: Extract<ImpactResult, { kind: "resolved" }>,
91
+ cwd: string,
92
+ ): CompositeBlock[] {
93
+ const footers = [...result.notes];
94
+ if (result.parseDegraded) footers.push("parser: degraded on at least one file");
95
+ return compositeSectionBlocks({
96
+ header: formatTargetHeader(result.target, cwd),
97
+ emptyLabel: "No impact edges.",
98
+ sections: result.sections.map((section) => ({
99
+ id: section.id,
100
+ label: section.label,
101
+ text: formatSection(section, cwd).join("\n"),
102
+ })),
103
+ footers,
104
+ });
105
+ }
@@ -0,0 +1,66 @@
1
+ import type { OutlineFileView, OutlineRow } from "../queries/outline.ts";
2
+ import type { ScanOutcome } from "../scan.ts";
3
+ import type { TraversalLimit } from "../../traverse.ts";
4
+ import { formatPathForDisplay } from "../../traverse.ts";
5
+
6
+ function lineRange(startLine: number, endLine: number): string {
7
+ return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
8
+ }
9
+
10
+ function formatRow(row: OutlineRow): string[] {
11
+ const indent = " ".repeat(row.depth);
12
+ const lines: string[] = [];
13
+ if (row.docs !== undefined && row.docs.length > 0) {
14
+ for (const docLine of row.docs.replace(/\s+$/u, "").split("\n")) {
15
+ lines.push(`${indent}${docLine}`);
16
+ }
17
+ }
18
+ const signature = row.signature.replace(/\s+$/u, "");
19
+ const sigLines = signature.length > 0 ? signature.split("\n") : [row.name];
20
+ const first = sigLines[0] ?? row.name;
21
+ lines.push(`${indent}${lineRange(row.startLine, row.endLine)}: ${first}`);
22
+ for (let i = 1; i < sigLines.length; i += 1) {
23
+ lines.push(`${indent}${sigLines[i] ?? ""}`);
24
+ }
25
+ return lines;
26
+ }
27
+
28
+ /** One complete outline unit (optional path header + rows). */
29
+ export function formatOutlineFile(view: OutlineFileView, cwd: string, includePathHeader: boolean): string {
30
+ const lines: string[] = [];
31
+ if (includePathHeader) lines.push(formatPathForDisplay(view.path, cwd));
32
+ if (view.parseDegraded) {
33
+ lines.push("warning: parser recovered with errors");
34
+ }
35
+ if (view.rows.length === 0) {
36
+ lines.push("No declarations");
37
+ } else {
38
+ for (const row of view.rows) lines.push(...formatRow(row));
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+
43
+ export function formatOutlineEmpty(names: readonly string[]): string {
44
+ return names.length > 0 ? "No matching declarations" : "No declarations";
45
+ }
46
+
47
+ function limitLabel(limit: TraversalLimit): string {
48
+ switch (limit) {
49
+ case "maxFiles":
50
+ return "file count";
51
+ case "maxSourceBytes":
52
+ return "source bytes";
53
+ case "maxDepth":
54
+ return "depth";
55
+ case "maxElapsedMs":
56
+ return "elapsed time";
57
+ case "cancelled":
58
+ return "cancelled";
59
+ }
60
+ }
61
+
62
+ /** Budget/limit footer line when a recursive scan stopped early. */
63
+ export function formatOutlineBudgetFooter(outcome: ScanOutcome): string | undefined {
64
+ if (outcome.limit === undefined || outcome.limit === "cancelled") return undefined;
65
+ return `limits reached: ${limitLabel(outcome.limit)}`;
66
+ }
@@ -0,0 +1,81 @@
1
+ import type { Candidate } from "../identity.ts";
2
+ import type { RelationshipQueryResult, RelationshipSite, RelationshipTarget } from "../graph/relationships.ts";
3
+ import { formatPathForDisplay } from "../../traverse.ts";
4
+
5
+ function formatCompetitors(competitors: readonly Candidate[], cwd: string): string[] {
6
+ const lines: string[] = [];
7
+ for (const c of competitors) {
8
+ const path = formatPathForDisplay(c.path, cwd);
9
+ const sig = c.signature.replace(/\s+/gu, " ").trim();
10
+ const short = sig.length > 80 ? `${sig.slice(0, 77)}...` : sig;
11
+ lines.push(` - ${path}:${c.startLine} ${c.kind} ${c.qualifiedName || c.name} ${short}`);
12
+ }
13
+ return lines;
14
+ }
15
+
16
+ function formatSiteRow(site: RelationshipSite): string {
17
+ const certainty = site.certainty === "exact" ? "" : ` ${site.certainty}`;
18
+ const preview = site.preview.length > 0 ? ` ${site.preview}` : "";
19
+ return ` L${site.line} ${site.kind}${certainty}${preview}`;
20
+ }
21
+
22
+ function formatResolved(
23
+ target: RelationshipTarget,
24
+ sites: readonly RelationshipSite[],
25
+ cwd: string,
26
+ resultLimit: number,
27
+ resultLimitReached: boolean,
28
+ parseDegraded: boolean,
29
+ ): string {
30
+ const path = formatPathForDisplay(target.path, cwd);
31
+ const header = `${target.qualifiedName || target.name} ${target.kind} ${path}:${target.startLine}`;
32
+ if (sites.length === 0) {
33
+ const lines = [header, "No relationship sites."];
34
+ if (parseDegraded) lines.push("parser: degraded on at least one file");
35
+ return lines.join("\n");
36
+ }
37
+
38
+ const lines: string[] = [header, ""];
39
+ let currentPath = "";
40
+ for (const site of sites) {
41
+ if (site.path !== currentPath) {
42
+ currentPath = site.path;
43
+ lines.push(formatPathForDisplay(site.path, cwd));
44
+ }
45
+ lines.push(formatSiteRow(site));
46
+ if (site.competitors.length > 0) {
47
+ lines.push(" competitors:");
48
+ lines.push(...formatCompetitors(site.competitors, cwd));
49
+ }
50
+ }
51
+
52
+ if (resultLimitReached) lines.push(`omitted (result limit ${resultLimit})`);
53
+ if (parseDegraded) lines.push("parser: degraded on at least one file");
54
+ return lines.join("\n");
55
+ }
56
+
57
+ export function formatCandidateList(candidates: readonly Candidate[], cwd: string): string {
58
+ const lines = ["Ambiguous target — disambiguate with targetPath or line:", ""];
59
+ for (const c of candidates) {
60
+ const path = formatPathForDisplay(c.path, cwd);
61
+ const sig = c.signature.replace(/\s+/gu, " ").trim();
62
+ const short = sig.length > 80 ? `${sig.slice(0, 77)}...` : sig;
63
+ lines.push(`${path}:${c.startLine} ${c.kind} ${c.qualifiedName || c.name}`);
64
+ if (short.length > 0) lines.push(` ${short}`);
65
+ }
66
+ return lines.join("\n");
67
+ }
68
+
69
+ export function formatRelationshipResult(result: RelationshipQueryResult, cwd: string): string {
70
+ if (result.kind === "error") return result.message;
71
+ if (result.kind === "notFound") return "No matching declaration.";
72
+ if (result.kind === "candidates") return formatCandidateList(result.candidates, cwd);
73
+ return formatResolved(
74
+ result.target,
75
+ result.sites,
76
+ cwd,
77
+ result.resultLimit,
78
+ result.resultLimitReached,
79
+ result.parseDegraded,
80
+ );
81
+ }
@@ -0,0 +1,38 @@
1
+ import type { ShowBatch, ShowBlock } from "../queries/show.ts";
2
+ import { formatPathForDisplay } from "../../traverse.ts";
3
+
4
+ function lineRange(startLine: number, endLine: number): string {
5
+ return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
6
+ }
7
+
8
+ function formatBlock(block: ShowBlock, cwd: string, includePath: boolean): string {
9
+ const lines: string[] = [];
10
+ if (includePath) lines.push(formatPathForDisplay(block.path, cwd));
11
+ lines.push(`${lineRange(block.startLine, block.endLine)}: ${block.name}`);
12
+ for (const warning of block.warnings) {
13
+ lines.push(`warning: ${warning}`);
14
+ }
15
+ lines.push(block.text);
16
+ return lines.join("\n");
17
+ }
18
+
19
+ /**
20
+ * Exact source blocks.
21
+ * Single block: no path chrome. Multi-block: path header when the path changes.
22
+ */
23
+ export function formatShowBatch(batch: ShowBatch, cwd: string): string {
24
+ if (batch.blocks.length === 0) return "";
25
+ if (batch.blocks.length === 1) {
26
+ const only = batch.blocks[0];
27
+ if (only === undefined) return "";
28
+ return formatBlock(only, cwd, false);
29
+ }
30
+ const parts: string[] = [];
31
+ let lastPath: string | undefined;
32
+ for (const block of batch.blocks) {
33
+ const includePath = block.path !== lastPath;
34
+ lastPath = block.path;
35
+ parts.push(formatBlock(block, cwd, includePath));
36
+ }
37
+ return parts.join("\n\n");
38
+ }