pi-usereq 0.5.0 → 0.7.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.
@@ -182,10 +182,9 @@ export interface ReferenceToolRepositorySection {
182
182
 
183
183
  /**
184
184
  * @brief Describes the full agent-oriented references payload.
185
- * @details Orders the top-level sections as request, summary, repository, and files for deterministic downstream traversal. The interface is compile-time only and introduces no runtime cost.
185
+ * @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.
186
186
  */
187
187
  export interface ReferenceToolPayload {
188
- request: ReferenceToolRequestSection;
189
188
  summary: ReferenceToolSummarySection;
190
189
  repository: ReferenceToolRepositorySection;
191
190
  files: ReferenceToolFileEntry[];
@@ -663,9 +662,9 @@ function analyzeReferenceFile(
663
662
 
664
663
  /**
665
664
  * @brief Builds the full agent-oriented references payload.
666
- * @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 a structured repository tree. Runtime is O(F log F + S). Side effects are limited to filesystem reads and optional stderr logging.
665
+ * @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.
667
666
  * @param[in] options {BuildReferenceToolPayloadOptions} Payload-construction options.
668
- * @return {ReferenceToolPayload} Structured references payload ordered as request, summary, repository, and files.
667
+ * @return {ReferenceToolPayload} Structured references payload ordered as summary, repository, and files.
669
668
  * @satisfies REQ-011, REQ-014, REQ-076, REQ-077, REQ-078, REQ-079
670
669
  */
671
670
  export function buildReferenceToolPayload(options: BuildReferenceToolPayloadOptions): ReferenceToolPayload {
@@ -761,17 +760,10 @@ export function buildReferenceToolPayload(options: BuildReferenceToolPayloadOpti
761
760
  const repositoryFileCanonicalPaths = [...new Set(analyzedFiles.map((file) => file.canonical_path))]
762
761
  .sort((left, right) => left.localeCompare(right));
763
762
 
763
+ void toolName;
764
+ void scope;
765
+ void canonicalRequestedPaths;
764
766
  return {
765
- request: {
766
- tool_name: toolName,
767
- scope,
768
- base_dir_path: absoluteBaseDir,
769
- source_directory_count: sourceDirectoryPaths.length,
770
- source_directory_paths: sourceDirectoryPaths.map((sourceDirectoryPath) => canonicalizeReferencePath(sourceDirectoryPath, absoluteBaseDir)),
771
- requested_file_count: requestedPaths.length,
772
- requested_input_paths: [...requestedPaths],
773
- requested_canonical_paths: canonicalRequestedPaths,
774
- },
775
767
  summary: {
776
768
  processable_file_count: processableFiles.length,
777
769
  analyzed_file_count: analyzedFiles.length,
@@ -26,10 +26,21 @@ export interface PiUsereqSettingsMenuChoice {
26
26
  export interface PiUsereqSettingsMenuBridge {
27
27
  title: string;
28
28
  choices: PiUsereqSettingsMenuChoice[];
29
+ selectedChoiceId?: string;
29
30
  selectByLabel: (label: string) => boolean;
30
31
  cancel: () => void;
31
32
  }
32
33
 
34
+ /**
35
+ * @brief Describes optional behavior overrides for one settings-menu render.
36
+ * @details Carries the caller-selected initial focus row so menu re-renders can
37
+ * preserve selection after an in-place toggle or value edit. The interface is
38
+ * compile-time only and introduces no runtime cost.
39
+ */
40
+ export interface PiUsereqSettingsMenuOptions {
41
+ initialSelectedId?: string;
42
+ }
43
+
33
44
  /**
34
45
  * @brief Represents a custom menu component augmented with the offline bridge.
35
46
  * @details Extends the generic TUI `Component` contract with one optional bridge field consumed only by deterministic test and debug harness adapters. The interface is compile-time only and introduces no runtime cost.
@@ -177,13 +188,15 @@ function buildSettingItems(
177
188
  * @param[in] ctx {ExtensionCommandContext} Active command context.
178
189
  * @param[in] title {string} Menu title displayed in the heading and offline bridge.
179
190
  * @param[in] choices {PiUsereqSettingsMenuChoice[]} Ordered menu-choice vector.
191
+ * @param[in] options {PiUsereqSettingsMenuOptions | undefined} Optional initial-focus override.
180
192
  * @return {Promise<string | undefined>} Selected choice identifier or `undefined` when cancelled.
181
- * @satisfies REQ-151, REQ-152, REQ-153, REQ-154, REQ-156
193
+ * @satisfies REQ-151, REQ-152, REQ-153, REQ-154, REQ-156, REQ-192
182
194
  */
183
195
  export async function showPiUsereqSettingsMenu(
184
196
  ctx: ExtensionCommandContext,
185
197
  title: string,
186
198
  choices: PiUsereqSettingsMenuChoice[],
199
+ options: PiUsereqSettingsMenuOptions = {},
187
200
  ): Promise<string | undefined> {
188
201
  return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
189
202
  const container = new Container();
@@ -199,6 +212,12 @@ export async function showPiUsereqSettingsMenu(
199
212
  () => undefined,
200
213
  () => done(undefined),
201
214
  );
215
+ const initialSelectedIndex = options.initialSelectedId === undefined
216
+ ? 0
217
+ : choices.findIndex((choice) => choice.id === options.initialSelectedId);
218
+ if (initialSelectedIndex >= 0) {
219
+ (settingsList as SettingsList & { selectedIndex: number }).selectedIndex = initialSelectedIndex;
220
+ }
202
221
  container.addChild(titleText);
203
222
  container.addChild(new Text("", 0, 0));
204
223
  container.addChild(settingsList);
@@ -218,6 +237,7 @@ export async function showPiUsereqSettingsMenu(
218
237
  __piUsereqSettingsMenu: {
219
238
  title,
220
239
  choices,
240
+ selectedChoiceId: initialSelectedIndex >= 0 ? choices[initialSelectedIndex]?.id : undefined,
221
241
  selectByLabel(label: string): boolean {
222
242
  const choice = choices.find(
223
243
  (candidate) => candidate.label === label || candidate.id === label,
@@ -6,9 +6,10 @@
6
6
 
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
- import { getEncoding } from "js-tiktoken";
9
+ import { createRequire } from "node:module";
10
10
  import { detectLanguage as detectSourceLanguage } from "./compress.js";
11
11
  import { parseDoxygenComment, type DoxygenFieldMap } from "./doxygen-parser.js";
12
+ import { ReqError } from "./errors.js";
12
13
 
13
14
  /**
14
15
  * @brief Declares the tokenizer encoding used by token-count workflows.
@@ -175,13 +176,11 @@ export interface TokenToolGuidanceSection {
175
176
 
176
177
  /**
177
178
  * @brief Describes the full agent-oriented token payload.
178
- * @details Orders the top-level sections as request, summary, files, and guidance for deterministic downstream traversal. The interface is compile-time only and introduces no runtime cost.
179
+ * @details Exposes only aggregate numeric totals plus per-file metrics, omitting request echoes and derived guidance that can be inferred from tool registration or recomputed by the caller. The interface is compile-time only and introduces no runtime cost.
179
180
  */
180
181
  export interface TokenToolPayload {
181
- request: TokenToolRequestSection;
182
182
  summary: TokenToolSummarySection;
183
183
  files: TokenToolFileEntry[];
184
- guidance: TokenToolGuidanceSection;
185
184
  }
186
185
 
187
186
  /**
@@ -198,6 +197,57 @@ export interface BuildTokenToolPayloadOptions {
198
197
  encodingName?: string;
199
198
  }
200
199
 
200
+ type TokenCounterEncoding = {
201
+ encode(content: string): ArrayLike<number>;
202
+ };
203
+
204
+ type JsTiktokenModule = {
205
+ getEncoding(encodingName: string): TokenCounterEncoding;
206
+ };
207
+
208
+ const require = createRequire(import.meta.url);
209
+
210
+ function defaultJsTiktokenModuleLoader(): JsTiktokenModule {
211
+ return require("js-tiktoken") as JsTiktokenModule;
212
+ }
213
+
214
+ let jsTiktokenModuleLoader: () => JsTiktokenModule = defaultJsTiktokenModuleLoader;
215
+
216
+ /**
217
+ * @brief Overrides the `js-tiktoken` loader for tests.
218
+ * @details Enables deterministic dependency-failure tests without mutating repository dependencies on disk. Runtime is O(1). Side effects are limited to module-local test state.
219
+ * @param[in] loader {(() => JsTiktokenModule) | undefined} Replacement loader, or `undefined` to restore the default loader.
220
+ * @return {void} No return value.
221
+ */
222
+ export function setJsTiktokenModuleLoaderForTests(loader?: () => JsTiktokenModule): void {
223
+ jsTiktokenModuleLoader = loader ?? defaultJsTiktokenModuleLoader;
224
+ }
225
+
226
+ /**
227
+ * @brief Loads the `js-tiktoken` module on demand.
228
+ * @details Defers dependency resolution until token counting is requested so extension registration can succeed even when the optional runtime dependency has not yet been installed. Runtime is O(1) plus module resolution cost. Side effects are limited to Node module loading.
229
+ * @return {JsTiktokenModule} Loaded tokenizer module.
230
+ * @throws {ReqError} Throws when `js-tiktoken` is unavailable.
231
+ */
232
+ function loadJsTiktokenModule(): JsTiktokenModule {
233
+ try {
234
+ const module = jsTiktokenModuleLoader();
235
+ if (!module || typeof module.getEncoding !== "function") {
236
+ throw new ReqError("Error: token-count dependency 'js-tiktoken' is unavailable. Run `npm ci` in the PI-useReq repository.", 1);
237
+ }
238
+ return module;
239
+ } catch (error) {
240
+ if (error instanceof ReqError) {
241
+ throw error;
242
+ }
243
+ const message = error instanceof Error ? error.message : String(error);
244
+ if (/Cannot find (module|package) 'js-tiktoken'|ERR_MODULE_NOT_FOUND/.test(message)) {
245
+ throw new ReqError("Error: token-count dependency 'js-tiktoken' is not installed. Run `npm ci` in the PI-useReq repository.", 1);
246
+ }
247
+ throw error;
248
+ }
249
+ }
250
+
201
251
  /**
202
252
  * @brief Encapsulates one tokenizer instance for repeated token counting.
203
253
  * @details Caches a `js-tiktoken` encoding object so multiple documents can be counted without repeated encoding lookup. Counting cost is O(n) in content length. The class mutates only instance state during construction.
@@ -207,7 +257,7 @@ export class TokenCounter {
207
257
  * @brief Stores the tokenizer implementation used for subsequent counts.
208
258
  * @details The field holds the encoder returned by `getEncoding`. Access complexity is O(1). The value is initialized once per instance.
209
259
  */
210
- private encoding;
260
+ private encoding: TokenCounterEncoding;
211
261
 
212
262
  /**
213
263
  * @brief Initializes a token counter for one encoding family.
@@ -216,7 +266,7 @@ export class TokenCounter {
216
266
  * @return {TokenCounter} New token counter instance.
217
267
  */
218
268
  constructor(encodingName = TOKEN_COUNTER_ENCODING) {
219
- this.encoding = getEncoding(encodingName);
269
+ this.encoding = loadJsTiktokenModule().getEncoding(encodingName);
220
270
  }
221
271
 
222
272
  /**
@@ -359,41 +409,6 @@ function roundRatio(numerator: number, denominator: number): number {
359
409
  return Number((numerator / denominator).toFixed(6));
360
410
  }
361
411
 
362
- /**
363
- * @brief Orders canonical file paths by one numeric metric while removing duplicates.
364
- * @details Filters to counted file entries, sorts by the supplied metric direction, breaks ties by canonical path, and preserves only the first occurrence of each path. Runtime is O(n log n). No external state is mutated.
365
- * @param[in] files {TokenToolFileEntry[]} Token payload file entries.
366
- * @param[in] metric {(entry: TokenToolFileEntry) => number} Numeric metric selector.
367
- * @param[in] direction {"asc" | "desc"} Sort direction.
368
- * @return {string[]} Unique canonical paths ordered by the requested metric.
369
- */
370
- function orderPathsByMetric(
371
- files: TokenToolFileEntry[],
372
- metric: (entry: TokenToolFileEntry) => number,
373
- direction: "asc" | "desc",
374
- ): string[] {
375
- const sorted = files
376
- .filter((entry) => entry.status === "counted")
377
- .sort((left, right) => {
378
- const leftMetric = metric(left);
379
- const rightMetric = metric(right);
380
- if (leftMetric === rightMetric) {
381
- return left.canonical_path.localeCompare(right.canonical_path);
382
- }
383
- return direction === "desc" ? rightMetric - leftMetric : leftMetric - rightMetric;
384
- });
385
- const seen = new Set<string>();
386
- const orderedPaths: string[] = [];
387
- for (const entry of sorted) {
388
- if (seen.has(entry.canonical_path)) {
389
- continue;
390
- }
391
- seen.add(entry.canonical_path);
392
- orderedPaths.push(entry.canonical_path);
393
- }
394
- return orderedPaths;
395
- }
396
-
397
412
  /**
398
413
  * @brief Probes one requested path before token counting.
399
414
  * @details Resolves whether the target exists and is a regular file while capturing a stable skip reason for missing or non-file inputs. Runtime is dominated by one filesystem stat. Side effects are limited to filesystem reads.
@@ -496,9 +511,9 @@ export function countFilesMetrics(filePaths: string[], encodingName = TOKEN_COUN
496
511
 
497
512
  /**
498
513
  * @brief Builds the agent-oriented JSON payload for token-centric tools.
499
- * @details Validates requested paths against the filesystem, counts token metrics for processable files, preserves caller order in the file table, separates raw observations from derived guidance, and emits direct-access file facts such as line ranges, sizes, headings, and optional Doxygen file fields. Runtime is O(F log F + S). Side effects are limited to filesystem reads.
514
+ * @details Validates requested paths against the filesystem, counts token metrics for processable files, preserves caller order in the file table, and emits direct-access file facts such as sizes, headings, and optional Doxygen file fields while omitting request echoes and derived guidance. Runtime is O(F + S). Side effects are limited to filesystem reads.
500
515
  * @param[in] options {BuildTokenToolPayloadOptions} Payload-construction options.
501
- * @return {TokenToolPayload} Structured token payload ordered as request, summary, files, guidance.
516
+ * @return {TokenToolPayload} Structured token payload ordered as summary then files.
502
517
  * @satisfies REQ-010, REQ-017, REQ-069, REQ-070, REQ-071, REQ-073, REQ-074, REQ-075
503
518
  */
504
519
  export function buildTokenToolPayload(options: BuildTokenToolPayloadOptions): TokenToolPayload {
@@ -596,71 +611,7 @@ export function buildTokenToolPayload(options: BuildTokenToolPayloadOptions): To
596
611
  const countedFileCount = filesWithShares.filter((entry) => entry.status === "counted").length;
597
612
  const errorFileCount = filesWithShares.filter((entry) => entry.status === "error").length;
598
613
  const skippedFileCount = filesWithShares.filter((entry) => entry.status === "skipped").length;
599
- const countedPathsByTokenCountDesc = orderPathsByMetric(filesWithShares, (entry) => entry.token_count, "desc");
600
- const countedPathsByTokenCountAsc = orderPathsByMetric(filesWithShares, (entry) => entry.token_count, "asc");
601
- const countedPathsByLineCountDesc = orderPathsByMetric(filesWithShares, (entry) => entry.line_count, "desc");
602
- const dominantTokenFileEntry = filesWithShares
603
- .filter((entry) => entry.status === "counted")
604
- .sort((left, right) => {
605
- if (left.token_count === right.token_count) {
606
- return left.canonical_path.localeCompare(right.canonical_path);
607
- }
608
- return right.token_count - left.token_count;
609
- })[0];
610
- const skippedInputs = filesWithShares
611
- .filter((entry) => entry.status === "skipped" && entry.error_message)
612
- .map((entry) => ({
613
- input_path: entry.input_path,
614
- canonical_path: entry.canonical_path,
615
- reason: entry.error_message!,
616
- }));
617
- const errorInputs = filesWithShares
618
- .filter((entry) => entry.status === "error" && entry.error_message)
619
- .map((entry) => ({
620
- input_path: entry.input_path,
621
- canonical_path: entry.canonical_path,
622
- reason: entry.error_message!,
623
- }));
624
- const derivedRecommendations: TokenToolRecommendation[] = countedPathsByTokenCountDesc.length > 0
625
- ? [
626
- {
627
- kind: "prioritize_high_token_paths",
628
- basis_metric_name: "token_count",
629
- ordered_paths: countedPathsByTokenCountDesc,
630
- },
631
- {
632
- kind: "defer_low_token_paths",
633
- basis_metric_name: "token_count",
634
- ordered_paths: countedPathsByTokenCountAsc,
635
- },
636
- ]
637
- : [];
638
- const actionableNextSteps: TokenToolNextStepHint[] = countedPathsByTokenCountDesc.length > 0
639
- ? [
640
- {
641
- kind: "read_top_token_paths_first",
642
- ordered_paths: countedPathsByTokenCountDesc.slice(0, 3),
643
- goal: "minimize context-truncation risk during initial review",
644
- },
645
- {
646
- kind: "reserve_low_token_paths_for_follow_up",
647
- ordered_paths: countedPathsByTokenCountAsc.slice(0, 3),
648
- goal: "defer lower-cost files until high-cost files have been reviewed",
649
- },
650
- ]
651
- : [];
652
614
  return {
653
- request: {
654
- tool_name: options.toolName,
655
- scope: options.scope,
656
- encoding_name: encodingName,
657
- base_dir_path: baseDir.split(path.sep).join("/"),
658
- requested_file_count: options.requestedPaths.length,
659
- requested_input_paths: options.requestedPaths,
660
- requested_canonical_paths: requestedEntries.map((entry) => entry.canonicalPath),
661
- docs_dir_path: options.docsDir,
662
- canonical_doc_names: options.canonicalDocNames,
663
- },
664
615
  summary: {
665
616
  processable_file_count: countedFileCount + errorFileCount,
666
617
  counted_file_count: countedFileCount,
@@ -676,23 +627,6 @@ export function buildTokenToolPayload(options: BuildTokenToolPayloadOptions): To
676
627
  average_line_count_per_counted_file: countedFileCount === 0 ? 0 : Number((totalLineCount / countedFileCount).toFixed(6)),
677
628
  },
678
629
  files: filesWithShares,
679
- guidance: {
680
- source_observations: {
681
- counted_paths_by_token_count_desc: countedPathsByTokenCountDesc,
682
- counted_paths_by_line_count_desc: countedPathsByLineCountDesc,
683
- skipped_inputs: skippedInputs,
684
- error_inputs: errorInputs,
685
- dominant_token_file: dominantTokenFileEntry
686
- ? {
687
- canonical_path: dominantTokenFileEntry.canonical_path,
688
- token_count: dominantTokenFileEntry.token_count,
689
- token_share: dominantTokenFileEntry.token_share,
690
- }
691
- : undefined,
692
- },
693
- derived_recommendations: derivedRecommendations,
694
- actionable_next_steps: actionableNextSteps,
695
- },
696
630
  };
697
631
  }
698
632