@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/index.js ADDED
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Model-facing `codegraph` tool family over `ctx.codegraph` and `ctx.fs`. The `codegraph` tool is
3
+ * read-only, with ten operations: the eight the seam answers directly, plus `explore` and `context`,
4
+ * which compose graph queries with source reads because a graph store returns positions and cannot
5
+ * reach a workspace's bytes. A second tool, `codegraph_index`, builds or refreshes the graph on
6
+ * explicit request; it is separate so it can carry its own, much larger timeout budget than a query
7
+ * — `defineTool`'s `timeoutMs` is fixed per registration, not per call, so one operation cannot borrow
8
+ * a bigger budget from within a shared tool.
9
+ *
10
+ * The tools own every default the seam refuses to guess — result limits, traversal depth, source
11
+ * caps — so the seam's requests stay fully specified and a deployment can retune the model's answer
12
+ * size without touching a store. They runtime-inject only `tools`, `codegraph`, `fs`, and
13
+ * `systemPrompt`, and import no store.
14
+ *
15
+ * Namespace plugin (named exports, no default export).
16
+ * @module @huanlin/dsh-plugin-codegraph-tool
17
+ */
18
+ import z from '@deepseek-ai/schemastery';
19
+ import { defineTool } from '@deepseek-ai/dsh-tools';
20
+ import { CodegraphError } from '@huanlin/dsh-plugin-codegraph-service';
21
+ import { assertNever } from '@deepseek-ai/dsh-util-values';
22
+ import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
23
+ import { declarationsOnly, groupByFile, mergeByHits, mergeRelations, taskTerms } from "./compose.js";
24
+ import { toAffected, toHop, toRelation, toSymbol } from "./projection.js";
25
+ import { renderCodegraph } from "./render.js";
26
+ import { CODEGRAPH_INDEX_PARAMETERS, CODEGRAPH_OUTPUT_SCHEMA, CODEGRAPH_PARAMETERS } from "./schema.js";
27
+ import { readSlice } from "./source.js";
28
+ export { CODEGRAPH_INDEX_PARAMETERS, CODEGRAPH_OPERATIONS, CODEGRAPH_OUTPUT_SCHEMA, CODEGRAPH_PARAMETERS, } from "./schema.js";
29
+ export { declarationsOnly, groupByFile, mergeByHits, mergeRelations, taskTerms } from "./compose.js";
30
+ export { toAffected, toHop, toRelation, toSymbol } from "./projection.js";
31
+ export { renderCodegraph } from "./render.js";
32
+ export { readSlice } from "./source.js";
33
+ /** Cordis plugin name for loader diagnostics. */
34
+ export const name = 'tool-codegraph';
35
+ /** Services required by this plugin. */
36
+ export const inject = ['tools', 'codegraph', 'fs', 'systemPrompt'];
37
+ /** Default tool-call timeout budget (ms) for the query-side `codegraph` tool. */
38
+ export const DEFAULT_CODEGRAPH_TOOL_TIMEOUT_MS = 30_000;
39
+ /** Default timeout budget (ms) for the `codegraph_index` tool. Indexing a monorepo is a different order of work than a query. */
40
+ export const DEFAULT_CODEGRAPH_INDEX_TIMEOUT_MS = 300_000;
41
+ /** The stable system-prompt guidance positioning the code graph against search and read. */
42
+ export const CODEGRAPH_PROMPT_TEXT = 'Use codegraph for structural questions about code: where a symbol is declared, what calls it, what it calls, what a change to it reaches, and how one symbol reaches another. It answers from a pre-built index, so it is both faster and more precise than grepping for a name, which also matches comments, strings, and unrelated identifiers. Use search/read instead for literal text, and when codegraph reports no index for a workspace. When status reports no index, call codegraph_index once to build one — it runs on its own, longer timeout budget than a query — then retry. Results reflect the last time the workspace was indexed; a declaration added since then is absent.';
43
+ export const Config = z.object({
44
+ defaultLimit: z.number().default(20),
45
+ maxLimit: z.number().default(200),
46
+ defaultDepth: z.number().default(2),
47
+ maxDepth: z.number().default(6),
48
+ maxPaths: z.number().default(5),
49
+ maxSourceFiles: z.number().default(5),
50
+ maxSourceLines: z.number().default(200),
51
+ maxSourceChars: z.number().default(8000),
52
+ maxDocstringChars: z.number().default(400),
53
+ maxSignatureChars: z.number().default(200),
54
+ maxContextTerms: z.number().default(6),
55
+ timeoutMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_CODEGRAPH_TOOL_TIMEOUT_MS),
56
+ indexTimeoutMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_CODEGRAPH_INDEX_TIMEOUT_MS),
57
+ });
58
+ /**
59
+ * The project root a call runs against: the model's explicit `project_path`, else the calling
60
+ * agent's session workspace. There is no process-cwd fallback — a graph query that silently answered
61
+ * about a different checkout than the session is working in would be wrong in a way the model cannot
62
+ * detect.
63
+ * @param args - the validated tool arguments.
64
+ * @param exec - the tool-execution context; only its optional `agent` is read.
65
+ * @returns the absolute project root.
66
+ */
67
+ export function projectRoot(args, exec) {
68
+ return resolvedProjectRoot(args.project_path, exec);
69
+ }
70
+ /**
71
+ * Shared root-resolution logic behind {@link projectRoot} and the index tool's own argument shape.
72
+ * @param explicit - the model's `project_path`, if given.
73
+ * @param exec - the tool-execution context; only its optional `agent` is read.
74
+ * @returns the absolute project root.
75
+ */
76
+ function resolvedProjectRoot(explicit, exec) {
77
+ if (explicit !== undefined && explicit.trim() !== '')
78
+ return explicit;
79
+ const sessionCwd = exec.agent?.session.header.cwd;
80
+ if (sessionCwd === undefined) {
81
+ throw new CodegraphError('the codegraph tool requires a session workspace or an explicit project_path', 'CODEGRAPH_WORKSPACE_REQUIRED');
82
+ }
83
+ return sessionCwd;
84
+ }
85
+ /** Read a required string argument, rejecting an absent or blank value. */
86
+ function required(args, field) {
87
+ const value = args[field];
88
+ if (value === undefined || value.trim() === '') {
89
+ throw new CodegraphError(`the codegraph "${args.operation}" operation requires a non-empty "${field}"`, 'CODEGRAPH_INVALID_REQUEST');
90
+ }
91
+ return value;
92
+ }
93
+ /** Clamp a model-supplied bound into the configured range. */
94
+ function bounded(value, fallback, max) {
95
+ if (value === undefined || !Number.isFinite(value))
96
+ return fallback;
97
+ return Math.min(max, Math.max(1, Math.trunc(value)));
98
+ }
99
+ /**
100
+ * Register the `codegraph` tool and its system-prompt guidance.
101
+ * @param ctx - the plugin context (must inject `tools`, `codegraph`, `fs`, `systemPrompt`).
102
+ * @param config - the resolved plugin configuration.
103
+ */
104
+ export function apply(ctx, config) {
105
+ const resolved = config;
106
+ for (const field of [
107
+ 'defaultLimit', 'maxLimit', 'defaultDepth', 'maxDepth', 'maxPaths', 'maxSourceFiles',
108
+ 'maxSourceLines', 'maxSourceChars', 'maxDocstringChars', 'maxSignatureChars', 'maxContextTerms',
109
+ ]) {
110
+ assertPositiveInteger(field, resolved[field]);
111
+ }
112
+ assertTimer('timeoutMs', resolved.timeoutMs);
113
+ assertTimer('indexTimeoutMs', resolved.indexTimeoutMs);
114
+ ctx.systemPrompt.section({ name: 'tool:codegraph', order: 111, text: CODEGRAPH_PROMPT_TEXT });
115
+ ctx.tools.register(defineTool({
116
+ name: 'codegraph',
117
+ description: 'Query a pre-built index of the workspace\'s declarations and their relationships. Find where a symbol is declared, what calls it, what it calls, what a change to it can affect, and how one symbol reaches another. More precise than text search: it matches declarations, not occurrences in comments or strings. Answers reflect the last time the workspace was indexed.',
118
+ parameters: CODEGRAPH_PARAMETERS,
119
+ output: {
120
+ schema: CODEGRAPH_OUTPUT_SCHEMA,
121
+ render: (_args, value) => [{ type: 'text', text: renderCodegraph(value) }],
122
+ },
123
+ timeoutMs: resolved.timeoutMs,
124
+ async execute(args, exec) {
125
+ return run(ctx, resolved, args, exec);
126
+ },
127
+ presentCall: args => ({
128
+ card: 'generic',
129
+ title: callTitle(args),
130
+ kind: 'search',
131
+ rawInput: args,
132
+ }),
133
+ }));
134
+ ctx.tools.register(defineTool({
135
+ name: 'codegraph_index',
136
+ description: 'Build or refresh the codegraph index for a workspace, so the codegraph tool can answer. Indexing a large workspace can take minutes, so this runs on its own timeout budget, separate from codegraph\'s query operations.',
137
+ parameters: CODEGRAPH_INDEX_PARAMETERS,
138
+ output: {
139
+ schema: CODEGRAPH_OUTPUT_SCHEMA,
140
+ render: (_args, value) => [{ type: 'text', text: renderCodegraph(value) }],
141
+ },
142
+ timeoutMs: resolved.indexTimeoutMs,
143
+ async execute(args, exec) {
144
+ return runIndex(ctx, args, exec);
145
+ },
146
+ presentCall: args => ({
147
+ card: 'generic',
148
+ title: args.project_path === undefined ? 'codegraph_index' : `codegraph_index ${args.project_path}`,
149
+ kind: 'search',
150
+ rawInput: args,
151
+ }),
152
+ }));
153
+ }
154
+ /**
155
+ * The one-line label a pending call shows.
156
+ * @param args - the validated tool arguments.
157
+ * @returns the card title naming the operation and whichever subject the operation takes.
158
+ */
159
+ export function callTitle(args) {
160
+ const subject = args.symbol ?? args.query ?? args.task
161
+ ?? (args.from === undefined ? undefined : `${args.from} → ${args.to ?? '?'}`)
162
+ ?? args.pattern ?? args.path;
163
+ return subject === undefined ? `codegraph ${args.operation}` : `codegraph ${args.operation} ${subject}`;
164
+ }
165
+ /**
166
+ * Build or refresh the on-disk index for one project. The dedicated tool this backs carries its own,
167
+ * much larger timeout budget than a query — indexing a monorepo is a different order of work.
168
+ * @param ctx - the plugin context.
169
+ * @param args - the index tool's validated arguments.
170
+ * @param exec - the tool-execution context.
171
+ * @returns the index operation's canonical value.
172
+ */
173
+ async function runIndex(ctx, args, exec) {
174
+ const root = resolvedProjectRoot(args.project_path, exec);
175
+ const report = await ctx.codegraph.index(root, exec.signal);
176
+ return {
177
+ operation: 'index',
178
+ project_path: root,
179
+ files_indexed: report.filesIndexed,
180
+ files_skipped: report.filesSkipped,
181
+ symbol_count: report.nodeCount,
182
+ edge_count: report.edgeCount,
183
+ unresolved_count: report.unresolvedCount,
184
+ unresolved_likely_internal_count: report.unresolvedLikelyInternalCount,
185
+ languages: report.languages.map(entry => ({ language: entry.language, file_count: entry.fileCount })),
186
+ };
187
+ }
188
+ /**
189
+ * Answer one tool call.
190
+ * @param ctx - the plugin context.
191
+ * @param config - the resolved plugin configuration.
192
+ * @param args - the validated tool arguments.
193
+ * @param exec - the tool-execution context.
194
+ * @returns the canonical value for the requested operation.
195
+ */
196
+ async function run(ctx, config, args, exec) {
197
+ const root = projectRoot(args, exec);
198
+ const limit = bounded(args.limit, config.defaultLimit, config.maxLimit);
199
+ const depth = bounded(args.depth, config.defaultDepth, config.maxDepth);
200
+ const projection = {
201
+ maxDocstringChars: config.maxDocstringChars,
202
+ maxSignatureChars: config.maxSignatureChars,
203
+ };
204
+ const source = { maxLines: config.maxSourceLines, maxChars: config.maxSourceChars };
205
+ const signal = exec.signal;
206
+ switch (args.operation) {
207
+ case 'search': {
208
+ const result = await ctx.codegraph.query({
209
+ operation: 'search',
210
+ projectRoot: root,
211
+ query: required(args, 'query'),
212
+ ...args.kind === undefined ? {} : { kind: args.kind },
213
+ ...args.language === undefined ? {} : { language: args.language },
214
+ limit,
215
+ }, signal);
216
+ return {
217
+ operation: 'search',
218
+ project_path: root,
219
+ symbols: result.nodes.map(node => toSymbol(node, projection)),
220
+ total: result.total,
221
+ truncated: result.truncated,
222
+ };
223
+ }
224
+ case 'node': {
225
+ const result = await ctx.codegraph.query({
226
+ operation: 'node',
227
+ projectRoot: root,
228
+ symbol: required(args, 'symbol'),
229
+ limit,
230
+ }, signal);
231
+ const symbol = result.node === null ? null : toSymbol(result.node, projection);
232
+ const code = args.include_code === true && result.node !== null
233
+ ? (await readSlice(ctx, root, result.node.filePath, result.node.startLine, result.node.endLine, source, signal)).code
234
+ : null;
235
+ return {
236
+ operation: 'node',
237
+ project_path: root,
238
+ symbol,
239
+ incoming: result.incoming.map(relation => toRelation(relation, projection)),
240
+ outgoing: result.outgoing.map(relation => toRelation(relation, projection)),
241
+ alternatives: result.alternatives.map(node => toSymbol(node, projection)),
242
+ code,
243
+ };
244
+ }
245
+ case 'callers':
246
+ case 'callees': {
247
+ const result = await ctx.codegraph.query({
248
+ operation: args.operation,
249
+ projectRoot: root,
250
+ symbol: required(args, 'symbol'),
251
+ limit,
252
+ }, signal);
253
+ return {
254
+ operation: args.operation,
255
+ project_path: root,
256
+ symbol: result.subject === null ? null : toSymbol(result.subject, projection),
257
+ relations: result.relations.map(relation => toRelation(relation, projection)),
258
+ total: result.total,
259
+ truncated: result.truncated,
260
+ };
261
+ }
262
+ case 'impact': {
263
+ const result = await ctx.codegraph.query({
264
+ operation: 'impact',
265
+ projectRoot: root,
266
+ symbol: required(args, 'symbol'),
267
+ depth,
268
+ limit,
269
+ }, signal);
270
+ return {
271
+ operation: 'impact',
272
+ project_path: root,
273
+ symbol: result.subject === null ? null : toSymbol(result.subject, projection),
274
+ affected: result.entries.map(entry => toAffected(entry, projection)),
275
+ total: result.total,
276
+ truncated: result.truncated,
277
+ };
278
+ }
279
+ case 'trace': {
280
+ const result = await ctx.codegraph.query({
281
+ operation: 'trace',
282
+ projectRoot: root,
283
+ from: required(args, 'from'),
284
+ to: required(args, 'to'),
285
+ maxDepth: depth,
286
+ maxPaths: config.maxPaths,
287
+ }, signal);
288
+ return {
289
+ operation: 'trace',
290
+ project_path: root,
291
+ from: result.from === null ? null : toSymbol(result.from, projection),
292
+ to: result.to === null ? null : toSymbol(result.to, projection),
293
+ paths: result.paths.map(path => path.map(hop => toHop(hop, projection))),
294
+ };
295
+ }
296
+ case 'files': {
297
+ const result = await ctx.codegraph.query({
298
+ operation: 'files',
299
+ projectRoot: root,
300
+ ...args.path === undefined ? {} : { path: args.path },
301
+ ...args.pattern === undefined ? {} : { pattern: args.pattern },
302
+ limit,
303
+ }, signal);
304
+ return {
305
+ operation: 'files',
306
+ project_path: root,
307
+ files: result.files.map(file => ({
308
+ path: file.path,
309
+ language: file.language,
310
+ size: file.size,
311
+ symbol_count: file.nodeCount,
312
+ })),
313
+ total: result.total,
314
+ truncated: result.truncated,
315
+ };
316
+ }
317
+ case 'status': {
318
+ const available = await ctx.codegraph.available(root, signal);
319
+ if (!available) {
320
+ return { operation: 'status', project_path: root, indexed: false };
321
+ }
322
+ const result = await ctx.codegraph.query({ operation: 'status', projectRoot: root }, signal);
323
+ return {
324
+ operation: 'status',
325
+ project_path: root,
326
+ indexed: true,
327
+ file_count: result.fileCount,
328
+ symbol_count: result.nodeCount,
329
+ edge_count: result.edgeCount,
330
+ format_version: result.formatVersion,
331
+ indexed_at: result.indexedAt,
332
+ languages: result.languages.map(entry => ({
333
+ language: entry.language,
334
+ file_count: entry.fileCount,
335
+ })),
336
+ stale_file_count: result.staleFileCount,
337
+ stale_file_count_truncated: result.staleFileCountTruncated,
338
+ };
339
+ }
340
+ case 'explore': {
341
+ const result = await ctx.codegraph.query({
342
+ operation: 'search',
343
+ projectRoot: root,
344
+ query: required(args, 'query'),
345
+ limit,
346
+ }, signal);
347
+ const declarations = declarationsOnly(result.nodes);
348
+ const groups = groupByFile(declarations, config.maxSourceFiles);
349
+ return {
350
+ operation: 'explore',
351
+ project_path: root,
352
+ files: await Promise.all(groups.map(group => explored(ctx, root, group, projection, source, signal))),
353
+ total: result.total,
354
+ truncated: result.truncated || groups.length < countFiles(declarations),
355
+ };
356
+ }
357
+ case 'context': {
358
+ const task = required(args, 'task');
359
+ const terms = taskTerms(task, config.maxContextTerms);
360
+ const batches = await Promise.all(terms.map(async (term) => (await ctx.codegraph.query({
361
+ operation: 'search',
362
+ projectRoot: root,
363
+ query: term,
364
+ limit,
365
+ }, signal)).nodes));
366
+ const ranked = mergeByHits(batches.map(declarationsOnly), limit).map(scored => scored.node);
367
+ const related = await relatedTo(ctx, root, ranked, config, signal);
368
+ const groups = groupByFile(ranked, config.maxSourceFiles);
369
+ return {
370
+ operation: 'context',
371
+ project_path: root,
372
+ task,
373
+ entry_points: ranked.map(node => toSymbol(node, projection)),
374
+ related: related.map(relation => toRelation(relation, projection)),
375
+ files: await Promise.all(groups.map(group => explored(ctx, root, group, projection, source, signal))),
376
+ };
377
+ }
378
+ /* v8 ignore next -- exhaustive over the parameter schema's closed operation enum; unreachable. */
379
+ default:
380
+ return assertNever(args.operation, 'tool-codegraph operation');
381
+ }
382
+ }
383
+ /** How many distinct files a ranked result set spans. */
384
+ function countFiles(nodes) {
385
+ return new Set(nodes.map(node => node.filePath)).size;
386
+ }
387
+ /** Read one file group's source and pair it with the declarations that selected it. */
388
+ async function explored(ctx, root, group, projection, source, signal) {
389
+ const slice = await readSlice(ctx, root, group.path, group.startLine, group.endLine, source, signal);
390
+ return {
391
+ path: group.path,
392
+ symbols: group.nodes.map(node => toSymbol(node, projection)),
393
+ code: slice.code,
394
+ ...slice.startLine === undefined ? {} : { code_start_line: slice.startLine },
395
+ truncated: slice.truncated,
396
+ };
397
+ }
398
+ /**
399
+ * Callers and callees of the highest-ranked declarations a task matched.
400
+ *
401
+ * Only the top declarations are expanded: a task's context is the neighbourhood of what it is about,
402
+ * and querying every match's relations would spend the result budget on the tail of the ranking.
403
+ * @param ctx - the plugin context.
404
+ * @param root - the project root.
405
+ * @param ranked - the task's matched declarations, most relevant first.
406
+ * @param config - the resolved plugin configuration.
407
+ * @param signal - aborts the queries.
408
+ * @returns the merged relations.
409
+ */
410
+ async function relatedTo(ctx, root, ranked, config, signal) {
411
+ const seeds = ranked.slice(0, config.maxSourceFiles);
412
+ const batches = await Promise.all(seeds.flatMap(node => ['callers', 'callees'].map(async (operation) => (await ctx.codegraph.query({
413
+ operation,
414
+ projectRoot: root,
415
+ symbol: node.qualifiedName,
416
+ limit: config.defaultLimit,
417
+ }, signal)).relations)));
418
+ return mergeRelations(batches, config.defaultLimit);
419
+ }
420
+ /** Reject a non-positive-integer config value at load, so misconfiguration fails loud. */
421
+ function assertPositiveInteger(field, value) {
422
+ if (!Number.isInteger(value) || value < 1) {
423
+ throw new Error(`tool-codegraph: ${field} must be a positive integer`);
424
+ }
425
+ }
426
+ /** Reject a timer value Node would clamp instead of scheduling as configured. */
427
+ function assertTimer(field, value) {
428
+ if (!Number.isInteger(value) || value < 1 || value > MAX_TIMER_DELAY_MS) {
429
+ throw new Error(`tool-codegraph: ${field} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`);
430
+ }
431
+ }
432
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-plugin-codegraph-tool`.
3
+ * @module dsh-plugin-codegraph-tool/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "tool-codegraph-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-plugin-codegraph-tool`.
3
+ * @module dsh-plugin-codegraph-tool/invariant
4
+ */
5
+ const PACKAGE_NAME = 'dsh-plugin-codegraph-tool';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'tool-codegraph-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * No runtime invariant: this stateless adapter contributes one tool and prompt section, while graph
12
+ * query results and source retrieval remain owned by the codegraph and filesystem seams it composes.
13
+ */
14
+ const install = () => { };
15
+ /**
16
+ * Register this package's invariant companion.
17
+ * @param ctx - Cordis context carrying the invariant service.
18
+ * @returns the installed registration's disposer after setup succeeds.
19
+ */
20
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
21
+ /* jscpd:ignore-end */
22
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Seam records to model-facing values. The tool owns this projection because the seam's vocabulary
3
+ * describes a graph while the model's describes code: `filePath`/`startLine` become `path`/`line`,
4
+ * an edge's kind becomes `via`, and the fields a model cannot act on — opaque node ids, index
5
+ * timestamps, provenance — are dropped rather than spent as tokens.
6
+ * @module @huanlin/dsh-plugin-codegraph-tool/projection
7
+ */
8
+ import type { CodegraphImpactEntry, CodegraphNode, CodegraphRelation, CodegraphTraceHop } from '@huanlin/dsh-plugin-codegraph-service';
9
+ /** Caps applied while projecting, so one enormous doc comment cannot dominate a result. */
10
+ export interface ProjectionLimits {
11
+ /** Largest documentation string carried per symbol, in characters. */
12
+ readonly maxDocstringChars: number;
13
+ /** Largest signature carried per symbol, in characters. */
14
+ readonly maxSignatureChars: number;
15
+ }
16
+ /** The model-facing shape of one declaration. */
17
+ export interface SymbolView {
18
+ readonly name: string;
19
+ readonly qualified_name: string;
20
+ readonly kind: string;
21
+ readonly path: string;
22
+ readonly line: number;
23
+ readonly end_line: number;
24
+ readonly language: string;
25
+ readonly exported: boolean;
26
+ readonly signature?: string;
27
+ readonly docstring?: string;
28
+ }
29
+ /**
30
+ * Project one declaration.
31
+ * @param node - the seam's node record.
32
+ * @param limits - the caps to apply to free text.
33
+ * @returns the model-facing symbol.
34
+ */
35
+ export declare function toSymbol(node: CodegraphNode, limits: ProjectionLimits): SymbolView;
36
+ /** A symbol plus how it was reached. */
37
+ export interface RelationView extends SymbolView {
38
+ readonly via: string;
39
+ readonly site_line?: number;
40
+ readonly site_count: number;
41
+ }
42
+ /**
43
+ * Project one related declaration.
44
+ * @param relation - the seam's relation record.
45
+ * @param limits - the caps to apply to free text.
46
+ * @returns the model-facing relation.
47
+ */
48
+ export declare function toRelation(relation: CodegraphRelation, limits: ProjectionLimits): RelationView;
49
+ /** A symbol reached by a transitive walk. */
50
+ export interface AffectedView extends SymbolView {
51
+ readonly via: string;
52
+ readonly distance: number;
53
+ }
54
+ /**
55
+ * Project one impact entry.
56
+ * @param entry - the seam's impact record.
57
+ * @param limits - the caps to apply to free text.
58
+ * @returns the model-facing affected symbol.
59
+ */
60
+ export declare function toAffected(entry: CodegraphImpactEntry, limits: ProjectionLimits): AffectedView;
61
+ /** One hop of a traced path. */
62
+ export interface HopView extends SymbolView {
63
+ readonly via?: string;
64
+ readonly site_line?: number;
65
+ }
66
+ /**
67
+ * Project one traced hop.
68
+ * @param hop - the seam's hop record.
69
+ * @param limits - the caps to apply to free text.
70
+ * @returns the model-facing hop.
71
+ */
72
+ export declare function toHop(hop: CodegraphTraceHop, limits: ProjectionLimits): HopView;
73
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Seam records to model-facing values. The tool owns this projection because the seam's vocabulary
3
+ * describes a graph while the model's describes code: `filePath`/`startLine` become `path`/`line`,
4
+ * an edge's kind becomes `via`, and the fields a model cannot act on — opaque node ids, index
5
+ * timestamps, provenance — are dropped rather than spent as tokens.
6
+ * @module @huanlin/dsh-plugin-codegraph-tool/projection
7
+ */
8
+ /** Cut a string to a cap, marking the cut so a truncated value never reads as complete. */
9
+ function clip(value, max) {
10
+ return value.length <= max ? value : `${value.slice(0, max)}…`;
11
+ }
12
+ /**
13
+ * Project one declaration.
14
+ * @param node - the seam's node record.
15
+ * @param limits - the caps to apply to free text.
16
+ * @returns the model-facing symbol.
17
+ */
18
+ export function toSymbol(node, limits) {
19
+ return {
20
+ name: node.name,
21
+ qualified_name: node.qualifiedName,
22
+ kind: node.kind,
23
+ path: node.filePath,
24
+ line: node.startLine,
25
+ end_line: node.endLine,
26
+ language: node.language,
27
+ exported: node.isExported,
28
+ ...node.signature === undefined ? {} : { signature: clip(node.signature, limits.maxSignatureChars) },
29
+ ...node.docstring === undefined ? {} : { docstring: clip(node.docstring, limits.maxDocstringChars) },
30
+ };
31
+ }
32
+ /**
33
+ * Project one related declaration.
34
+ * @param relation - the seam's relation record.
35
+ * @param limits - the caps to apply to free text.
36
+ * @returns the model-facing relation.
37
+ */
38
+ export function toRelation(relation, limits) {
39
+ return {
40
+ ...toSymbol(relation.node, limits),
41
+ via: relation.edge.kind,
42
+ ...relation.edge.line === undefined ? {} : { site_line: relation.edge.line },
43
+ site_count: relation.siteCount,
44
+ };
45
+ }
46
+ /**
47
+ * Project one impact entry.
48
+ * @param entry - the seam's impact record.
49
+ * @param limits - the caps to apply to free text.
50
+ * @returns the model-facing affected symbol.
51
+ */
52
+ export function toAffected(entry, limits) {
53
+ return { ...toSymbol(entry.node, limits), via: entry.via, distance: entry.distance };
54
+ }
55
+ /**
56
+ * Project one traced hop.
57
+ * @param hop - the seam's hop record.
58
+ * @param limits - the caps to apply to free text.
59
+ * @returns the model-facing hop.
60
+ */
61
+ export function toHop(hop, limits) {
62
+ return {
63
+ ...toSymbol(hop.node, limits),
64
+ ...hop.edge === undefined ? {} : { via: hop.edge.kind },
65
+ ...hop.edge?.line === undefined ? {} : { site_line: hop.edge.line },
66
+ };
67
+ }
68
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,16 @@
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 type { CodegraphToolValue } from './schema.ts';
10
+ /**
11
+ * Render one result as the text the model reads.
12
+ * @param value - the canonical value the operation returned.
13
+ * @returns the rendered text.
14
+ */
15
+ export declare function renderCodegraph(value: CodegraphToolValue): string;
16
+ //# sourceMappingURL=render.d.ts.map