@huanlin/dsh-plugin-codegraph-tool 0.1.8

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/lib/render.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Model-facing text for each operation's canonical value.
3
+ *
4
+ * Every line leads with `path:line` so a location can be acted on directly, and a truncated answer
5
+ * always says so — a capped list that reads as complete is worse than a short one, because the model
6
+ * concludes it has seen everything.
7
+ * @module @huanlin/dsh-plugin-codegraph-tool/render
8
+ */
9
+ import { assertNever } from '@deepseek-ai/dsh-util-values';
10
+ /** A symbol as one scannable line. */
11
+ function symbolLine(symbol) {
12
+ const exported = symbol.exported ? '' : ' (local)';
13
+ const signature = symbol.signature === undefined ? '' : ` ${symbol.signature.replaceAll('\n', ' ')}`;
14
+ return `${symbol.path}:${symbol.line} ${symbol.kind} ${symbol.name}${exported}${signature}`;
15
+ }
16
+ /** The `N shown of M` suffix a truncatable list carries. */
17
+ function counted(shown, total, truncated) {
18
+ return truncated ? ` (${shown} of ${total} shown)` : ` (${total})`;
19
+ }
20
+ /** A fenced source block, labelled with the line the slice starts at. */
21
+ function codeBlock(path, code, startLine) {
22
+ if (code === null)
23
+ return [` (source unavailable for ${path})`];
24
+ const from = startLine === undefined ? '' : ` from line ${startLine}`;
25
+ return [` ${path}${from}:`, '```', code, '```'];
26
+ }
27
+ /**
28
+ * Render one result as the text the model reads.
29
+ * @param value - the canonical value the operation returned.
30
+ * @returns the rendered text.
31
+ */
32
+ export function renderCodegraph(value) {
33
+ switch (value.operation) {
34
+ case 'search': {
35
+ if (value.symbols.length === 0)
36
+ return `No declaration matches in ${value.project_path}.`;
37
+ const header = `Declarations${counted(value.symbols.length, value.total, value.truncated)}:`;
38
+ return [header, ...value.symbols.map(symbol => symbolLine(symbol))].join('\n');
39
+ }
40
+ case 'node': {
41
+ if (value.symbol === null)
42
+ return `No declaration matches in ${value.project_path}.`;
43
+ const lines = [symbolLine(value.symbol)];
44
+ if (value.symbol.docstring !== undefined)
45
+ lines.push(` ${value.symbol.docstring.replaceAll('\n', '\n ')}`);
46
+ if (value.alternatives.length > 0) {
47
+ lines.push(`Also named this${counted(value.alternatives.length, value.alternatives.length, false)}:`);
48
+ lines.push(...value.alternatives.map(symbol => ` ${symbolLine(symbol)}`));
49
+ }
50
+ if (value.incoming.length > 0) {
51
+ lines.push('Reached by:');
52
+ lines.push(...value.incoming.map(relation => ` [${relation.via}] ${symbolLine(relation)}`));
53
+ }
54
+ if (value.outgoing.length > 0) {
55
+ lines.push('Reaches:');
56
+ lines.push(...value.outgoing.map(relation => ` [${relation.via}] ${symbolLine(relation)}`));
57
+ }
58
+ if (value.code !== null)
59
+ lines.push(...codeBlock(value.symbol.path, value.code, value.symbol.line));
60
+ return lines.join('\n');
61
+ }
62
+ case 'callers':
63
+ case 'callees': {
64
+ if (value.symbol === null)
65
+ return `No declaration matches in ${value.project_path}.`;
66
+ const direction = value.operation === 'callers' ? 'Callers of' : 'Called by';
67
+ if (value.relations.length === 0)
68
+ return `${direction} ${symbolLine(value.symbol)}: none in the index.`;
69
+ const header = `${direction} ${value.symbol.name}${counted(value.relations.length, value.total, value.truncated)}:`;
70
+ return [
71
+ header,
72
+ ...value.relations.map((relation) => {
73
+ const site = relation.site_line === undefined ? '' : ` at line ${relation.site_line}`;
74
+ const repeats = relation.site_count > 1 ? ` ×${relation.site_count}` : '';
75
+ return `${symbolLine(relation)}${site}${repeats}`;
76
+ }),
77
+ ].join('\n');
78
+ }
79
+ case 'impact': {
80
+ if (value.symbol === null)
81
+ return `No declaration matches in ${value.project_path}.`;
82
+ if (value.affected.length === 0)
83
+ return `Nothing in the index depends on ${value.symbol.name}.`;
84
+ const header = `Changing ${value.symbol.name} can affect${counted(value.affected.length, value.total, value.truncated)}:`;
85
+ return [
86
+ header,
87
+ ...value.affected.map(entry => `${symbolLine(entry)} [${entry.distance} hop${entry.distance === 1 ? '' : 's'}, via ${entry.via}]`),
88
+ ].join('\n');
89
+ }
90
+ case 'trace': {
91
+ if (value.from === null || value.to === null)
92
+ return `No declaration matches in ${value.project_path}.`;
93
+ if (value.paths.length === 0) {
94
+ return `No call path from ${value.from.name} to ${value.to.name} within the searched depth. The flow may cross a dynamic dispatch the index cannot follow.`;
95
+ }
96
+ const lines = [`${value.paths.length} path${value.paths.length === 1 ? '' : 's'} from ${value.from.name} to ${value.to.name}:`];
97
+ value.paths.forEach((path, index) => {
98
+ lines.push(`Path ${index + 1}:`);
99
+ for (const hop of path) {
100
+ const via = hop.via === undefined ? '' : ` [${hop.via}${hop.site_line === undefined ? '' : ` at line ${hop.site_line}`}]`;
101
+ lines.push(` ${symbolLine(hop)}${via}`);
102
+ }
103
+ });
104
+ return lines.join('\n');
105
+ }
106
+ case 'files': {
107
+ if (value.files.length === 0)
108
+ return `No indexed file matches in ${value.project_path}.`;
109
+ const header = `Indexed files${counted(value.files.length, value.total, value.truncated)}:`;
110
+ return [
111
+ header,
112
+ ...value.files.map(file => `${file.path} ${file.language} ${file.symbol_count} symbols`),
113
+ ].join('\n');
114
+ }
115
+ case 'status': {
116
+ if (!value.indexed) {
117
+ return `No index for \`${value.project_path}\`. Run codegraph_index to build one.`;
118
+ }
119
+ const languages = (value.languages ?? []).map(entry => `${entry.language} ${entry.file_count}`).join(', ');
120
+ const indexedAt = value.indexed_at;
121
+ const indexed = indexedAt === undefined || indexedAt === null ? 'never' : new Date(indexedAt).toISOString();
122
+ const lines = [
123
+ `Index for ${value.project_path} (format version ${value.format_version}):`,
124
+ `${value.file_count} files, ${value.symbol_count} symbols, ${value.edge_count} relationships.`,
125
+ `Languages: ${languages || 'none'}.`,
126
+ `Last indexed: ${indexed}.`,
127
+ ];
128
+ const stale = value.stale_file_count;
129
+ // Undefined only when a caller builds a partial value directly (as some tests do); the tool's
130
+ // own `status` handler always sets this alongside `indexed: true`.
131
+ if (stale !== undefined && stale > 0) {
132
+ const truncated = value.stale_file_count_truncated === true;
133
+ const amount = truncated ? `at least ${stale}` : `${stale}`;
134
+ const noun = stale === 1 && !truncated ? 'file' : 'files';
135
+ lines.push(`${amount} indexed ${noun} changed on disk or went missing since indexing. Call codegraph_index to refresh.`);
136
+ }
137
+ return lines.join('\n');
138
+ }
139
+ case 'explore': {
140
+ if (value.files.length === 0)
141
+ return `No declaration matches in ${value.project_path}.`;
142
+ const lines = [`Source for ${value.files.length} file${value.files.length === 1 ? '' : 's'}${value.truncated ? ` (of ${value.total} matched)` : ''}:`];
143
+ for (const file of value.files) {
144
+ lines.push(...file.symbols.map(symbol => symbolLine(symbol)));
145
+ lines.push(...codeBlock(file.path, file.code, file.code_start_line));
146
+ if (file.truncated)
147
+ lines.push(' (source truncated)');
148
+ }
149
+ return lines.join('\n');
150
+ }
151
+ case 'context': {
152
+ if (value.entry_points.length === 0)
153
+ return `Nothing in the index matches "${value.task}".`;
154
+ const lines = [`Context for "${value.task}":`, 'Entry points:'];
155
+ lines.push(...value.entry_points.map(symbol => ` ${symbolLine(symbol)}`));
156
+ if (value.related.length > 0) {
157
+ lines.push('Related:');
158
+ lines.push(...value.related.map(relation => ` [${relation.via}] ${symbolLine(relation)}`));
159
+ }
160
+ for (const file of value.files) {
161
+ lines.push(...codeBlock(file.path, file.code, file.code_start_line));
162
+ if (file.truncated)
163
+ lines.push(' (source truncated)');
164
+ }
165
+ return lines.join('\n');
166
+ }
167
+ case 'index': {
168
+ const languages = value.languages.map(entry => `${entry.language} ${entry.file_count}`).join(', ');
169
+ const unresolved = value.unresolved_count === 0
170
+ ? 'Every call site resolved.'
171
+ : `${value.unresolved_likely_internal_count} of ${value.unresolved_count} unresolved call sites look like genuine gaps; the rest are member calls or already-imported names, never workspace-edge candidates.`;
172
+ return [
173
+ `Indexed ${value.project_path}:`,
174
+ `${value.files_indexed} files indexed, ${value.files_skipped} skipped.`,
175
+ `${value.symbol_count} symbols, ${value.edge_count} relationships. ${unresolved}`,
176
+ `Languages: ${languages || 'none'}.`,
177
+ ].join('\n');
178
+ }
179
+ /* v8 ignore next -- exhaustive over the output schema's closed union; unreachable. */
180
+ default:
181
+ return assertNever(value, 'tool-codegraph output');
182
+ }
183
+ }
184
+ //# sourceMappingURL=render.js.map