pi-usereq 0.11.0 → 0.13.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 (53) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +1135 -834
  5. package/pi-usereq/docs/REQUIREMENTS.md +177 -110
  6. package/pi-usereq/docs/WORKFLOW.md +244 -79
  7. package/scripts/lib/extension-debug-harness.ts +2 -2
  8. package/scripts/tool-args-to-params.ts +2 -2
  9. package/src/cli.ts +12 -12
  10. package/src/core/config.ts +541 -180
  11. package/src/core/extension-status.ts +69 -12
  12. package/src/core/path-context.ts +19 -4
  13. package/src/core/pi-notify.ts +5 -5
  14. package/src/core/pi-usereq-tools.ts +4 -2
  15. package/src/core/prompt-command-catalog.ts +4 -5
  16. package/src/core/prompt-command-runtime.ts +183 -44
  17. package/src/core/prompts.ts +0 -2
  18. package/src/core/req-references-command.ts +175 -0
  19. package/src/core/req-reset-command.ts +323 -0
  20. package/src/core/resources.ts +6 -23
  21. package/src/core/settings-menu.ts +85 -28
  22. package/src/core/tool-runner.ts +26 -6
  23. package/src/index.ts +601 -116
  24. package/tests/attended-results-scenarios.ts +15 -9
  25. package/tests/cli-command-option-parity.test.ts +53 -35
  26. package/tests/debug-extension-harness.test.ts +8 -10
  27. package/tests/extension-registration.test.ts +1204 -205
  28. package/tests/helpers.ts +29 -6
  29. package/tests/oracle-project.test.ts +4 -4
  30. package/tests/oracle-standalone.test.ts +5 -5
  31. package/src/core/reference-payload.ts +0 -752
  32. package/src/resources/prompts/references.md +0 -64
  33. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  50. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  51. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  52. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  53. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
