pi-usereq 0.10.0 → 0.12.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 (52) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +861 -704
  5. package/pi-usereq/docs/REQUIREMENTS.md +152 -95
  6. package/pi-usereq/docs/WORKFLOW.md +228 -65
  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/debug-runtime.ts +2 -2
  11. package/src/core/extension-status.ts +98 -36
  12. package/src/core/pi-notify.ts +5 -5
  13. package/src/core/pi-usereq-tools.ts +4 -2
  14. package/src/core/prompt-command-catalog.ts +4 -5
  15. package/src/core/prompt-command-runtime.ts +347 -42
  16. package/src/core/prompts.ts +0 -2
  17. package/src/core/req-references-command.ts +175 -0
  18. package/src/core/req-reset-command.ts +323 -0
  19. package/src/core/resources.ts +6 -23
  20. package/src/core/runtime-project-paths.ts +21 -1
  21. package/src/core/settings-menu.ts +85 -28
  22. package/src/core/tool-runner.ts +26 -6
  23. package/src/index.ts +530 -104
  24. package/tests/attended-results-scenarios.ts +5 -5
  25. package/tests/cli-command-option-parity.test.ts +25 -25
  26. package/tests/debug-extension-harness.test.ts +8 -2
  27. package/tests/extension-registration.test.ts +1109 -76
  28. package/tests/oracle-project.test.ts +4 -4
  29. package/tests/oracle-standalone.test.ts +5 -5
  30. package/src/core/reference-payload.ts +0 -752
  31. package/src/resources/prompts/references.md +0 -64
  32. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  33. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  50. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  51. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  52. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
