formatter.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /**
  2. * Context Formatter
  3. *
  4. * Formats TaskContext as markdown or JSON for consumption by Claude.
  5. */
  6. import { Node, Edge, TaskContext, Subgraph } from '../types';
  7. import { isGeneratedFile } from '../extraction/generated-detection';
  8. /**
  9. * Format context as markdown
  10. *
  11. * Creates a compact markdown document optimized for Claude with minimal context usage:
  12. * - Brief summary
  13. * - Entry points with locations
  14. * - Code blocks only for key symbols
  15. */
  16. export function formatContextAsMarkdown(context: TaskContext): string {
  17. const lines: string[] = [];
  18. // Header with query
  19. lines.push('## Code Context\n');
  20. lines.push(`**Query:** ${context.query}\n`);
  21. // Entry points - compact format. Re-sort so generated files (.pb.go,
  22. // .pulsar.go, mocks, …) rank LAST — a flow query should lead with the
  23. // hand-written implementation, not protobuf scaffolding.
  24. const orderedEntries = [...context.entryPoints].sort((a, b) => {
  25. const aGen = isGeneratedFile(a.filePath) ? 1 : 0;
  26. const bGen = isGeneratedFile(b.filePath) ? 1 : 0;
  27. return aGen - bGen;
  28. });
  29. if (orderedEntries.length > 0) {
  30. lines.push('### Entry Points\n');
  31. for (const node of orderedEntries) {
  32. const location = node.startLine ? `:${node.startLine}` : '';
  33. lines.push(`- **${node.name}** (${node.kind}) - ${node.filePath}${location}`);
  34. if (node.signature) {
  35. lines.push(` \`${node.signature}\``);
  36. }
  37. }
  38. lines.push('');
  39. }
  40. // Related symbols - compact list (skip verbose structure tree). Drop nodes
  41. // in generated source files (`.pb.go` / `.pulsar.go` / mocks / …) — agents
  42. // chasing a flow never want to land on protobuf scaffolding (cosmos-Q3 used
  43. // to list `gov.pulsar.go::GetExpeditedThreshold` and `1.pulsar.go::Get` in
  44. // Related Symbols, pure noise that displaced real-flow entries).
  45. const otherSymbols = Array.from(context.subgraph.nodes.values())
  46. .filter(n => !context.entryPoints.some(e => e.id === n.id))
  47. .filter(n => !isGeneratedFile(n.filePath))
  48. .slice(0, 10); // Limit to 10 related symbols
  49. if (otherSymbols.length > 0) {
  50. lines.push('### Related Symbols\n');
  51. const byFile = new Map<string, Node[]>();
  52. for (const node of otherSymbols) {
  53. const existing = byFile.get(node.filePath) || [];
  54. existing.push(node);
  55. byFile.set(node.filePath, existing);
  56. }
  57. for (const [file, nodes] of byFile) {
  58. const nodeList = nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
  59. lines.push(`- ${file}: ${nodeList}`);
  60. }
  61. lines.push('');
  62. }
  63. // Code blocks - only for key entry points. Re-sort so non-generated blocks
  64. // show first (consistent with Entry Points reordering above).
  65. if (context.codeBlocks.length > 0) {
  66. const orderedBlocks = [...context.codeBlocks].sort((a, b) => {
  67. const aGen = isGeneratedFile(a.filePath) ? 1 : 0;
  68. const bGen = isGeneratedFile(b.filePath) ? 1 : 0;
  69. return aGen - bGen;
  70. });
  71. lines.push('### Code\n');
  72. for (const block of orderedBlocks) {
  73. const nodeName = block.node?.name ?? 'Unknown';
  74. lines.push(`#### ${nodeName} (${block.filePath}:${block.startLine})\n`);
  75. lines.push('```' + block.language);
  76. lines.push(block.content);
  77. lines.push('```\n');
  78. }
  79. }
  80. return lines.join('\n');
  81. }
  82. /**
  83. * Format context as JSON
  84. *
  85. * Returns a structured JSON representation suitable for programmatic use.
  86. */
  87. export function formatContextAsJson(context: TaskContext): string {
  88. // Convert Map to array for JSON serialization
  89. const serializable = {
  90. query: context.query,
  91. summary: context.summary,
  92. entryPoints: context.entryPoints.map(serializeNode),
  93. nodes: Array.from(context.subgraph.nodes.values()).map(serializeNode),
  94. edges: context.subgraph.edges.map(serializeEdge),
  95. codeBlocks: context.codeBlocks.map((block) => ({
  96. filePath: block.filePath,
  97. startLine: block.startLine,
  98. endLine: block.endLine,
  99. language: block.language,
  100. content: block.content,
  101. nodeName: block.node?.name,
  102. nodeKind: block.node?.kind,
  103. })),
  104. relatedFiles: context.relatedFiles,
  105. stats: context.stats,
  106. };
  107. return JSON.stringify(serializable, null, 2);
  108. }
  109. /**
  110. * Format a subgraph as an ASCII tree structure
  111. */
  112. export function formatSubgraphTree(subgraph: Subgraph, entryPoints: Node[]): string {
  113. const lines: string[] = [];
  114. const printed = new Set<string>();
  115. // Build adjacency list for outgoing edges
  116. const outgoing = new Map<string, Edge[]>();
  117. for (const edge of subgraph.edges) {
  118. const existing = outgoing.get(edge.source) ?? [];
  119. existing.push(edge);
  120. outgoing.set(edge.source, existing);
  121. }
  122. // Print each entry point as a tree root
  123. for (const entry of entryPoints) {
  124. formatNodeTree(entry, subgraph, outgoing, printed, lines, 0, '');
  125. lines.push(''); // Blank line between trees
  126. }
  127. // Print any remaining nodes not reached from entry points
  128. const remaining: Node[] = [];
  129. for (const node of subgraph.nodes.values()) {
  130. if (!printed.has(node.id)) {
  131. remaining.push(node);
  132. }
  133. }
  134. if (remaining.length > 0 && remaining.length <= 10) {
  135. lines.push('Other relevant symbols:');
  136. for (const node of remaining) {
  137. const location = node.startLine ? `:${node.startLine}` : '';
  138. lines.push(` ${node.kind}: ${node.name} (${node.filePath}${location})`);
  139. }
  140. } else if (remaining.length > 10) {
  141. lines.push(`... and ${remaining.length} more related symbols`);
  142. }
  143. return lines.join('\n').trim();
  144. }
  145. /**
  146. * Format a single node and its relationships
  147. */
  148. function formatNodeTree(
  149. node: Node,
  150. subgraph: Subgraph,
  151. outgoing: Map<string, Edge[]>,
  152. printed: Set<string>,
  153. lines: string[],
  154. depth: number,
  155. prefix: string
  156. ): void {
  157. if (printed.has(node.id)) {
  158. return;
  159. }
  160. printed.add(node.id);
  161. // Node header
  162. const location = node.startLine ? `:${node.startLine}` : '';
  163. const signature = node.signature ? ` - ${truncate(node.signature, 50)}` : '';
  164. lines.push(`${prefix}${node.kind}: ${node.name} (${node.filePath}${location})${signature}`);
  165. // Outgoing edges
  166. const edges = outgoing.get(node.id) ?? [];
  167. const significantEdges = edges.filter((e) =>
  168. ['calls', 'extends', 'implements', 'imports', 'references'].includes(e.kind)
  169. );
  170. // Group by kind
  171. const edgesByKind = new Map<string, Edge[]>();
  172. for (const edge of significantEdges) {
  173. const existing = edgesByKind.get(edge.kind) ?? [];
  174. existing.push(edge);
  175. edgesByKind.set(edge.kind, existing);
  176. }
  177. // Print edges grouped by kind
  178. const newPrefix = prefix + ' ';
  179. for (const [kind, kindEdges] of edgesByKind) {
  180. if (kindEdges.length > 3) {
  181. // Summarize if too many
  182. const names = kindEdges
  183. .slice(0, 3)
  184. .map((e) => {
  185. const target = subgraph.nodes.get(e.target);
  186. return target?.name ?? 'unknown';
  187. })
  188. .join(', ');
  189. lines.push(`${newPrefix}├── ${kind}: ${names} and ${kindEdges.length - 3} more`);
  190. } else {
  191. for (let i = 0; i < kindEdges.length; i++) {
  192. const edge = kindEdges[i]!;
  193. const target = subgraph.nodes.get(edge.target);
  194. const targetName = target?.name ?? 'unknown';
  195. const connector = i === kindEdges.length - 1 ? '└──' : '├──';
  196. lines.push(`${newPrefix}${connector} ${kind} → ${targetName}`);
  197. }
  198. }
  199. }
  200. // Recurse for directly connected nodes (limited depth)
  201. if (depth < 1) {
  202. for (const edge of significantEdges.slice(0, 3)) {
  203. const target = subgraph.nodes.get(edge.target);
  204. if (target && !printed.has(target.id)) {
  205. formatNodeTree(target, subgraph, outgoing, printed, lines, depth + 1, newPrefix);
  206. }
  207. }
  208. }
  209. }
  210. /**
  211. * Serialize a node for JSON output
  212. */
  213. function serializeNode(node: Node): Record<string, unknown> {
  214. return {
  215. id: node.id,
  216. kind: node.kind,
  217. name: node.name,
  218. qualifiedName: node.qualifiedName,
  219. filePath: node.filePath,
  220. language: node.language,
  221. startLine: node.startLine,
  222. endLine: node.endLine,
  223. signature: node.signature,
  224. docstring: node.docstring,
  225. visibility: node.visibility,
  226. isExported: node.isExported,
  227. isAsync: node.isAsync,
  228. isStatic: node.isStatic,
  229. };
  230. }
  231. /**
  232. * Serialize an edge for JSON output
  233. */
  234. function serializeEdge(edge: Edge): Record<string, unknown> {
  235. return {
  236. source: edge.source,
  237. target: edge.target,
  238. kind: edge.kind,
  239. line: edge.line,
  240. column: edge.column,
  241. };
  242. }
  243. /**
  244. * Truncate a string with ellipsis
  245. */
  246. function truncate(str: string, maxLength: number): string {
  247. if (str.length <= maxLength) {
  248. return str;
  249. }
  250. return str.slice(0, maxLength - 3) + '...';
  251. }
  252. /**
  253. * Format bytes as human-readable string
  254. */
  255. export function formatBytes(bytes: number): string {
  256. if (bytes < 1024) {
  257. return `${bytes} bytes`;
  258. } else if (bytes < 1024 * 1024) {
  259. return `${(bytes / 1024).toFixed(1)} KB`;
  260. } else {
  261. return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  262. }
  263. }