@@ -1,752 +0,0 @@
1
- /**
2
- * @file
3
- * @brief Builds agent-oriented JSON payloads for `files-references` and `references`.
4
- * @details Converts analyzed source files into deterministic JSON sections ordered for LLM traversal, including repository structure, per-file metrics, imports, symbols, structured Doxygen fields, and structured comment evidence. Runtime is O(F log F + S) where F is file count and S is total source size. Side effects are limited to filesystem reads and optional stderr logging.
5
- */
6
-
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import {
10
- ElementType,
11
- SourceAnalyzer,
12
- type SourceElement,
13
- collectElementDoxygenFields,
14
- collectFileLevelDoxygenFields,
15
- } from "./source-analyzer.js";
16
- import { detectLanguage } from "./compress.js";
17
- import {
18
- countDoxygenFieldValues,
19
- structureDoxygenFields,
20
- type StructuredDoxygenFields,
21
- } from "./doxygen-parser.js";
22
-
23
- /**
24
- * @brief Enumerates supported references-payload scopes.
25
- * @details Distinguishes explicit-file requests from configured project scans while preserving one stable JSON contract. The alias is compile-time only and introduces no runtime cost.
26
- */
27
- export type ReferenceToolScope = "explicit-files" | "configured-source-directories";
28
-
29
- /**
30
- * @brief Enumerates supported per-file references entry statuses.
31
- * @details Separates analyzed files, analysis failures, and skipped inputs so downstream agents can branch without reparsing stderr text. The alias is compile-time only and introduces no runtime cost.
32
- */
33
- export type ReferenceFileStatus = "analyzed" | "error" | "skipped";
34
-
35
- /**
36
- * @brief Describes one numeric source line range.
37
- * @details Exposes start and end line numbers plus the same inclusive range as a numeric tuple for direct agent access. The interface is compile-time only and introduces no runtime cost.
38
- */
39
- export interface ReferenceLineRange {
40
- start_line_number: number;
41
- end_line_number: number;
42
- line_range: [number, number];
43
- }
44
-
45
- /**
46
- * @brief Describes one structured import record.
47
- * @details Stores the normalized import identity, raw import statement, and declaration line range without requiring agents to parse markdown blocks. The interface is compile-time only and introduces no runtime cost.
48
- */
49
- export interface ReferenceImportEntry extends ReferenceLineRange {
50
- import_name: string;
51
- statement_text: string;
52
- }
53
-
54
- /**
55
- * @brief Describes one structured standalone or attached comment record.
56
- * @details Preserves normalized comment text plus per-line comment fragments so agents can consume comment evidence without reparsing source delimiters. The interface is compile-time only and introduces no runtime cost.
57
- */
58
- export interface ReferenceCommentEntry extends ReferenceLineRange {
59
- text: string;
60
- text_lines: string[];
61
- }
62
-
63
- /**
64
- * @brief Describes one structured exit-point annotation.
65
- * @details Preserves the normalized exit expression text together with its source line number for downstream reasoning about control flow hints. The interface is compile-time only and introduces no runtime cost.
66
- */
67
- export interface ReferenceExitPointEntry {
68
- line_number: number;
69
- text: string;
70
- }
71
-
72
- /**
73
- * @brief Describes one structured symbol record.
74
- * @details Orders direct-access identity fields before hierarchy, locations, Doxygen metadata, and comment evidence so agents can branch without reparsing monolithic summaries. The interface is compile-time only and introduces no runtime cost.
75
- */
76
- export interface ReferenceSymbolEntry extends ReferenceLineRange {
77
- declaration_order_index: number;
78
- symbol_name: string;
79
- qualified_name: string;
80
- symbol_kind: string;
81
- type_label: string;
82
- signature_text?: string;
83
- visibility?: string;
84
- parent_symbol_name?: string;
85
- parent_qualified_name?: string;
86
- child_symbol_names: string[];
87
- child_qualified_names: string[];
88
- depth: number;
89
- inherits_text?: string;
90
- decorator_text?: string;
91
- attached_comment_summary_text?: string;
92
- attached_comment_lines?: string[];
93
- doxygen?: StructuredDoxygenFields;
94
- body_comment_entries: ReferenceCommentEntry[];
95
- exit_point_entries: ReferenceExitPointEntry[];
96
- }
97
-
98
- /**
99
- * @brief Describes one per-file references payload entry.
100
- * @details Stores canonical identity, line metrics, structured imports, structured symbols, structured comment evidence, and optional file-level Doxygen metadata. Derivable identity and filesystem-probe fields are intentionally omitted to reduce token cost. The interface is compile-time only and introduces no runtime cost.
101
- */
102
- export interface ReferenceToolFileEntry extends ReferenceLineRange {
103
- canonical_path: string;
104
- status: ReferenceFileStatus;
105
- import_count: number;
106
- symbol_count: number;
107
- comment_count: number;
108
- standalone_comment_count: number;
109
- doxygen_field_count: number;
110
- file_doxygen?: StructuredDoxygenFields;
111
- imports: ReferenceImportEntry[];
112
- symbols: ReferenceSymbolEntry[];
113
- standalone_comments: ReferenceCommentEntry[];
114
- error_message?: string;
115
- }
116
-
117
- /**
118
- * @brief Describes the request section of the references payload.
119
- * @details Captures tool identity, scope, base directory, requested path inventory, and configured source-directory scope so agents can reason about how the file set was selected. The interface is compile-time only and introduces no runtime cost.
120
- */
121
- export interface ReferenceToolRequestSection {
122
- tool_name: string;
123
- scope: ReferenceToolScope;
124
- base_dir_path: string;
125
- source_directory_count: number;
126
- source_directory_paths: string[];
127
- requested_file_count: number;
128
- requested_input_paths: string[];
129
- requested_canonical_paths: string[];
130
- }
131
-
132
- /**
133
- * @brief Describes the summary section of the references payload.
134
- * @details Exposes aggregate file, symbol, import, comment, and Doxygen counts as numeric fields plus deterministic symbol-kind totals. The interface is compile-time only and introduces no runtime cost.
135
- */
136
- export interface ReferenceToolSummarySection {
137
- processable_file_count: number;
138
- analyzed_file_count: number;
139
- error_file_count: number;
140
- skipped_file_count: number;
141
- total_symbol_count: number;
142
- total_import_count: number;
143
- total_comment_count: number;
144
- total_standalone_comment_count: number;
145
- total_doxygen_field_count: number;
146
- symbol_kind_counts: Record<string, number>;
147
- }
148
-
149
- /**
150
- * @brief Describes one repository tree node in the references payload.
151
- * @details Encodes directory and file hierarchy without ASCII-art decoration so agents can traverse repository structure as structured JSON. The interface is compile-time only and introduces no runtime cost.
152
- */
153
- export interface ReferenceRepositoryTreeNode {
154
- node_name: string;
155
- relative_path: string;
156
- node_kind: "directory" | "file";
157
- child_count: number;
158
- children: ReferenceRepositoryTreeNode[];
159
- }
160
-
161
- /**
162
- * @brief Describes the repository section of the references payload.
163
- * @details Stores configured source-directory scope, the canonical analyzed file list, and the structured directory tree used during analysis. Static root-path echoes are intentionally omitted to reduce token cost. The interface is compile-time only and introduces no runtime cost.
164
- */
165
- export interface ReferenceToolRepositorySection {
166
- source_directory_paths: string[];
167
- file_canonical_paths: string[];
168
- directory_tree: ReferenceRepositoryTreeNode;
169
- }
170
-
171
- /**
172
- * @brief Describes the full agent-oriented references payload.
173
- * @details Exposes only aggregate analysis totals, repository structure, and per-file reference records, omitting request echoes that are already known to the caller or encoded in the tool registration. The interface is compile-time only and introduces no runtime cost.
174
- */
175
- export interface ReferenceToolPayload {
176
- summary: ReferenceToolSummarySection;
177
- repository: ReferenceToolRepositorySection;
178
- files: ReferenceToolFileEntry[];
179
- }
180
-
181
- /**
182
- * @brief Describes the options required to build one references payload.
183
- * @details Supplies tool identity, scope, base directory, requested paths, and optional configured source directories while keeping payload construction deterministic. The interface is compile-time only and introduces no runtime cost.
184
- */
185
- export interface BuildReferenceToolPayloadOptions {
186
- toolName: string;
187
- scope: ReferenceToolScope;
188
- baseDir: string;
189
- requestedPaths: string[];
190
- sourceDirectoryPaths?: string[];
191
- verbose?: boolean;
192
- }
193
-
194
- /**
195
- * @brief Canonicalizes one filesystem path relative to the payload base directory.
196
- * @details Emits a slash-normalized relative path when the target is under the base directory; otherwise emits the normalized absolute path. Runtime is O(p) in path length. No side effects occur.
197
- * @param[in] targetPath {string} Absolute or relative filesystem path.
198
- * @param[in] baseDir {string} Base directory used for relative canonicalization.
199
- * @return {string} Canonicalized path string.
200
- */
201
- function canonicalizeReferencePath(targetPath: string, baseDir: string): string {
202
- const absolutePath = path.resolve(targetPath);
203
- const absoluteBaseDir = path.resolve(baseDir);
204
- const relativePath = path.relative(absoluteBaseDir, absolutePath).split(path.sep).join("/");
205
- if (relativePath !== "" && relativePath !== "." && !relativePath.startsWith("../") && relativePath !== "..") {
206
- return relativePath;
207
- }
208
- return absolutePath.split(path.sep).join("/");
209
- }
210
-
211
- /**
212
- * @brief Builds one structured line-range record.
213
- * @details Duplicates the inclusive range as start, end, and tuple fields so callers can address whichever shape is most convenient. Runtime is O(1). No side effects occur.
214
- * @param[in] startLineNumber {number} Inclusive start line number.
215
- * @param[in] endLineNumber {number} Inclusive end line number.
216
- * @return {ReferenceLineRange} Structured line-range record.
217
- */
218
- function buildLineRange(startLineNumber: number, endLineNumber: number): ReferenceLineRange {
219
- return {
220
- start_line_number: startLineNumber,
221
- end_line_number: endLineNumber,
222
- line_range: [startLineNumber, endLineNumber],
223
- };
224
- }
225
-
226
- /**
227
- * @brief Extracts normalized plain text from one comment element.
228
- * @details Removes language comment markers, drops delimiter-only lines, joins content with spaces, and optionally truncates the result. Runtime is O(n) in comment length. No side effects occur.
229
- * @param[in] commentElement {SourceElement} Comment element.
230
- * @param[in] maxLength {number} Optional maximum output length; `0` disables truncation.
231
- * @return {string} Cleaned comment text.
232
- */
233
- function extractCommentText(commentElement: SourceElement, maxLength = 0): string {
234
- const cleaned: string[] = [];
235
- for (const line of commentElement.extract.split("\n")) {
236
- let value = line.trim();
237
- for (const prefix of ["///", "//!", "//", "#!", "##", "#", "--", ";;"]) {
238
- if (value.startsWith(prefix)) {
239
- value = value.slice(prefix.length).trim();
240
- break;
241
- }
242
- }
243
- value = value.replace(/^[/*"']+|[/*"']+$/g, "").trim();
244
- if (value && !value.startsWith("=begin") && !value.startsWith("=end")) {
245
- cleaned.push(value);
246
- }
247
- }
248
- let text = cleaned.join(" ");
249
- if (maxLength > 0 && text.length > maxLength) {
250
- text = `${text.slice(0, maxLength - 3)}...`;
251
- }
252
- return text;
253
- }
254
-
255
- /**
256
- * @brief Extracts cleaned individual lines from one comment element.
257
- * @details Removes language comment markers while preserving line granularity for structured comment payloads. Runtime is O(n) in comment length. No side effects occur.
258
- * @param[in] commentElement {SourceElement} Comment element.
259
- * @return {string[]} Cleaned comment lines.
260
- */
261
- function extractCommentLines(commentElement: SourceElement): string[] {
262
- return commentElement.extract
263
- .split("\n")
264
- .map((line) => {
265
- let value = line.trim();
266
- for (const prefix of ["///", "//!", "//", "#!", "##", "#", "--", ";;"]) {
267
- if (value.startsWith(prefix)) {
268
- value = value.slice(prefix.length).trim();
269
- break;
270
- }
271
- }
272
- return value.replace(/^[/*"']+|[/*"']+$/g, "").trim();
273
- })
274
- .filter((line) => !!line && !line.startsWith("=begin") && !line.startsWith("=end"));
275
- }
276
-
277
- /**
278
- * @brief Associates nearby comment blocks with definitions and standalone comment groups.
279
- * @details Reuses the repository comment-attachment heuristic that binds comments within three lines of a definition while preserving early file-description text. Runtime is O(n log n). No side effects occur.
280
- * @param[in] elements {SourceElement[]} Analyzed source elements.
281
- * @return {[Record<number, SourceElement[]>, SourceElement[], string]} Attached-comment map, standalone comments, and compact file description.
282
- */
283
- function buildCommentMaps(elements: SourceElement[]): [Record<number, SourceElement[]>, SourceElement[], string] {
284
- const sorted = [...elements].sort((left, right) => left.lineStart - right.lineStart);
285
- const definitionTypes = new Set(
286
- Object.values(ElementType).filter(
287
- (value) => ![ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI, ElementType.IMPORT, ElementType.DECORATOR].includes(value as ElementType),
288
- ) as ElementType[],
289
- );
290
- const definitionStarts = new Set(elements.filter((element) => definitionTypes.has(element.elementType)).map((element) => element.lineStart));
291
- const importStarts = new Set(elements.filter((element) => element.elementType === ElementType.IMPORT).map((element) => element.lineStart));
292
- const comments = sorted.filter((element) => [ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI].includes(element.elementType));
293
- const docForDef: Record<number, SourceElement[]> = {};
294
- const standaloneComments: SourceElement[] = [];
295
- let fileDescription = "";
296
-
297
- for (const comment of comments) {
298
- if (comment.lineStart > 10) {
299
- break;
300
- }
301
- const text = extractCommentText(comment);
302
- if (text && !text.startsWith("/usr/") && !text.startsWith("usr/")) {
303
- fileDescription = text.length > 200 ? `${text.slice(0, 197)}...` : text;
304
- break;
305
- }
306
- }
307
-
308
- comments.forEach((comment) => {
309
- if (comment.name === "inline") {
310
- return;
311
- }
312
- let attached = false;
313
- for (let gap = 1; gap < 4; gap += 1) {
314
- const targetLine = comment.lineEnd + gap;
315
- if (definitionStarts.has(targetLine)) {
316
- docForDef[targetLine] ??= [];
317
- docForDef[targetLine].push(comment);
318
- attached = true;
319
- break;
320
- }
321
- if (importStarts.has(targetLine)) {
322
- break;
323
- }
324
- }
325
- if (!attached && comment !== comments[0]) {
326
- standaloneComments.push(comment);
327
- } else if (!attached && comment === comments[0] && !fileDescription) {
328
- standaloneComments.push(comment);
329
- }
330
- });
331
-
332
- return [docForDef, standaloneComments, fileDescription];
333
- }
334
-
335
- /**
336
- * @brief Resolves one stable symbol name from an analyzed element.
337
- * @details Prefers explicit analyzer name metadata, then falls back to the derived signature or the first source line so every symbol retains a direct-access identifier. Runtime is O(1). No side effects occur.
338
- * @param[in] element {SourceElement} Source element.
339
- * @return {string} Stable symbol name.
340
- */
341
- function resolveSymbolName(element: SourceElement): string {
342
- return element.name ?? element.signature ?? ((element.extract.split("\n")[0] ?? "").trim() || "?");
343
- }
344
-
345
- /**
346
- * @brief Resolves the direct parent element for one child symbol.
347
- * @details Matches by parent name plus inclusive line containment and chooses the deepest enclosing definition. Runtime is O(n) in definition count. No side effects occur.
348
- * @param[in] definitions {SourceElement[]} Sorted definition elements.
349
- * @param[in] child {SourceElement} Candidate child symbol.
350
- * @return {SourceElement | undefined} Matched parent definition when available.
351
- */
352
- function resolveParentElement(definitions: SourceElement[], child: SourceElement): SourceElement | undefined {
353
- if (!child.parentName) {
354
- return undefined;
355
- }
356
- return definitions
357
- .filter(
358
- (candidate) => candidate.name === child.parentName && candidate.lineStart <= child.lineStart && candidate.lineEnd >= child.lineEnd,
359
- )
360
- .sort((left, right) => right.depth - left.depth || right.lineStart - left.lineStart)[0];
361
- }
362
-
363
- /**
364
- * @brief Builds one structured comment record from a comment element.
365
- * @details Preserves numeric line-range metadata plus normalized text and per-line fragments. Runtime is O(n) in comment length. No side effects occur.
366
- * @param[in] commentElement {SourceElement} Source comment element.
367
- * @return {ReferenceCommentEntry} Structured comment record.
368
- */
369
- function buildCommentEntry(commentElement: SourceElement): ReferenceCommentEntry {
370
- const textLines = extractCommentLines(commentElement);
371
- return {
372
- ...buildLineRange(commentElement.lineStart, commentElement.lineEnd),
373
- text: extractCommentText(commentElement),
374
- text_lines: textLines,
375
- };
376
- }
377
-
378
- /**
379
- * @brief Builds one structured repository tree from canonical file paths.
380
- * @details Materializes a nested directory map and converts it into recursively ordered JSON nodes without decorative ASCII formatting. Runtime is O(n log n) in path count. No side effects occur.
381
- * @param[in] canonicalPaths {string[]} Canonical file paths.
382
- * @return {ReferenceRepositoryTreeNode} Structured repository tree rooted at `.`.
383
- */
384
- function buildRepositoryTree(canonicalPaths: string[]): ReferenceRepositoryTreeNode {
385
- const root: ReferenceRepositoryTreeNode = {
386
- node_name: ".",
387
- relative_path: ".",
388
- node_kind: "directory",
389
- child_count: 0,
390
- children: [],
391
- };
392
-
393
- const ensureDirectory = (parent: ReferenceRepositoryTreeNode, nodeName: string, relativePath: string): ReferenceRepositoryTreeNode => {
394
- const existing = parent.children.find((child) => child.node_kind === "directory" && child.node_name === nodeName);
395
- if (existing) {
396
- return existing;
397
- }
398
- const created: ReferenceRepositoryTreeNode = {
399
- node_name: nodeName,
400
- relative_path: relativePath,
401
- node_kind: "directory",
402
- child_count: 0,
403
- children: [],
404
- };
405
- parent.children.push(created);
406
- return created;
407
- };
408
-
409
- [...canonicalPaths].sort((left, right) => left.localeCompare(right)).forEach((canonicalPath) => {
410
- const parts = canonicalPath.split("/").filter(Boolean);
411
- let cursor = root;
412
- parts.forEach((part, index) => {
413
- const relativePath = index === 0 ? part : `${cursor.relative_path === "." ? "" : `${cursor.relative_path}/`}${part}`;
414
- const isLeaf = index === parts.length - 1;
415
- if (isLeaf) {
416
- cursor.children.push({
417
- node_name: part,
418
- relative_path: relativePath,
419
- node_kind: "file",
420
- child_count: 0,
421
- children: [],
422
- });
423
- } else {
424
- cursor = ensureDirectory(cursor, part, relativePath);
425
- }
426
- });
427
- });
428
-
429
- const finalizeNode = (node: ReferenceRepositoryTreeNode): ReferenceRepositoryTreeNode => {
430
- node.children = node.children
431
- .map((child) => finalizeNode(child))
432
- .sort((left, right) => {
433
- if (left.node_kind !== right.node_kind) {
434
- return left.node_kind === "directory" ? -1 : 1;
435
- }
436
- return left.node_name.localeCompare(right.node_name);
437
- });
438
- node.child_count = node.children.length;
439
- return node;
440
- };
441
-
442
- return finalizeNode(root);
443
- }
444
-
445
- /**
446
- * @brief Builds one analyzed file entry for the references payload.
447
- * @details Parses the file with `SourceAnalyzer`, extracts structured imports and symbols, attaches structured Doxygen fields, and preserves standalone comment evidence. Runtime is O(S log S) in file size and symbol count. Side effects are limited to filesystem reads and optional stderr logging.
448
- * @param[in] analyzer {SourceAnalyzer} Shared source analyzer instance.
449
- * @param[in] inputPath {string} Caller-provided input path.
450
- * @param[in] absolutePath {string} Absolute file path.
451
- * @param[in] requestIndex {number} Zero-based request index.
452
- * @param[in] baseDir {string} Base directory used for canonical paths.
453
- * @param[in] verbose {boolean} When `true`, emit per-file progress diagnostics to stderr.
454
- * @return {ReferenceToolFileEntry} Structured file entry.
455
- */
456
- function analyzeReferenceFile(
457
- analyzer: SourceAnalyzer,
458
- inputPath: string,
459
- absolutePath: string,
460
- requestIndex: number,
461
- baseDir: string,
462
- verbose: boolean,
463
- ): ReferenceToolFileEntry {
464
- const canonicalPath = canonicalizeReferencePath(absolutePath, baseDir);
465
- const languageId = detectLanguage(absolutePath);
466
- if (!languageId) {
467
- if (verbose) {
468
- console.error(` SKIP ${inputPath} (unsupported extension)`);
469
- }
470
- return {
471
- ...buildLineRange(0, 0),
472
- canonical_path: canonicalPath,
473
- status: "skipped",
474
- import_count: 0,
475
- symbol_count: 0,
476
- comment_count: 0,
477
- standalone_comment_count: 0,
478
- doxygen_field_count: 0,
479
- imports: [],
480
- symbols: [],
481
- standalone_comments: [],
482
- error_message: "unsupported extension",
483
- };
484
- }
485
-
486
- try {
487
- const elements = analyzer.analyze(absolutePath, languageId);
488
- analyzer.enrich(elements, languageId, absolutePath);
489
- const fileContent = fs.readFileSync(absolutePath, "utf8");
490
- const lineCount = fileContent === "" ? 0 : fileContent.split(/\r?\n/).length - (fileContent.endsWith("\n") ? 1 : 0);
491
- const [docForDef, standaloneCommentsRaw] = buildCommentMaps(elements);
492
- const fileLevelDoxygen = collectFileLevelDoxygenFields(elements);
493
- const structuredFileDoxygen = structureDoxygenFields(fileLevelDoxygen);
494
- const imports = elements
495
- .filter((element) => element.elementType === ElementType.IMPORT)
496
- .sort((left, right) => left.lineStart - right.lineStart)
497
- .map((element) => ({
498
- ...buildLineRange(element.lineStart, element.lineEnd),
499
- import_name: resolveSymbolName(element),
500
- statement_text: (element.extract.split("\n")[0] ?? "").trim(),
501
- } satisfies ReferenceImportEntry));
502
- const standaloneComments = standaloneCommentsRaw.map((comment) => buildCommentEntry(comment));
503
- const definitions = elements
504
- .filter((element) => ![ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI, ElementType.IMPORT, ElementType.DECORATOR].includes(element.elementType))
505
- .sort((left, right) => left.lineStart - right.lineStart);
506
- const decoratorByTargetLine = new Map<number, string>();
507
- elements
508
- .filter((element) => element.elementType === ElementType.DECORATOR)
509
- .forEach((element) => decoratorByTargetLine.set(element.lineStart + 1, (element.extract.split("\n")[0] ?? "").trim()));
510
- const parentByLineStart = new Map<number, SourceElement | undefined>();
511
- definitions.forEach((element) => parentByLineStart.set(element.lineStart, resolveParentElement(definitions, element)));
512
- const qualifiedNameByLineStart = new Map<number, string>();
513
-
514
- const symbols = definitions.map((element, index) => {
515
- const symbolName = resolveSymbolName(element);
516
- const parentElement = parentByLineStart.get(element.lineStart);
517
- const parentSymbolName = parentElement ? resolveSymbolName(parentElement) : undefined;
518
- const parentQualifiedName = parentElement ? qualifiedNameByLineStart.get(parentElement.lineStart) : undefined;
519
- const qualifiedName = parentQualifiedName ? `${parentQualifiedName}.${symbolName}` : symbolName;
520
- qualifiedNameByLineStart.set(element.lineStart, qualifiedName);
521
-
522
- const aggregateDoxygen = collectElementDoxygenFields(element);
523
- const structuredDoxygen = structureDoxygenFields(aggregateDoxygen);
524
- const attachedComments = docForDef[element.lineStart] ?? [];
525
- const attachedCommentSummaryText = structuredDoxygen.brief?.[0] ?? attachedComments.map((comment) => extractCommentText(comment, 150)).find(Boolean);
526
- const attachedCommentLines = Object.keys(structuredDoxygen).length > 0
527
- ? undefined
528
- : attachedComments.flatMap((comment) => extractCommentLines(comment));
529
-
530
- return {
531
- ...buildLineRange(element.lineStart, element.lineEnd),
532
- declaration_order_index: index,
533
- symbol_name: symbolName,
534
- qualified_name: qualifiedName,
535
- symbol_kind: element.elementType,
536
- type_label: element.typeLabel,
537
- signature_text: element.signature,
538
- visibility: element.visibility,
539
- parent_symbol_name: parentSymbolName,
540
- parent_qualified_name: parentQualifiedName,
541
- child_symbol_names: [] as string[],
542
- child_qualified_names: [] as string[],
543
- depth: element.depth,
544
- inherits_text: element.inherits,
545
- decorator_text: decoratorByTargetLine.get(element.lineStart),
546
- attached_comment_summary_text: attachedCommentSummaryText,
547
- attached_comment_lines: attachedCommentLines && attachedCommentLines.length > 0 ? attachedCommentLines : undefined,
548
- doxygen: Object.keys(structuredDoxygen).length > 0 ? structuredDoxygen : undefined,
549
- body_comment_entries: element.bodyComments.map(([startLineNumber, endLineNumber, text]) => ({
550
- ...buildLineRange(startLineNumber, endLineNumber),
551
- text,
552
- text_lines: text.split("\n").map((line) => line.trim()).filter(Boolean),
553
- })),
554
- exit_point_entries: element.exitPoints.map(([lineNumber, text]) => ({
555
- line_number: lineNumber,
556
- text,
557
- })),
558
- } satisfies ReferenceSymbolEntry;
559
- });
560
-
561
- const childrenByParentQualifiedName = new Map<string, ReferenceSymbolEntry[]>();
562
- symbols.forEach((symbol) => {
563
- if (!symbol.parent_qualified_name) {
564
- return;
565
- }
566
- const children = childrenByParentQualifiedName.get(symbol.parent_qualified_name) ?? [];
567
- children.push(symbol);
568
- childrenByParentQualifiedName.set(symbol.parent_qualified_name, children);
569
- });
570
- symbols.forEach((symbol) => {
571
- const children = (childrenByParentQualifiedName.get(symbol.qualified_name) ?? []).sort(
572
- (left, right) => left.declaration_order_index - right.declaration_order_index,
573
- );
574
- symbol.child_symbol_names = children.map((child) => child.symbol_name);
575
- symbol.child_qualified_names = children.map((child) => child.qualified_name);
576
- });
577
-
578
- const commentCount = elements.filter(
579
- (element) => [ElementType.COMMENT_SINGLE, ElementType.COMMENT_MULTI].includes(element.elementType) && element.name !== "inline",
580
- ).length;
581
- const doxygenFieldCount = countDoxygenFieldValues(fileLevelDoxygen)
582
- + symbols.reduce((sum, symbol) => sum + countDoxygenFieldValues(collectElementDoxygenFields(definitions[symbol.declaration_order_index]!)), 0);
583
-
584
- if (verbose) {
585
- console.error(` OK ${absolutePath}`);
586
- }
587
- return {
588
- ...buildLineRange(lineCount > 0 ? 1 : 0, lineCount),
589
- canonical_path: canonicalPath,
590
- status: "analyzed",
591
- import_count: imports.length,
592
- symbol_count: symbols.length,
593
- comment_count: commentCount,
594
- standalone_comment_count: standaloneComments.length,
595
- doxygen_field_count: doxygenFieldCount,
596
- file_doxygen: Object.keys(structuredFileDoxygen).length > 0 ? structuredFileDoxygen : undefined,
597
- imports,
598
- symbols,
599
- standalone_comments: standaloneComments,
600
- };
601
- } catch (error) {
602
- const message = error instanceof Error ? error.message : String(error);
603
- if (verbose) {
604
- console.error(` FAIL ${absolutePath} (${message})`);
605
- }
606
- return {
607
- ...buildLineRange(0, 0),
608
- canonical_path: canonicalPath,
609
- status: "error",
610
- import_count: 0,
611
- symbol_count: 0,
612
- comment_count: 0,
613
- standalone_comment_count: 0,
614
- doxygen_field_count: 0,
615
- imports: [],
616
- symbols: [],
617
- standalone_comments: [],
618
- error_message: message,
619
- };
620
- }
621
- }
622
-
623
- /**
624
- * @brief Builds the full agent-oriented references payload.
625
- * @details Validates requested paths against the filesystem, analyzes processable files in caller order, preserves skipped and failed inputs in structured file entries, computes aggregate numeric totals, and emits structured repository data without echoing request metadata already known to the caller. Runtime is O(F log F + S). Side effects are limited to filesystem reads and optional stderr logging.
626
- * @param[in] options {BuildReferenceToolPayloadOptions} Payload-construction options.
627
- * @return {ReferenceToolPayload} Structured references payload ordered as summary, repository, and files.
628
- * @satisfies REQ-011, REQ-014, REQ-076, REQ-077, REQ-078, REQ-079
629
- */
630
- export function buildReferenceToolPayload(options: BuildReferenceToolPayloadOptions): ReferenceToolPayload {
631
- const {
632
- toolName,
633
- scope,
634
- baseDir,
635
- requestedPaths,
636
- sourceDirectoryPaths = [],
637
- verbose = false,
638
- } = options;
639
- const absoluteBaseDir = path.resolve(baseDir);
640
- const analyzer = new SourceAnalyzer();
641
- const canonicalRequestedPaths = requestedPaths.map((requestedPath) => canonicalizeReferencePath(requestedPath, absoluteBaseDir));
642
- const files: ReferenceToolFileEntry[] = requestedPaths.map((requestedPath, requestIndex) => {
643
- const absolutePath = path.resolve(absoluteBaseDir, requestedPath);
644
- const canonicalPath = canonicalizeReferencePath(absolutePath, absoluteBaseDir);
645
- if (!fs.existsSync(absolutePath)) {
646
- if (verbose) {
647
- console.error(` SKIP ${requestedPath} (file not found)`);
648
- }
649
- return {
650
- ...buildLineRange(0, 0),
651
- canonical_path: canonicalPath,
652
- status: "skipped",
653
- import_count: 0,
654
- symbol_count: 0,
655
- comment_count: 0,
656
- standalone_comment_count: 0,
657
- doxygen_field_count: 0,
658
- imports: [],
659
- symbols: [],
660
- standalone_comments: [],
661
- error_message: "not found",
662
- };
663
- }
664
- const stats = fs.statSync(absolutePath);
665
- if (!stats.isFile()) {
666
- if (verbose) {
667
- console.error(` SKIP ${requestedPath} (not a file)`);
668
- }
669
- return {
670
- ...buildLineRange(0, 0),
671
- canonical_path: canonicalPath,
672
- status: "skipped",
673
- import_count: 0,
674
- symbol_count: 0,
675
- comment_count: 0,
676
- standalone_comment_count: 0,
677
- doxygen_field_count: 0,
678
- imports: [],
679
- symbols: [],
680
- standalone_comments: [],
681
- error_message: "not a file",
682
- };
683
- }
684
- return analyzeReferenceFile(analyzer, requestedPath, absolutePath, requestIndex, absoluteBaseDir, verbose);
685
- });
686
-
687
- if (verbose) {
688
- const analyzedCount = files.filter((file) => file.status === "analyzed").length;
689
- const errorCount = files.filter((file) => file.status === "error").length;
690
- console.error(`\n Processed: ${analyzedCount} ok, ${errorCount} failed`);
691
- }
692
-
693
- const analyzedFiles = files.filter((file) => file.status === "analyzed");
694
- const processableFiles = files.filter((file) => file.status !== "skipped");
695
- const symbolKindCounts = new Map<string, number>();
696
- analyzedFiles.forEach((file) => {
697
- file.symbols.forEach((symbol) => {
698
- symbolKindCounts.set(symbol.symbol_kind, (symbolKindCounts.get(symbol.symbol_kind) ?? 0) + 1);
699
- });
700
- });
701
- const orderedSymbolKindCounts = Object.fromEntries(
702
- [...symbolKindCounts.entries()].sort(([left], [right]) => left.localeCompare(right)),
703
- );
704
- const repositoryFileCanonicalPaths = [...new Set(analyzedFiles.map((file) => file.canonical_path))]
705
- .sort((left, right) => left.localeCompare(right));
706
-
707
- void toolName;
708
- void scope;
709
- void canonicalRequestedPaths;
710
- return {
711
- summary: {
712
- processable_file_count: processableFiles.length,
713
- analyzed_file_count: analyzedFiles.length,
714
- error_file_count: files.filter((file) => file.status === "error").length,
715
- skipped_file_count: files.filter((file) => file.status === "skipped").length,
716
- total_symbol_count: analyzedFiles.reduce((sum, file) => sum + file.symbol_count, 0),
717
- total_import_count: analyzedFiles.reduce((sum, file) => sum + file.import_count, 0),
718
- total_comment_count: analyzedFiles.reduce((sum, file) => sum + file.comment_count, 0),
719
- total_standalone_comment_count: analyzedFiles.reduce((sum, file) => sum + file.standalone_comment_count, 0),
720
- total_doxygen_field_count: analyzedFiles.reduce((sum, file) => sum + file.doxygen_field_count, 0),
721
- symbol_kind_counts: orderedSymbolKindCounts,
722
- },
723
- repository: {
724
- source_directory_paths: sourceDirectoryPaths.map((sourceDirectoryPath) => canonicalizeReferencePath(sourceDirectoryPath, absoluteBaseDir)),
725
- file_canonical_paths: repositoryFileCanonicalPaths,
726
- directory_tree: buildRepositoryTree(repositoryFileCanonicalPaths),
727
- },
728
- files,
729
- };
730
- }
731
-
732
- /**
733
- * @brief Builds deterministic stderr diagnostics from a references payload.
734
- * @details Serializes skipped-input and analysis-error entries into stable newline-delimited diagnostics while leaving fully analyzed payloads silent. Runtime is O(n) in file-entry count. No side effects occur.
735
- * @param[in] payload {ReferenceToolPayload} Structured references payload.
736
- * @return {string} Newline-delimited diagnostics.
737
- */
738
- export function buildReferenceToolExecutionStderr(payload: ReferenceToolPayload): string {
739
- const diagnostics = payload.files.flatMap((file) => {
740
- if (!file.error_message) {
741
- return [];
742
- }
743
- if (file.status === "skipped") {
744
- return [` Warning: skipped: ${file.canonical_path}: ${file.error_message}`];
745
- }
746
- if (file.status === "error") {
747
- return [` Error: failed: ${file.canonical_path}: ${file.error_message}`];
748
- }
749
- return [];
750
- });
751
- return diagnostics.join("\n");
752
- }