@@ -9,7 +9,7 @@ import { Container, SettingsList, Text, type Component, type SettingItem, type S
9
9
 
10
10
  /**
11
11
  * @brief Describes one selectable pi-usereq settings-menu choice.
12
- * @details Stores the stable action identifier, left-column label, optional label and value tone overrides, optional disabled state, right-column current value, and bottom-line description consumed by the shared settings-menu renderer. The interface is compile-time only and introduces no runtime cost.
12
+ * @details Stores the stable action identifier, left-column label, optional label and value tone overrides, optional disabled state, right-column current value, optional inline-cycle values, and bottom-line description consumed by the shared settings-menu renderer. The interface is compile-time only and introduces no runtime cost.
13
13
  */
14
14
  export interface PiUsereqSettingsMenuChoice {
15
15
  id: string;
@@ -18,6 +18,7 @@ export interface PiUsereqSettingsMenuChoice {
18
18
  value: string;
19
19
  valueTone?: "default" | "dim";
20
20
  disabled?: boolean;
21
+ values?: readonly string[];
21
22
  description: string;
22
23
  }
23
24
 
@@ -35,12 +36,12 @@ export interface PiUsereqSettingsMenuBridge {
35
36
 
36
37
  /**
37
38
  * @brief Describes optional behavior overrides for one settings-menu render.
38
- * @details Carries the caller-selected initial focus row so menu re-renders can
39
- * preserve selection after an in-place toggle or value edit. The interface is
40
- * compile-time only and introduces no runtime cost.
39
+ * @details Carries the caller-selected initial focus row, the optional dynamic choice supplier used to rebuild dependent rows after inline toggles, and the optional inline-change callback used to persist `SettingsList` value cycles without closing the menu. The interface is compile-time only and introduces no runtime cost.
41
40
  */
42
41
  export interface PiUsereqSettingsMenuOptions {
43
42
  initialSelectedId?: string;
43
+ getChoices?: () => PiUsereqSettingsMenuChoice[];
44
+ onChange?: (choiceId: string, newValue: string) => void;
44
45
  }
45
46
 
46
47
  /**
@@ -162,7 +163,7 @@ function createImmediateSelectionComponent(choiceId: string, done: (value?: stri
162
163
 
163
164
  /**
164
165
  * @brief Builds `SettingsList` items from one menu-choice vector.
165
- * @details Copies labels, current values, label-tone overrides, value-tone overrides, disabled-state semantics, and descriptions into `SettingItem` records and attaches a submenu that resolves the outer custom UI with the selected choice identifier only for enabled rows. Runtime is O(n) in choice count. No external state is mutated.
166
+ * @details Copies labels, current values, label-tone overrides, value-tone overrides, disabled-state semantics, inline-cycle values, and descriptions into `SettingItem` records. Non-disabled rows with `values` cycle inline on `Enter` or `Space`, while other non-disabled rows resolve the outer custom UI through the immediate submenu bridge. Runtime is O(n) in choice count. No external state is mutated.
166
167
  * @param[in] theme {PiUsereqSettingsTheme} Callback-local pi theme adapter.
167
168
  * @param[in] choices {PiUsereqSettingsMenuChoice[]} Ordered menu-choice vector.
168
169
  * @param[in] done {(value?: string) => void} Outer custom-UI completion callback.
@@ -182,19 +183,49 @@ function buildSettingItems(
182
183
  currentValue: choice.valueTone === "dim"
183
184
  ? theme.fg("dim", choice.value)
184
185
  : choice.value,
185
- submenu: choice.disabled
186
+ values: choice.disabled || choice.values === undefined
187
+ ? undefined
188
+ : [...choice.values],
189
+ submenu: choice.disabled || choice.values !== undefined
186
190
  ? undefined
187
191
  : () => createImmediateSelectionComponent(choice.id, done),
188
192
  }));
189
193
  }
190
194
 
195
+ /**
196
+ * @brief Writes one best-effort selected row index into a `SettingsList` instance.
197
+ * @details Uses reflective access so pi-usereq can preserve focus across menu re-renders without depending on the private field at compile time. Runtime is O(1). Side effect: mutates the underlying `SettingsList` selection state when the field exists.
198
+ * @param[in,out] settingsList {SettingsList} Mutable settings-list instance.
199
+ * @param[in] selectedIndex {number} Zero-based row index to restore.
200
+ * @return {void} No return value.
201
+ */
202
+ function setSettingsListSelectedIndex(
203
+ settingsList: SettingsList,
204
+ selectedIndex: number,
205
+ ): void {
206
+ Reflect.set(settingsList as object, "selectedIndex", selectedIndex);
207
+ }
208
+
209
+ /**
210
+ * @brief Reads the current selected row index from a `SettingsList` instance.
211
+ * @details Uses reflective access so pi-usereq can report the current focused row through the offline bridge without referencing the private field in the static type system. Runtime is O(1). No external state is mutated.
212
+ * @param[in] settingsList {SettingsList} Settings-list instance.
213
+ * @return {number | undefined} Zero-based selected row index when available.
214
+ */
215
+ function getSettingsListSelectedIndex(
216
+ settingsList: SettingsList,
217
+ ): number | undefined {
218
+ const selectedIndex = Reflect.get(settingsList as object, "selectedIndex");
219
+ return typeof selectedIndex === "number" ? selectedIndex : undefined;
220
+ }
221
+
191
222
  /**
192
223
  * @brief Renders one shared pi-usereq settings menu and resolves the selected action.
193
- * @details Uses `ctx.ui.custom(...)` plus `SettingsList` so every configuration menu shares pi.dev styling, right-aligned current values, circular scrolling, bottom-line descriptions, and optional disabled rows. The returned custom component also exposes an offline bridge for deterministic tests and debug harnesses. Runtime is O(n) in visible choice count plus user interaction cost. Side effects are limited to transient custom-UI rendering.
224
+ * @details Uses `ctx.ui.custom(...)` plus `SettingsList` so every configuration menu shares pi.dev styling, right-aligned current values, circular scrolling, bottom-line descriptions, optional disabled rows, and inline toggle cycles that do not close the menu. When callers provide `getChoices(...)`, dependent rows are rebuilt after inline changes while preserving focus on the changed row. The returned custom component also exposes an offline bridge for deterministic tests and debug harnesses. Runtime is O(n) in visible choice count plus user interaction cost. Side effects are limited to transient custom-UI rendering and caller-owned inline-change callbacks.
194
225
  * @param[in] ctx {ExtensionCommandContext} Active command context.
195
226
  * @param[in] title {string} Menu title displayed in the heading and offline bridge.
196
227
  * @param[in] choices {PiUsereqSettingsMenuChoice[]} Ordered menu-choice vector.
197
- * @param[in] options {PiUsereqSettingsMenuOptions | undefined} Optional initial-focus override.
228
+ * @param[in] options {PiUsereqSettingsMenuOptions | undefined} Optional initial-focus override plus inline-change behavior.
198
229
  * @return {Promise<string | undefined>} Selected choice identifier or `undefined` when cancelled.
199
230
  * @satisfies REQ-151, REQ-152, REQ-153, REQ-154, REQ-156, REQ-192
200
231
  */
@@ -211,22 +242,43 @@ export async function showPiUsereqSettingsMenu(
211
242
  0,
212
243
  0,
213
244
  );
214
- const settingsList = new SettingsList(
215
- buildSettingItems(theme, choices, done),
216
- Math.min(Math.max(choices.length, 1), 12),
217
- buildPiUsereqSettingsListTheme(theme),
218
- () => undefined,
219
- () => done(undefined),
220
- );
221
- const initialSelectedIndex = options.initialSelectedId === undefined
222
- ? 0
223
- : choices.findIndex((choice) => choice.id === options.initialSelectedId);
224
- if (initialSelectedIndex >= 0) {
225
- (settingsList as SettingsList & { selectedIndex: number }).selectedIndex = initialSelectedIndex;
226
- }
227
- container.addChild(titleText);
228
- container.addChild(new Text("", 0, 0));
229
- container.addChild(settingsList);
245
+ const spacer = new Text("", 0, 0);
246
+ let currentChoices = options.getChoices?.() ?? choices;
247
+ let settingsList: SettingsList;
248
+
249
+ const buildSettingsList = (
250
+ menuChoices: PiUsereqSettingsMenuChoice[],
251
+ selectedChoiceId?: string,
252
+ ): SettingsList => {
253
+ const nextSettingsList = new SettingsList(
254
+ buildSettingItems(theme, menuChoices, done),
255
+ Math.min(Math.max(menuChoices.length, 1), 12),
256
+ buildPiUsereqSettingsListTheme(theme),
257
+ (choiceId, newValue) => {
258
+ options.onChange?.(choiceId, newValue);
259
+ rebuildMenu(choiceId);
260
+ },
261
+ () => done(undefined),
262
+ );
263
+ const initialSelectedIndex = selectedChoiceId === undefined
264
+ ? 0
265
+ : menuChoices.findIndex((choice) => choice.id === selectedChoiceId);
266
+ if (initialSelectedIndex >= 0) {
267
+ setSettingsListSelectedIndex(nextSettingsList, initialSelectedIndex);
268
+ }
269
+ return nextSettingsList;
270
+ };
271
+
272
+ const rebuildMenu = (selectedChoiceId?: string): void => {
273
+ currentChoices = options.getChoices?.() ?? choices;
274
+ settingsList = buildSettingsList(currentChoices, selectedChoiceId);
275
+ container.clear();
276
+ container.addChild(titleText);
277
+ container.addChild(spacer);
278
+ container.addChild(settingsList);
279
+ };
280
+
281
+ rebuildMenu(options.initialSelectedId);
230
282
 
231
283
  const component: PiUsereqSettingsMenuComponent = {
232
284
  render(width: number): string[] {
@@ -242,13 +294,18 @@ export async function showPiUsereqSettingsMenu(
242
294
  },
243
295
  __piUsereqSettingsMenu: {
244
296
  title,
245
- choices,
246
- selectedChoiceId: initialSelectedIndex >= 0 ? choices[initialSelectedIndex]?.id : undefined,
297
+ get choices(): PiUsereqSettingsMenuChoice[] {
298
+ return currentChoices;
299
+ },
300
+ get selectedChoiceId(): string | undefined {
301
+ const selectedIndex = getSettingsListSelectedIndex(settingsList) ?? 0;
302
+ return currentChoices[selectedIndex]?.id;
303
+ },
247
304
  selectByLabel(label: string): boolean {
248
- const choice = choices.find(
305
+ const choice = currentChoices.find(
249
306
  (candidate) => candidate.label === label || candidate.id === label,
250
307
  );
251
- if (!choice || choice.disabled) {
308
+ if (!choice) {
252
309
  return false;
253
310
  }
254
311
  done(choice.id);
@@ -239,15 +239,15 @@ export function runFilesTokens(files: string[]): ToolResult {
239
239
  }
240
240
 
241
241
  /**
242
- * @brief Generates the monolithic references markdown for explicit files.
243
- * @details Delegates to `generateMarkdown(...)`, keeps output paths relative to the caller cwd, and returns the Python-compatible markdown document through stdout. Runtime is O(F + S). Side effects are limited to filesystem reads and optional stderr logging.
242
+ * @brief Generates the monolithic summary markdown for explicit files.
243
+ * @details Delegates to `generateMarkdown(...)`, keeps output paths relative to the caller cwd, and returns the Python-compatible summary markdown document through stdout. Runtime is O(F + S). Side effects are limited to filesystem reads and optional stderr logging.
244
244
  * @param[in] files {string[]} Explicit file paths.
245
245
  * @param[in] cwd {string} Base directory used for relative output paths. Defaults to `process.cwd()`.
246
246
  * @param[in] verbose {boolean} When `true`, emit per-file progress diagnostics to stderr.
247
247
  * @return {ToolResult} Successful tool result containing monolithic markdown.
248
248
  * @satisfies REQ-011, REQ-076, REQ-077, REQ-078, REQ-079
249
249
  */
250
- export function runFilesReferences(files: string[], cwd = process.cwd(), verbose = false): ToolResult {
250
+ export function runFilesSummarize(files: string[], cwd = process.cwd(), verbose = false): ToolResult {
251
251
  try {
252
252
  return ok(`${generateMarkdown(files, verbose, cwd)}\n`);
253
253
  } catch (error) {
@@ -286,8 +286,8 @@ export function runFilesSearch(argsList: string[], enableLineNumbers = false, ve
286
286
  }
287
287
 
288
288
  /**
289
- * @brief Generates the monolithic references markdown for configured source directories.
290
- * @details Resolves the project base, collects configured source files, prepends the repository file-structure markdown block, and returns the Python-compatible references document through stdout. Runtime is O(F log F + S). Side effects are limited to filesystem reads and optional stderr logging.
289
+ * @brief Generates the monolithic summary markdown for configured source directories.
290
+ * @details Resolves the project base, collects configured source files, prepends the repository file-structure markdown block, and returns the Python-compatible summary document through stdout. Runtime is O(F log F + S). Side effects are limited to filesystem reads and optional stderr logging.
291
291
  * @param[in] projectBase {string} Candidate project root.
292
292
  * @param[in] config {UseReqConfig | undefined} Optional preloaded configuration.
293
293
  * @param[in] verbose {boolean} When `true`, emit per-file diagnostics to stderr.
@@ -295,7 +295,7 @@ export function runFilesSearch(argsList: string[], enableLineNumbers = false, ve
295
295
  * @throws {ReqError} Throws when no source files are found or no file can be analyzed.
296
296
  * @satisfies REQ-014, REQ-076, REQ-077, REQ-078, REQ-079
297
297
  */
298
- export function runReferences(projectBase: string, config?: UseReqConfig, verbose = false): ToolResult {
298
+ export function runSummarize(projectBase: string, config?: UseReqConfig, verbose = false): ToolResult {
299
299
  const [base, srcDirs] = resolveProjectSrcDirs(projectBase, config);
300
300
  const files = collectSourceFiles(srcDirs, base);
301
301
  if (files.length === 0) fail("Error: no source files found in configured directories.", 1);
@@ -308,6 +308,26 @@ export function runReferences(projectBase: string, config?: UseReqConfig, verbos
308
308
  }
309
309
  }
310
310
 
311
+ /**
312
+ * @brief Writes configured project references markdown to the canonical docs file.
313
+ * @details Reuses `runSummarize(...)` to generate the same file-structure-plus-summary markdown, resolves `<docs-dir>/REFERENCES.md` from the effective project configuration, overwrites the target file, and returns the status-only stdout `success`. Runtime is O(F log F + S) plus one file write. Side effects include filesystem writes.
314
+ * @param[in] projectBase {string} Candidate project root.
315
+ * @param[in] config {UseReqConfig | undefined} Optional preloaded configuration.
316
+ * @param[in] verbose {boolean} When `true`, emit per-file diagnostics to stderr during summary generation.
317
+ * @return {ToolResult} Successful tool result containing the status-only stdout payload.
318
+ * @throws {ReqError} Throws when source discovery, summary generation, or file writing fails.
319
+ * @satisfies REQ-293
320
+ */
321
+ export function runReferences(projectBase: string, config?: UseReqConfig, verbose = false): ToolResult {
322
+ const base = resolveProjectBase(projectBase);
323
+ const effectiveConfig = config ?? loadConfig(base);
324
+ const docsDir = effectiveConfig["docs-dir"].replace(/[/\\]+$/, "");
325
+ const referencesPath = path.join(base, docsDir, "REFERENCES.md");
326
+ const summarizeResult = runSummarize(base, effectiveConfig, verbose);
327
+ fs.writeFileSync(referencesPath, summarizeResult.stdout, "utf8");
328
+ return ok("success\n");
329
+ }
330
+
311
331
  /**
312
332
  * @brief Compresses all source files from configured source directories.
313
333
  * @details Resolves the project base, collects source files, and delegates to `compressFiles`. Runtime is O(F + S). Side effects are limited to filesystem reads and optional stderr logging.