peaks-loop 4.0.31 → 4.0.33

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 (56) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README-en.md +3 -4
  3. package/README.md +20 -28
  4. package/dist/cli/commands/_register.js +4 -3
  5. package/dist/cli/commands/audit-commands.js +12 -39
  6. package/dist/cli/commands/capability-commands.d.ts +6 -0
  7. package/dist/cli/commands/capability-commands.js +20 -2
  8. package/dist/cli/commands/ecc-commands.js +3 -2
  9. package/dist/cli/commands/project-commands.js +1 -1
  10. package/dist/cli/commands/shadcn-commands.d.ts +32 -0
  11. package/dist/cli/commands/shadcn-commands.js +120 -0
  12. package/dist/cli/commands/slice-commands.js +1 -4
  13. package/dist/services/audit/static-service.d.ts +1 -1
  14. package/dist/services/audit/static-service.js +1 -1
  15. package/dist/services/code-review/ecc-bridge.d.ts +94 -15
  16. package/dist/services/code-review/ecc-bridge.js +133 -13
  17. package/dist/services/dashboard/project-dashboard-service.d.ts +0 -7
  18. package/dist/services/dashboard/project-dashboard-service.js +1 -9
  19. package/dist/services/preferences/preferences-types.d.ts +16 -7
  20. package/dist/services/recommendations/capability-seed-items.js +0 -1
  21. package/dist/services/recommendations/installed-capability-detector.d.ts +33 -0
  22. package/dist/services/recommendations/installed-capability-detector.js +76 -0
  23. package/dist/services/scan/orphan-service.js +1 -1
  24. package/dist/services/slice/multi-pass-orchestrator.js +0 -13
  25. package/dist/services/slice/slice-decompose-import-edges.d.ts +2 -2
  26. package/dist/services/slice/slice-decompose-import-edges.js +1 -29
  27. package/dist/services/slice/slice-decompose-runners.d.ts +2 -4
  28. package/dist/services/slice/slice-decompose-runners.js +1 -23
  29. package/dist/services/slice/slice-decompose-service.d.ts +3 -4
  30. package/dist/services/slice/slice-decompose-service.js +10 -29
  31. package/dist/services/slice/slice-decompose-types.d.ts +6 -47
  32. package/dist/services/slice/slice-decompose-types.js +3 -2
  33. package/dist/services/slice/slice-topology-types.d.ts +2 -3
  34. package/package.json +5 -5
  35. package/schemas/decomposition-v1.json +0 -12
  36. package/schemas/decomposition-v2.json +1 -15
  37. package/skills/bee/peaks-prd/SKILL.md +0 -1
  38. package/skills/bee/peaks-rd/SKILL.md +1 -1
  39. package/skills/bee/peaks-rd/references/external-references.md +1 -3
  40. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -2
  41. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +22 -8
  42. package/skills/bee/peaks-rd/references/rd-runbook.md +0 -2
  43. package/skills/bee/peaks-txt/SKILL.md +0 -12
  44. package/skills/peaks-doctor/SKILL.md +1 -1
  45. package/skills/peaks-slice-decompose/SKILL.md +0 -1
  46. package/skills/peaks-slice-decompose/references/v2-schema.md +1 -12
  47. package/dist/cli/commands/understand-commands.d.ts +0 -3
  48. package/dist/cli/commands/understand-commands.js +0 -148
  49. package/dist/services/understand/hybrid-types.d.ts +0 -32
  50. package/dist/services/understand/hybrid-types.js +0 -1
  51. package/dist/services/understand/understand-hybrid-service.d.ts +0 -10
  52. package/dist/services/understand/understand-hybrid-service.js +0 -100
  53. package/dist/services/understand/understand-scan-service.d.ts +0 -28
  54. package/dist/services/understand/understand-scan-service.js +0 -170
  55. package/dist/services/understand/understand-types.d.ts +0 -50
  56. package/dist/services/understand/understand-types.js +0 -1
@@ -21,7 +21,6 @@ The CLI serialises via `SchemaRouter.writeResult()` → `JSON.stringify(result,
21
21
  | `crossPassEdges` | `readonly CrossPassEdge[]` | `cross-pass-edge-merger.ts` (via `merge`) | Empty when `opts.llmRunner` is not provided. |
22
22
  | `llmArbitrations` | `readonly LlmArbitration[]` | `merge.llmCalls` mapped via `llmCallsToArbitrations` | Empty when no LLM call was made. Cap: ≤ 2 calls per invocation. |
23
23
  | `codegraph` | `CodegraphEnvelope` | from Pass 1 / Pass 2 result | Reused from v1 (see `slice-decompose-types.ts`). Falls back to `zeroCodegraph()` if neither pass ran. |
24
- | `understandAnything` | `UnderstandAnythingEnvelope` | from Pass 1 / Pass 2 result | Reused from v1. Falls back to `zeroUnderstand()` if neither pass ran. |
25
24
  | `partial` | `boolean` | orchestrator | `true` iff any pass failed to complete. Currently always `false` from `MultiPassOrchestrator`; reserved for future partial-failure paths. |
26
25
 
27
26
  ## `CodegraphEnvelope` (re-exported from v1)
@@ -35,16 +34,6 @@ The CLI serialises via `SchemaRouter.writeResult()` → `JSON.stringify(result,
35
34
  | `affectedCrossFile` | `boolean` | `true` iff `codegraph.affected` reported > 0 cross-file dependents. |
36
35
  | `note` | `string` | Free-form operator note; `'no decomposition run'` when both passes were skipped. |
37
36
 
38
- ## `UnderstandAnythingEnvelope` (re-exported from v1)
39
-
40
- | Field | Type | Notes |
41
- |---|---|---|
42
- | `kgNodes` | `number` | Knowledge-graph node count. |
43
- | `kgEdges` | `number` | Knowledge-graph edge count. |
44
- | `available` | `boolean` | `false` when the `.understand-anything/knowledge-graph.json` file is missing. |
45
- | `fallback` | `'semantic' \| 'structural-only'` | `'structural-only'` when `available` is `false`. |
46
- | `note` | `string` | Free-form note. |
47
-
48
37
  ## `PassResult`
49
38
 
50
39
  | Field | Type | Notes |
@@ -64,7 +53,7 @@ The CLI serialises via `SchemaRouter.writeResult()` → `JSON.stringify(result,
64
53
  | `files` | `readonly string[]` | Project-relative paths. |
65
54
  | `loc` | `number` | Total LoC across all `files`. |
66
55
  | `parentSliceId` | `string \| null` | `null` for Pass 1. For Pass 2, the id of the parent Pass 1 slice. |
67
- | `semanticAnchor` | `string` | `"domain:<name>"` when understand-anything indexed, else `"file:<path>"`. Currently always `"file:<wu.filePath>"` from the orchestrator's `workUnitToSliceV2`. |
56
+ | `semanticAnchor` | `string` | Always `"file:<wu.filePath>"` from the orchestrator's `workUnitToSliceV2` (codegraph / structural evidence). |
68
57
 
69
58
  ### `InternalEdge` (reserved)
70
59
 
@@ -1,3 +0,0 @@
1
- import { Command } from 'commander';
2
- import { type ProgramIO } from '../cli-helpers.js';
3
- export declare function registerUnderstandCommands(program: Command, io: ProgramIO): void;
@@ -1,148 +0,0 @@
1
- import { InvalidArgumentError } from 'commander';
2
- import { scanUnderstandAnything, summarizeKnowledgeGraph } from '../../services/understand/understand-scan-service.js';
3
- import { buildUnderstandContext } from '../../services/understand/understand-hybrid-service.js';
4
- import { fail, ok } from 'peaks-loop-shared/result';
5
- import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
6
- function parsePositiveInteger(value) {
7
- if (!/^\d+$/.test(value)) {
8
- throw new InvalidArgumentError('must be a positive integer');
9
- }
10
- const parsed = Number(value);
11
- if (!Number.isSafeInteger(parsed) || parsed < 1) {
12
- throw new InvalidArgumentError('must be a positive integer');
13
- }
14
- return parsed;
15
- }
16
- const INSTALL_HINT = 'Install Understand Anything in Claude Code: `/plugin marketplace add Lum1104/Understand-Anything` then `/plugin install understand-anything`, then run `/understand` in the target project to generate .understand-anything/knowledge-graph.json.';
17
- export function registerUnderstandCommands(program, io) {
18
- const understand = program.command('understand').description('Inspect Understand Anything artifacts inside a project (read-only)');
19
- addJsonOption(understand
20
- .command('status')
21
- .description('Report whether Understand Anything has produced a knowledge graph in the target project')
22
- .requiredOption('--project <path>', 'target project root')
23
- .option('--artifact-dir <path>', 'override the default .understand-anything directory')).action(async (options) => {
24
- try {
25
- const scanOptions = { projectRoot: options.project };
26
- if (options.artifactDir !== undefined) {
27
- scanOptions.artifactDir = options.artifactDir;
28
- }
29
- const report = await scanUnderstandAnything(scanOptions);
30
- const nextActions = [];
31
- if (!report.exists) {
32
- nextActions.push(INSTALL_HINT);
33
- }
34
- else if (!report.graph.exists) {
35
- nextActions.push('Run `/understand` inside Claude Code on this project to generate .understand-anything/knowledge-graph.json.');
36
- }
37
- else if (report.graph.parseError !== undefined) {
38
- nextActions.push('Re-run `/understand` to regenerate the knowledge graph; the current file is not valid JSON.');
39
- }
40
- printResult(io, ok('understand.status', report, [], nextActions), options.json);
41
- }
42
- catch (error) {
43
- printResult(io, fail('understand.status', 'UNDERSTAND_STATUS_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .understand-anything directory before retrying']), options.json);
44
- process.exitCode = 1;
45
- }
46
- });
47
- addJsonOption(understand
48
- .command('show')
49
- .description('Summarize the Understand Anything knowledge graph (counts, layers, tours, sample nodes) for RD/TXT consumption')
50
- .requiredOption('--project <path>', 'target project root')
51
- .option('--artifact-dir <path>', 'override the default .understand-anything directory')
52
- .option('--sample <n>', 'maximum number of sample node ids to return (default 5)', parsePositiveInteger)).action(async (options) => {
53
- try {
54
- const summaryOptions = { projectRoot: options.project };
55
- if (options.artifactDir !== undefined) {
56
- summaryOptions.artifactDir = options.artifactDir;
57
- }
58
- if (options.sample !== undefined) {
59
- summaryOptions.sampleSize = options.sample;
60
- }
61
- const summary = await summarizeKnowledgeGraph(summaryOptions);
62
- if (!summary.exists) {
63
- printResult(io, fail('understand.show', 'UNDERSTAND_GRAPH_MISSING', `No knowledge graph found at ${summary.path}`, summary, [INSTALL_HINT]), options.json);
64
- process.exitCode = 1;
65
- return;
66
- }
67
- if (summary.parseError !== undefined) {
68
- printResult(io, fail('understand.show', 'UNDERSTAND_GRAPH_PARSE_ERROR', `Knowledge graph at ${summary.path} is not valid JSON: ${summary.parseError}`, summary, ['Re-run `/understand` to regenerate the knowledge graph']), options.json);
69
- process.exitCode = 1;
70
- return;
71
- }
72
- printResult(io, ok('understand.show', summary), options.json);
73
- }
74
- catch (error) {
75
- printResult(io, fail('understand.show', 'UNDERSTAND_SHOW_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and .understand-anything directory before retrying']), options.json);
76
- process.exitCode = 1;
77
- }
78
- });
79
- // L3.1: opt-in UX subcommand. Returns the AskUserQuestion payload that
80
- // the LLM-side UX layer (peaks-code / peaks-ide) should surface when
81
- // uaPrompt === 'unset' and UA is absent. When uaPrompt is skip-this-session
82
- // or skip-forever, returns a no-op envelope (caller does not prompt).
83
- addJsonOption(understand
84
- .command('opt-in')
85
- .description('Returns the UA opt-in prompt payload (Slice L3.1) when uaPrompt is unset; no-op otherwise')
86
- .requiredOption('--project <path>', 'target project root')).action(async (options) => {
87
- try {
88
- const report = await scanUnderstandAnything({ projectRoot: options.project });
89
- const uaPrompt = report.uaPrompt ?? 'unset';
90
- if (report.exists || uaPrompt !== 'unset') {
91
- // No prompt needed: UA is installed OR user already decided.
92
- printResult(io, ok('understand.opt-in', { promptNeeded: false, uaPrompt, uaInstalled: report.exists }), options.json);
93
- return;
94
- }
95
- const prompt = {
96
- version: 1,
97
- tool: 'ua-opt-in',
98
- artifactDir: report.artifactDir,
99
- reason: 'ua-artifact-missing',
100
- options: [
101
- { id: 'install', label: 'Install UA in Claude Code', description: INSTALL_HINT },
102
- { id: 'fallback-this-session', label: 'Use codegraph fallback this session', description: 'Skip UA this run; do not write preferences.json' },
103
- { id: 'fallback-forever', label: 'Use codegraph fallback forever', description: 'Write preferences.json:uaPrompt=skip-forever; suppress future prompts' }
104
- ]
105
- };
106
- printResult(io, ok('understand.opt-in', { promptNeeded: true, uaPrompt, uaInstalled: false, prompt }), options.json);
107
- }
108
- catch (error) {
109
- printResult(io, fail('understand.opt-in', 'UNDERSTAND_OPTIN_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path']), options.json);
110
- process.exitCode = 1;
111
- }
112
- });
113
- // Slice 2026-07-02-codegraph-ua-hybrid: hybrid context subcommand.
114
- // Routes UA-first / codegraph-fallback / hybrid in the service layer;
115
- // the CLI is a thin shell that surfaces the envelope and exit code.
116
- addJsonOption(understand
117
- .command('context')
118
- .description('Build a hybrid project-context envelope: UA knowledge graph when present, codegraph affected as fallback, or both in parallel')
119
- .requiredOption('--project <path>', 'target project root')
120
- .option('--artifact-dir <path>', 'override the default .understand-anything directory')
121
- .option('--sample <n>', 'maximum number of sample node ids to include (default 5)', parsePositiveInteger)
122
- .option('--files <file...>', 'file globs to feed `codegraph affected` (default: src/index.ts, package.json, README.md)')).action(async (options) => {
123
- try {
124
- const ctxOptions = { projectRoot: options.project };
125
- if (options.artifactDir !== undefined)
126
- ctxOptions.artifactDir = options.artifactDir;
127
- if (options.sample !== undefined)
128
- ctxOptions.sampleSize = options.sample;
129
- if (options.files !== undefined)
130
- ctxOptions.files = options.files;
131
- const result = await buildUnderstandContext(ctxOptions);
132
- const exitCode = result.source === 'both-missing' ? 2 : 0;
133
- if (exitCode !== 0) {
134
- printResult(io, fail('understand.context', 'UNDERSTAND_CONTEXT_NO_EVIDENCE', `No UA graph and codegraph affected produced no usable evidence`, result, [
135
- INSTALL_HINT,
136
- 'Run `peaks codegraph index` in this project to populate the codegraph index, then retry'
137
- ]), options.json);
138
- process.exitCode = exitCode;
139
- return;
140
- }
141
- printResult(io, ok('understand.context', result, [], result.warnings.length > 0 ? result.warnings : undefined), options.json);
142
- }
143
- catch (error) {
144
- printResult(io, fail('understand.context', 'UNDERSTAND_CONTEXT_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path and codegraph installation before retrying']), options.json);
145
- process.exitCode = 1;
146
- }
147
- });
148
- }
@@ -1,32 +0,0 @@
1
- import type { CodegraphExecutionResult } from '../codegraph/codegraph-service.js';
2
- import type { UnderstandScanReport } from './understand-types.js';
3
- import type { UnderstandGraphSummary } from './understand-scan-service.js';
4
- /**
5
- * Slice 2026-07-02-codegraph-ua-hybrid — envelope contract for the
6
- * `peaks understand context` command. The service layer decides which
7
- * evidence to include based on environment detection; the source field
8
- * is the authoritative routing signal.
9
- */
10
- export type UnderstandContextSource = 'ua-only' | 'ua-missing-fallback-codegraph' | 'ua-and-codegraph-hybrid' | 'both-missing';
11
- export type CodegraphContextBlock = {
12
- invocation: {
13
- subcommand: 'affected';
14
- files: string[];
15
- };
16
- execution: CodegraphExecutionResult;
17
- /** Parsed JSON from `codegraph affected --json` when exitCode === 0; null otherwise. */
18
- payload: unknown;
19
- parseError?: string;
20
- };
21
- export type UnderstandContextResult = {
22
- projectRoot: string;
23
- source: UnderstandContextSource;
24
- ua?: {
25
- scan: UnderstandScanReport;
26
- summary?: UnderstandGraphSummary;
27
- };
28
- codegraph?: CodegraphContextBlock;
29
- /** Wall-clock milliseconds spent on the slowest parallel branch. */
30
- durationMs: number;
31
- warnings: string[];
32
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,10 +0,0 @@
1
- import { type CodegraphProcessRunner } from '../codegraph/codegraph-service.js';
2
- import type { UnderstandContextResult } from './hybrid-types.js';
3
- export type BuildUnderstandContextOptions = {
4
- projectRoot: string;
5
- files?: string[];
6
- sampleSize?: number;
7
- artifactDir?: string;
8
- codegraphRunner?: CodegraphProcessRunner;
9
- };
10
- export declare function buildUnderstandContext(options: BuildUnderstandContextOptions): Promise<UnderstandContextResult>;
@@ -1,100 +0,0 @@
1
- import { createCodegraphInvocation, executeCodegraphInvocation } from '../codegraph/codegraph-service.js';
2
- import { scanUnderstandAnything, summarizeKnowledgeGraph } from './understand-scan-service.js';
3
- import { getErrorMessage } from 'peaks-loop-shared/result';
4
- const DEFAULT_FILES = ['src/index.ts', 'package.json', 'README.md'];
5
- function resolveFiles(files) {
6
- if (files !== undefined && files.length > 0) {
7
- return [...files];
8
- }
9
- return [...DEFAULT_FILES];
10
- }
11
- async function safeSummarize(options) {
12
- try {
13
- return { summary: await summarizeKnowledgeGraph(options) };
14
- }
15
- catch (error) {
16
- return { warning: `UA summary failed: ${getErrorMessage(error)}` };
17
- }
18
- }
19
- async function runCodegraph(projectRoot, files, runner) {
20
- try {
21
- const invocation = createCodegraphInvocation({
22
- subcommand: 'affected',
23
- project: projectRoot,
24
- files,
25
- json: true
26
- });
27
- const execution = await executeCodegraphInvocation(invocation, runner);
28
- if (execution.exitCode !== 0) {
29
- return { warning: `codegraph affected exited with code ${execution.exitCode}: ${execution.stderr.slice(0, 200)}` };
30
- }
31
- let payload = null;
32
- let parseError;
33
- try {
34
- payload = JSON.parse(execution.stdout);
35
- }
36
- catch (error) {
37
- parseError = getErrorMessage(error);
38
- }
39
- return {
40
- block: {
41
- invocation: { subcommand: 'affected', files },
42
- execution,
43
- payload,
44
- ...(parseError !== undefined ? { parseError } : {})
45
- }
46
- };
47
- }
48
- catch (error) {
49
- return { warning: `codegraph affected failed: ${getErrorMessage(error)}` };
50
- }
51
- }
52
- function decideSource(uaScan, hasCodegraph) {
53
- const uaPresent = uaScan.exists && uaScan.graph.exists && uaScan.graph.parseError === undefined;
54
- if (uaPresent && hasCodegraph)
55
- return 'ua-and-codegraph-hybrid';
56
- if (uaPresent)
57
- return 'ua-only';
58
- if (hasCodegraph)
59
- return 'ua-missing-fallback-codegraph';
60
- return 'both-missing';
61
- }
62
- export async function buildUnderstandContext(options) {
63
- const startedAt = Date.now();
64
- const files = resolveFiles(options.files);
65
- const sampleSize = options.sampleSize ?? 5;
66
- const warnings = [];
67
- const scanOptions = { projectRoot: options.projectRoot };
68
- if (options.artifactDir !== undefined) {
69
- scanOptions.artifactDir = options.artifactDir;
70
- }
71
- const uaScan = await scanUnderstandAnything(scanOptions);
72
- const uaSummaryPromise = uaScan.exists && uaScan.graph.exists
73
- ? safeSummarize({ projectRoot: options.projectRoot, sampleSize, ...(options.artifactDir !== undefined ? { artifactDir: options.artifactDir } : {}) })
74
- : Promise.resolve({});
75
- const [uaSummary, codegraphResult] = await Promise.all([
76
- uaSummaryPromise,
77
- runCodegraph(options.projectRoot, files, options.codegraphRunner)
78
- ]);
79
- if (uaSummary.warning)
80
- warnings.push(uaSummary.warning);
81
- if (codegraphResult.warning)
82
- warnings.push(codegraphResult.warning);
83
- const source = decideSource(uaScan, codegraphResult.block !== undefined);
84
- const result = {
85
- projectRoot: options.projectRoot,
86
- source,
87
- durationMs: Date.now() - startedAt,
88
- warnings
89
- };
90
- if (source === 'ua-only' || source === 'ua-and-codegraph-hybrid') {
91
- result.ua = {
92
- scan: uaScan,
93
- ...(uaSummary.summary !== undefined ? { summary: uaSummary.summary } : {})
94
- };
95
- }
96
- if (codegraphResult.block) {
97
- result.codegraph = codegraphResult.block;
98
- }
99
- return result;
100
- }
@@ -1,28 +0,0 @@
1
- import type { UnderstandScanReport } from './understand-types.js';
2
- export type UnderstandScanOptions = {
3
- projectRoot: string;
4
- artifactDir?: string;
5
- };
6
- export declare function scanUnderstandAnything(options: UnderstandScanOptions): Promise<UnderstandScanReport>;
7
- export type UnderstandGraphSummary = {
8
- exists: boolean;
9
- path: string;
10
- generatedAt: string | null;
11
- topLevelFields: string[];
12
- counts: {
13
- nodes: number;
14
- edges: number;
15
- layers: number;
16
- tours: number;
17
- };
18
- layerNames: string[];
19
- tourNames: string[];
20
- sampleNodes: string[];
21
- parseError?: string;
22
- };
23
- export type SummarizeKnowledgeGraphOptions = {
24
- projectRoot: string;
25
- artifactDir?: string;
26
- sampleSize?: number;
27
- };
28
- export declare function summarizeKnowledgeGraph(options: SummarizeKnowledgeGraphOptions): Promise<UnderstandGraphSummary>;
@@ -1,170 +0,0 @@
1
- import { stat } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { isDirectory, pathExists, readText } from 'peaks-loop-shared/fs';
4
- import { getErrorMessage } from 'peaks-loop-shared/result';
5
- import { loadPreferences } from '../preferences/preferences-service.js';
6
- function defaultArtifactDir(projectRoot) {
7
- return join(projectRoot, '.understand-anything');
8
- }
9
- function countArrayField(record, key) {
10
- const value = record[key];
11
- return Array.isArray(value) ? value.length : 0;
12
- }
13
- async function readFlag(path) {
14
- return { exists: await pathExists(path), path };
15
- }
16
- async function readGraph(graphPath) {
17
- if (!(await pathExists(graphPath))) {
18
- return { exists: false, path: graphPath };
19
- }
20
- const stats = await stat(graphPath);
21
- const raw = await readText(graphPath);
22
- try {
23
- const parsed = JSON.parse(raw);
24
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
25
- return {
26
- exists: true,
27
- path: graphPath,
28
- sizeBytes: stats.size,
29
- parseError: 'knowledge-graph.json must be a JSON object'
30
- };
31
- }
32
- const record = parsed;
33
- return {
34
- exists: true,
35
- path: graphPath,
36
- sizeBytes: stats.size,
37
- topLevelFields: Object.keys(record).sort(),
38
- counts: {
39
- nodes: countArrayField(record, 'nodes'),
40
- edges: countArrayField(record, 'edges'),
41
- layers: countArrayField(record, 'layers'),
42
- tours: countArrayField(record, 'tours')
43
- }
44
- };
45
- }
46
- catch (error) {
47
- return {
48
- exists: true,
49
- path: graphPath,
50
- sizeBytes: stats.size,
51
- parseError: getErrorMessage(error)
52
- };
53
- }
54
- }
55
- export async function scanUnderstandAnything(options) {
56
- const artifactDir = options.artifactDir ?? defaultArtifactDir(options.projectRoot);
57
- const exists = await isDirectory(artifactDir);
58
- // L3.1: read uaPrompt from preferences.json (graceful — returns 'unset' if missing/broken)
59
- const uaPrompt = await readUaPrompt(options.projectRoot);
60
- if (!exists) {
61
- return {
62
- exists: false,
63
- artifactDir,
64
- graph: { exists: false, path: join(artifactDir, 'knowledge-graph.json') },
65
- intermediate: { exists: false, path: join(artifactDir, 'intermediate') },
66
- diffOverlay: { exists: false, path: join(artifactDir, 'diff-overlay.json') },
67
- uaPrompt
68
- };
69
- }
70
- const graph = await readGraph(join(artifactDir, 'knowledge-graph.json'));
71
- const intermediate = await readFlag(join(artifactDir, 'intermediate'));
72
- const diffOverlay = await readFlag(join(artifactDir, 'diff-overlay.json'));
73
- return { exists: true, artifactDir, graph, intermediate, diffOverlay, uaPrompt };
74
- }
75
- async function readUaPrompt(projectRoot) {
76
- try {
77
- const prefs = await loadPreferences(projectRoot);
78
- return prefs.uaPrompt;
79
- }
80
- catch {
81
- return 'unset';
82
- }
83
- }
84
- function pickStringId(value) {
85
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
86
- return null;
87
- }
88
- const record = value;
89
- for (const key of ['id', 'path', 'name', 'label']) {
90
- const candidate = record[key];
91
- if (typeof candidate === 'string' && candidate.length > 0) {
92
- return candidate;
93
- }
94
- }
95
- return null;
96
- }
97
- function pickStringField(record, key) {
98
- const value = record[key];
99
- return typeof value === 'string' && value.length > 0 ? value : null;
100
- }
101
- function pickNameArray(record, key) {
102
- const value = record[key];
103
- if (!Array.isArray(value)) {
104
- return [];
105
- }
106
- return value
107
- .map((entry) => {
108
- if (typeof entry === 'string') {
109
- return entry;
110
- }
111
- return pickStringId(entry);
112
- })
113
- .filter((entry) => entry !== null);
114
- }
115
- export async function summarizeKnowledgeGraph(options) {
116
- const scanOptions = { projectRoot: options.projectRoot };
117
- if (options.artifactDir !== undefined) {
118
- scanOptions.artifactDir = options.artifactDir;
119
- }
120
- const scan = await scanUnderstandAnything(scanOptions);
121
- const sampleSize = options.sampleSize ?? 5;
122
- if (!scan.graph.exists) {
123
- return {
124
- exists: false,
125
- path: scan.graph.path,
126
- generatedAt: null,
127
- topLevelFields: [],
128
- counts: { nodes: 0, edges: 0, layers: 0, tours: 0 },
129
- layerNames: [],
130
- tourNames: [],
131
- sampleNodes: []
132
- };
133
- }
134
- if (scan.graph.parseError !== undefined) {
135
- return {
136
- exists: true,
137
- path: scan.graph.path,
138
- generatedAt: null,
139
- topLevelFields: [],
140
- counts: { nodes: 0, edges: 0, layers: 0, tours: 0 },
141
- layerNames: [],
142
- tourNames: [],
143
- sampleNodes: [],
144
- parseError: scan.graph.parseError
145
- };
146
- }
147
- const raw = await readText(scan.graph.path);
148
- const parsed = JSON.parse(raw);
149
- const record = parsed;
150
- const generatedAt = pickStringField(record, 'generatedAt');
151
- const nodes = Array.isArray(record.nodes) ? record.nodes : [];
152
- const sampleNodes = nodes.slice(0, sampleSize)
153
- .map((entry) => pickStringId(entry))
154
- .filter((entry) => entry !== null);
155
- return {
156
- exists: true,
157
- path: scan.graph.path,
158
- generatedAt,
159
- topLevelFields: Object.keys(record).sort(),
160
- counts: {
161
- nodes: countArrayField(record, 'nodes'),
162
- edges: countArrayField(record, 'edges'),
163
- layers: countArrayField(record, 'layers'),
164
- tours: countArrayField(record, 'tours')
165
- },
166
- layerNames: pickNameArray(record, 'layers'),
167
- tourNames: pickNameArray(record, 'tours'),
168
- sampleNodes
169
- };
170
- }
@@ -1,50 +0,0 @@
1
- export type UnderstandGraphReport = {
2
- exists: boolean;
3
- path: string;
4
- sizeBytes?: number;
5
- topLevelFields?: string[];
6
- counts?: {
7
- nodes: number;
8
- edges: number;
9
- layers: number;
10
- tours: number;
11
- };
12
- parseError?: string;
13
- };
14
- export type UnderstandFlagReport = {
15
- exists: boolean;
16
- path: string;
17
- };
18
- /**
19
- * Slice L3.1 — UA opt-in UX state. 'unset' triggers an opt-in prompt on
20
- * first scan; 'skip-this-session' suppresses the prompt for the current
21
- * session; 'skip-forever' writes to .peaks/preferences.json to suppress
22
- * all future prompts. Mirrors preferences.json:uaPrompt.
23
- */
24
- export type UaPromptDecision = 'unset' | 'skip-this-session' | 'skip-forever';
25
- export type UnderstandScanReport = {
26
- exists: boolean;
27
- artifactDir: string;
28
- graph: UnderstandGraphReport;
29
- intermediate: UnderstandFlagReport;
30
- diffOverlay: UnderstandFlagReport;
31
- /** Slice L3.1: opt-in UX state from preferences.json:uaPrompt. */
32
- readonly uaPrompt?: UaPromptDecision;
33
- };
34
- /**
35
- * Slice L3.1 — opt-in prompt payload. When uaPrompt === 'unset' and UA is
36
- * absent, the peaks-code / peaks-ide layer surfaces this to the user via
37
- * AskUserQuestion. The CLI does not prompt directly; it returns this
38
- * payload so the LLM-side UX layer can decide.
39
- */
40
- export interface UaOptInPrompt {
41
- readonly version: 1;
42
- readonly tool: 'ua-opt-in';
43
- readonly artifactDir: string;
44
- readonly reason: 'ua-artifact-missing';
45
- readonly options: readonly {
46
- readonly id: 'install' | 'fallback-this-session' | 'fallback-forever';
47
- readonly label: string;
48
- readonly description: string;
49
- }[];
50
- }
@@ -1 +0,0 @@
1
- export {};