peaks-loop 4.0.15 → 4.0.17

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 (90) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +47 -27
  5. package/dist/cli/commands/_register.js +4 -2
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/code-review-commands.d.ts +2 -30
  12. package/dist/cli/commands/code-review-commands.js +43 -65
  13. package/dist/cli/commands/cron-commands.js +16 -4
  14. package/dist/cli/commands/dashboard-long-run.js +8 -4
  15. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  16. package/dist/cli/commands/hooks-commands.js +56 -3
  17. package/dist/cli/commands/lint-commands.d.ts +21 -0
  18. package/dist/cli/commands/lint-commands.js +146 -0
  19. package/dist/cli/commands/qa-commands.js +1 -2
  20. package/dist/cli/commands/security-audit-commands.d.ts +1 -1
  21. package/dist/cli/commands/security-audit-commands.js +1 -1
  22. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  23. package/dist/cli/commands/slice-commands.js +20 -8
  24. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  25. package/dist/cli/commands/statusline-commands.js +17 -3
  26. package/dist/cli/commands/tech-commands.d.ts +1 -1
  27. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  28. package/dist/services/artifacts/request-artifact-service.js +10 -1
  29. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  30. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  31. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  32. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  33. package/dist/services/code/orchestrator-can-do.js +48 -2
  34. package/dist/services/code/post-compact-detector.d.ts +1 -1
  35. package/dist/services/code-review/ecc-bridge.d.ts +2 -7
  36. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  37. package/dist/services/config/config-service.d.ts +2 -19
  38. package/dist/services/config/config-service.js +2 -56
  39. package/dist/services/config/config-types.d.ts +4 -24
  40. package/dist/services/config/config-types.js +1 -10
  41. package/dist/services/container/container-lease.js +21 -8
  42. package/dist/services/context/spillover-store.d.ts +1 -1
  43. package/dist/services/crystallization/crystallization-types.js +20 -8
  44. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  45. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  46. package/dist/services/evolution/evolution-types.js +38 -21
  47. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  48. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  49. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  50. package/dist/services/ide/current-model-detector.js +1 -2
  51. package/dist/services/lint/detect-eslint.d.ts +10 -0
  52. package/dist/services/lint/detect-eslint.js +62 -0
  53. package/dist/services/lint/detect-ocr-18.d.ts +10 -0
  54. package/dist/services/lint/detect-ocr-18.js +40 -0
  55. package/dist/services/lint/eslint-runner.d.ts +57 -0
  56. package/dist/services/lint/eslint-runner.js +370 -0
  57. package/dist/services/lint/npx-resolver.d.ts +6 -0
  58. package/dist/services/lint/npx-resolver.js +47 -0
  59. package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
  60. package/dist/services/lint/ocr-multilang-adapter.js +143 -0
  61. package/dist/services/loop/loop-release-types.js +32 -13
  62. package/dist/services/loop/spec-service.d.ts +7 -1
  63. package/dist/services/loop/spec-service.js +218 -182
  64. package/dist/services/rd/rd-service.js +22 -9
  65. package/dist/services/scan/archetype-service.js +21 -9
  66. package/dist/services/session/binding-store.js +1 -2
  67. package/dist/services/share/bundle-reader.d.ts +6 -9
  68. package/dist/services/share/bundle-reader.js +297 -223
  69. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  70. package/dist/services/skills/hooks-settings-service.js +27 -1
  71. package/dist/services/slice/calibration-store.js +21 -8
  72. package/dist/services/slice/slice-check-service.js +35 -14
  73. package/dist/services/slice/slice-decompose-service.js +459 -223
  74. package/dist/services/standards/project-context.js +135 -73
  75. package/dist/services/verdict/envelopes.d.ts +1 -2
  76. package/dist/services/verdict/envelopes.js +1 -2
  77. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  78. package/dist/services/vm/vm-lease.js +19 -8
  79. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  80. package/dist/services/worktree/worktree-lease.js +19 -8
  81. package/dist/shared/fs-utils.d.ts +1 -1
  82. package/package.json +8 -12
  83. package/skills/bee/peaks-rd/SKILL.md +1 -1
  84. package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
  85. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
  86. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
  87. package/skills/peaks-code/SKILL.md +47 -1
  88. package/dist/services/code-review/ocr-service.d.ts +0 -129
  89. package/dist/services/code-review/ocr-service.js +0 -372
  90. package/skills/bee/peaks-rd/references/ocr-integration.md +0 -229
@@ -20,116 +20,93 @@
20
20
  */
21
21
  import { join, relative, dirname, basename } from 'node:path';
22
22
  import { calibrate } from './calibration-store.js';
23
+ export async function decomposeSlices(rid, prdMarkdown, projectRoot, options = {}) {
24
+ const runners = resolveRunners(options);
25
+ const stageOrder = [
26
+ 'validate-codegraph',
27
+ 'resolve-work-units',
28
+ 'build-dep-dag',
29
+ 'scc-critical-path',
30
+ 'min-cut',
31
+ 'partition-estimate-emit'
32
+ ];
33
+ const initial = createInitialState(rid, prdMarkdown, projectRoot, runners);
34
+ const final = await runPipeline(initial, stageOrder);
35
+ if (final.result === null) {
36
+ throw new Error('decomposeSlices FSM completed without emitting a result');
37
+ }
38
+ return final.result;
39
+ }
40
+ function createInitialState(rid, prdMarkdown, projectRoot, runners) {
41
+ return {
42
+ rid,
43
+ prdMarkdown,
44
+ projectRoot,
45
+ runners,
46
+ cgStatus: null,
47
+ workUnits: [],
48
+ depEdges: [],
49
+ kg: null,
50
+ understandAvailable: false,
51
+ codegraphAffectedCrossFile: false,
52
+ scc: null,
53
+ criticalPath: null,
54
+ minCut: null,
55
+ batchesWithEstimates: [],
56
+ result: null
57
+ };
58
+ }
59
+ async function runPipeline(state, stages) {
60
+ let current = state;
61
+ for (const stage of stages) {
62
+ current = await runStage(current, stage);
63
+ }
64
+ return current;
65
+ }
66
+ async function runStage(state, stage) {
67
+ switch (stage) {
68
+ case 'validate-codegraph':
69
+ return validateCodegraphStage(state);
70
+ case 'resolve-work-units':
71
+ return resolveWorkUnitsStage(state);
72
+ case 'build-dep-dag':
73
+ return buildDepDagStage(state);
74
+ case 'scc-critical-path':
75
+ return sccCriticalPathStage(state);
76
+ case 'min-cut':
77
+ return minCutStage(state);
78
+ case 'partition-estimate-emit':
79
+ return partitionEstimateEmitStage(state);
80
+ }
81
+ }
82
+ function resolveRunners(options) {
83
+ return {
84
+ cg: options.codegraphRunner ?? defaultCodegraphRunner(),
85
+ ur: options.understandRunner ?? defaultUnderstandRunner(),
86
+ ier: options.importEdgeRunner ?? defaultImportEdgeRunner()
87
+ };
88
+ }
23
89
  // =====================================================================
24
- // PUBLIC: decomposeSlices (the 6-stage pure algorithm)
90
+ // Stage 0: validate codegraph index
25
91
  // =====================================================================
26
- export async function decomposeSlices(rid, prdMarkdown, projectRoot, options = {}) {
27
- const cg = options.codegraphRunner ?? defaultCodegraphRunner();
28
- const ur = options.understandRunner ?? defaultUnderstandRunner();
29
- const ier = options.importEdgeRunner ?? defaultImportEdgeRunner();
30
- // ---------- Stage 0: Validate codegraph ----------
31
- const cgStatus = await cg.status(projectRoot);
92
+ async function validateCodegraphStage(state) {
93
+ const cgStatus = await state.runners.cg.status(state.projectRoot);
32
94
  if (!cgStatus.indexed) {
33
95
  throw new Error('peaks codegraph not initialised on this project. ' +
34
96
  'Run `npx codegraph init` then `npx codegraph index` in ' +
35
- projectRoot +
97
+ state.projectRoot +
36
98
  ' before invoking `peaks slice decompose`.');
37
99
  }
38
- // ---------- Read understand-anything ----------
39
- const kg = await ur.read(projectRoot);
40
- const understandAvailable = kg !== null;
41
- // ---------- Stage 1: Work-unit resolution ----------
42
- const acs = extractAcceptanceCriteria(prdMarkdown);
43
- let workUnits = await resolveWorkUnits(rid, acs, cg, projectRoot);
44
- // ---------- Stage 2: Dependency DAG ----------
45
- const explicitFiles = workUnits.flatMap((w) => w.files);
46
- const probeImports = await ier.importsOf(projectRoot, explicitFiles);
47
- const fileSet = new Set(explicitFiles);
48
- for (const e of probeImports) {
49
- fileSet.add(e.from);
50
- fileSet.add(e.to);
51
- }
52
- const allFiles = Array.from(fileSet);
53
- // If no ACs but the import graph has files, create implicit WUs (one per file).
54
- // This is the fallback path for the chain/diamond test cases.
55
- if (workUnits.length === 0 && allFiles.length > 0) {
56
- workUnits = allFiles.map((file, i) => ({
57
- id: `F${i + 1}`,
58
- label: basename(file),
59
- files: [file],
60
- loc: 110,
61
- testsAdded: 0,
62
- filePath: file,
63
- candidates: [`file:${file}`]
64
- }));
65
- }
66
- let codegraphAffectedCrossFile = false;
67
- if (allFiles.length > 0) {
68
- try {
69
- const aff = await cg.affected(allFiles, projectRoot);
70
- codegraphAffectedCrossFile = aff.totalDependentsTraversed > 0;
71
- }
72
- catch {
73
- codegraphAffectedCrossFile = false;
74
- }
75
- }
76
- const importEdges = await ier.importsOf(projectRoot, allFiles);
77
- const depEdges = buildDependencyEdges(workUnits, importEdges, kg, projectRoot);
78
- // ---------- Stage 3: SCC + critical path ----------
79
- const nodeIds = workUnits.map((w) => w.id);
80
- const scc = findSCCs(nodeIds, depEdges);
81
- const criticalPath = findCriticalPath(workUnits, depEdges);
82
- // ---------- Stage 4: Min-cut ----------
83
- const minCut = findMinCut(workUnits, depEdges, criticalPath, kg);
84
- // ---------- Stage 5+6: Partition + Estimate + Emit ----------
85
- const parallelBatches = partitionIntoBatches(workUnits, depEdges, criticalPath);
86
- const batchesWithEstimates = parallelBatches.map((b) => ({
87
- ...b,
88
- slices: b.slices.map((s) => ({
89
- ...s,
90
- estimate: estimateSlice(s, kg)
91
- }))
92
- }));
93
- const totalSlices = batchesWithEstimates.reduce((sum, b) => sum + b.slices.length, 0);
94
- const pickHint = totalSlices > 10
95
- ? `slice count is ${totalSlices} (>10); split into multiple pick sessions for operator comfort`
96
- : undefined;
97
- const codegraphEnvelope = {
98
- nodes: cgStatus.nodes,
99
- edges: cgStatus.edges,
100
- dbMB: cgStatus.dbMB,
101
- freshness: cgStatus.freshness,
102
- affectedCrossFile: codegraphAffectedCrossFile,
103
- note: codegraphAffectedCrossFile
104
- ? 'cross-file dependents resolved via codegraph.affected'
105
- : 'codegraph.affected returned 0 cross-file dependents (v0.7.10 limitation); used real import edges'
106
- };
107
- const understandEnvelope = {
108
- kgNodes: kg?.nodes.length ?? 0,
109
- kgEdges: kg?.edges.length ?? 0,
110
- available: understandAvailable,
111
- fallback: understandAvailable ? 'semantic' : 'structural-only',
112
- note: understandAvailable
113
- ? 'read from .understand-anything/knowledge-graph.json'
114
- : '.understand-anything/knowledge-graph.json not found; algorithm falls back to structural cuts'
115
- };
116
- return {
117
- rid,
118
- generatedAt: new Date().toISOString(),
119
- codegraph: codegraphEnvelope,
120
- understandAnything: understandEnvelope,
121
- workUnits,
122
- dependencyDAG: { edges: depEdges },
123
- sccAnalysis: scc,
124
- criticalPath,
125
- minCutResult: minCut,
126
- parallelBatches: batchesWithEstimates,
127
- ...(pickHint !== undefined ? { pickHint } : {})
128
- };
100
+ return { ...state, cgStatus };
129
101
  }
130
102
  // =====================================================================
131
- // Stage 1 helpers
103
+ // Stage 1: work-unit resolution
132
104
  // =====================================================================
105
+ async function resolveWorkUnitsStage(state) {
106
+ const acs = extractAcceptanceCriteria(state.prdMarkdown);
107
+ const workUnits = await resolveWorkUnits(state.rid, acs, state.runners.cg, state.projectRoot);
108
+ return { ...state, workUnits };
109
+ }
133
110
  function extractAcceptanceCriteria(prd) {
134
111
  const acs = [];
135
112
  const lines = prd.split('\n');
@@ -190,58 +167,139 @@ function matchAcToHit(ac, hits) {
190
167
  return hits[0];
191
168
  }
192
169
  // =====================================================================
193
- // Stage 2 helpers
170
+ // Stage 2: dependency DAG
194
171
  // =====================================================================
195
- function buildDependencyEdges(wus, importEdges, kg, projectRoot) {
172
+ async function buildDepDagStage(state) {
173
+ const kg = await state.runners.ur.read(state.projectRoot);
174
+ const understandAvailable = kg !== null;
175
+ const fileSet = await collectFileSet(state.workUnits, state.runners.ier, state.projectRoot);
176
+ const allFiles = Array.from(fileSet);
177
+ const codegraphAffectedCrossFile = await probeCrossFileAffected(allFiles, state.runners.cg, state.projectRoot);
178
+ const workUnits = applyImplicitWuFallback(state.workUnits, allFiles);
179
+ const importEdges = await state.runners.ier.importsOf(state.projectRoot, allFiles);
180
+ const depEdges = buildDependencyEdges(workUnits, importEdges, kg, state.projectRoot);
181
+ return { ...state, kg, understandAvailable, codegraphAffectedCrossFile, workUnits, depEdges };
182
+ }
183
+ async function collectFileSet(workUnits, ier, projectRoot) {
184
+ const explicitFiles = workUnits.flatMap((w) => w.files);
185
+ const probeImports = await ier.importsOf(projectRoot, explicitFiles);
186
+ const fileSet = new Set(explicitFiles);
187
+ for (const e of probeImports) {
188
+ fileSet.add(e.from);
189
+ fileSet.add(e.to);
190
+ }
191
+ return fileSet;
192
+ }
193
+ async function probeCrossFileAffected(allFiles, cg, projectRoot) {
194
+ if (allFiles.length === 0)
195
+ return false;
196
+ try {
197
+ const aff = await cg.affected(allFiles, projectRoot);
198
+ return aff.totalDependentsTraversed > 0;
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
204
+ /** If no ACs but the import graph has files, create implicit WUs (one
205
+ * per file). This is the fallback path for the chain/diamond test
206
+ * cases. */
207
+ function applyImplicitWuFallback(workUnits, allFiles) {
208
+ if (workUnits.length > 0 || allFiles.length === 0)
209
+ return [...workUnits];
210
+ return allFiles.map((file, i) => ({
211
+ id: `F${i + 1}`,
212
+ label: basename(file),
213
+ files: [file],
214
+ loc: 110,
215
+ testsAdded: 0,
216
+ filePath: file,
217
+ candidates: [`file:${file}`]
218
+ }));
219
+ }
220
+ function indexFilesToWorkUnits(wus) {
196
221
  const fileToWu = new Map();
197
222
  for (const wu of wus) {
198
223
  for (const f of wu.files) {
199
224
  fileToWu.set(f, wu.id);
200
225
  }
201
226
  }
202
- const edges = [];
203
- const seen = new Set();
204
- const addEdge = (from, to, kind, weight, evidence, isSemantic, confidence) => {
205
- if (from === to)
206
- return;
207
- const key = `${from}->${to}|${kind}`;
208
- if (seen.has(key))
209
- return;
210
- seen.add(key);
211
- edges.push({ from, to, kind, weight, evidence, isSemantic, confidence });
212
- };
227
+ return fileToWu;
228
+ }
229
+ function classifyImportEdge(imp, fileToWu, projectRoot) {
230
+ let fromWu = fileToWu.get(imp.from);
231
+ let toWu = fileToWu.get(imp.to);
232
+ if (!fromWu) {
233
+ const resolved = resolveRelativeImport(projectRoot, imp.from, imp.evidence);
234
+ fromWu = fileToWu.get(resolved);
235
+ }
236
+ if (!toWu) {
237
+ const resolved = resolveRelativeImport(projectRoot, imp.from, imp.evidence);
238
+ toWu = fileToWu.get(resolved);
239
+ }
240
+ if (!fromWu || !toWu)
241
+ return null;
242
+ return { fromWu, toWu, evidence: imp.evidence };
243
+ }
244
+ function pushIfNew(collector, edge) {
245
+ if (edge.from === edge.to)
246
+ return;
247
+ const key = `${edge.from}->${edge.to}|${edge.kind}`;
248
+ if (collector.seen.has(key))
249
+ return;
250
+ collector.seen.add(key);
251
+ collector.edges.push(edge);
252
+ }
253
+ function collectImportEdges(importEdges, fileToWu, projectRoot, collector) {
213
254
  for (const imp of importEdges) {
214
- let fromWu = fileToWu.get(imp.from);
215
- let toWu = fileToWu.get(imp.to);
216
- if (!fromWu) {
217
- const resolved = resolveRelativeImport(projectRoot, imp.from, imp.evidence);
218
- fromWu = fileToWu.get(resolved);
219
- }
220
- if (!toWu) {
221
- const resolved = resolveRelativeImport(projectRoot, imp.from, imp.evidence);
222
- toWu = fileToWu.get(resolved);
223
- }
224
- if (fromWu && toWu) {
225
- addEdge(fromWu, toWu, 'imports', 10.0, imp.evidence, false, 'structural');
226
- }
255
+ const classified = classifyImportEdge(imp, fileToWu, projectRoot);
256
+ if (classified === null)
257
+ continue;
258
+ const edge = {
259
+ from: classified.fromWu,
260
+ to: classified.toWu,
261
+ kind: 'imports',
262
+ weight: 10.0,
263
+ evidence: classified.evidence,
264
+ isSemantic: false,
265
+ confidence: 'structural'
266
+ };
267
+ pushIfNew(collector, edge);
227
268
  }
269
+ }
270
+ function collectSemanticEdges(kg, fileToWu, collector) {
271
+ for (const e of kg.edges) {
272
+ if (e.type !== 'contains_flow' && e.type !== 'flow_step')
273
+ continue;
274
+ const fromNode = kg.nodes.find((n) => n.id === e.source);
275
+ const toNode = kg.nodes.find((n) => n.id === e.target);
276
+ if (!fromNode?.filePath || !toNode?.filePath)
277
+ continue;
278
+ const fromWu = fileToWu.get(fromNode.filePath);
279
+ const toWu = fileToWu.get(toNode.filePath);
280
+ if (!fromWu || !toWu || fromWu === toWu)
281
+ continue;
282
+ const weight = e.type === 'flow_step' ? 0.05 : 0.1;
283
+ const edge = {
284
+ from: fromWu,
285
+ to: toWu,
286
+ kind: e.type,
287
+ weight,
288
+ evidence: `understand-anything: ${e.type} ${e.source}->${e.target}`,
289
+ isSemantic: true,
290
+ confidence: 'semantic'
291
+ };
292
+ pushIfNew(collector, edge);
293
+ }
294
+ }
295
+ function buildDependencyEdges(wus, importEdges, kg, projectRoot) {
296
+ const fileToWu = indexFilesToWorkUnits(wus);
297
+ const collector = { edges: [], seen: new Set() };
298
+ collectImportEdges(importEdges, fileToWu, projectRoot, collector);
228
299
  if (kg !== null) {
229
- for (const e of kg.edges) {
230
- if (e.type !== 'contains_flow' && e.type !== 'flow_step')
231
- continue;
232
- const fromNode = kg.nodes.find((n) => n.id === e.source);
233
- const toNode = kg.nodes.find((n) => n.id === e.target);
234
- if (!fromNode?.filePath || !toNode?.filePath)
235
- continue;
236
- const fromWu = fileToWu.get(fromNode.filePath);
237
- const toWu = fileToWu.get(toNode.filePath);
238
- if (fromWu && toWu && fromWu !== toWu) {
239
- const weight = e.type === 'flow_step' ? 0.05 : 0.1;
240
- addEdge(fromWu, toWu, e.type, weight, `understand-anything: ${e.type} ${e.source}->${e.target}`, true, 'semantic');
241
- }
242
- }
300
+ collectSemanticEdges(kg, fileToWu, collector);
243
301
  }
244
- return edges;
302
+ return collector.edges;
245
303
  }
246
304
  function resolveRelativeImport(projectRoot, sourceFile, evidence) {
247
305
  const match = /from\s+['"]([^'"]+)['"]/.exec(evidence);
@@ -257,7 +315,19 @@ function resolveRelativeImport(projectRoot, sourceFile, evidence) {
257
315
  // =====================================================================
258
316
  // Stage 3: Tarjan SCC + longest path
259
317
  // =====================================================================
318
+ function sccCriticalPathStage(state) {
319
+ const nodeIds = state.workUnits.map((w) => w.id);
320
+ const scc = findSCCs(nodeIds, state.depEdges);
321
+ const criticalPath = findCriticalPath(state.workUnits, state.depEdges);
322
+ return { ...state, scc, criticalPath };
323
+ }
260
324
  function findSCCs(nodeIds, edges) {
325
+ const adj = buildAdjacencyList(nodeIds, edges);
326
+ const sccs = runTarjan(nodeIds, adj);
327
+ return summariseSccs(sccs, edges);
328
+ }
329
+ /** Build a forward adjacency map: nodeId → list of distinct neighbours. */
330
+ function buildAdjacencyList(nodeIds, edges) {
261
331
  const adj = new Map();
262
332
  for (const id of nodeIds)
263
333
  adj.set(id, []);
@@ -266,42 +336,66 @@ function findSCCs(nodeIds, edges) {
266
336
  if (list && !list.includes(e.to))
267
337
  list.push(e.to);
268
338
  }
269
- let index = 0;
270
- const idx = new Map();
271
- const lowlink = new Map();
272
- const onStack = new Set();
273
- const stack = [];
274
- const sccs = [];
339
+ return adj;
340
+ }
341
+ function createTarjanVisitor() {
342
+ return {
343
+ index: 0,
344
+ idx: new Map(),
345
+ lowlink: new Map(),
346
+ onStack: new Set(),
347
+ stack: [],
348
+ sccs: []
349
+ };
350
+ }
351
+ function enterNode(visitor, v) {
352
+ visitor.idx.set(v, visitor.index);
353
+ visitor.lowlink.set(v, visitor.index);
354
+ visitor.index++;
355
+ visitor.stack.push(v);
356
+ visitor.onStack.add(v);
357
+ }
358
+ function relaxEdge(visitor, v, w, recurse) {
359
+ if (!visitor.idx.has(w)) {
360
+ recurse(w);
361
+ visitor.lowlink.set(v, Math.min(visitor.lowlink.get(v) ?? 0, visitor.lowlink.get(w) ?? 0));
362
+ }
363
+ else if (visitor.onStack.has(w)) {
364
+ visitor.lowlink.set(v, Math.min(visitor.lowlink.get(v) ?? 0, visitor.idx.get(w) ?? 0));
365
+ }
366
+ }
367
+ function finaliseRoot(visitor, v) {
368
+ if ((visitor.lowlink.get(v) ?? 0) !== (visitor.idx.get(v) ?? 0))
369
+ return;
370
+ const component = [];
371
+ let w;
372
+ do {
373
+ w = visitor.stack.pop();
374
+ visitor.onStack.delete(w);
375
+ component.push(w);
376
+ } while (w !== v);
377
+ visitor.sccs.push(component);
378
+ }
379
+ /** Run Tarjan's SCC algorithm; returns the list of components in
380
+ * discovery order. Visitor state is hoisted to a named struct
381
+ * (`TarjanVisitor`) so the recursive body can be split into
382
+ * enterNode / relaxEdge / finaliseRoot without parameter sprawl. */
383
+ function runTarjan(nodeIds, adj) {
384
+ const visitor = createTarjanVisitor();
275
385
  const strongconnect = (v) => {
276
- idx.set(v, index);
277
- lowlink.set(v, index);
278
- index++;
279
- stack.push(v);
280
- onStack.add(v);
386
+ enterNode(visitor, v);
281
387
  for (const w of adj.get(v) ?? []) {
282
- if (!idx.has(w)) {
283
- strongconnect(w);
284
- lowlink.set(v, Math.min(lowlink.get(v) ?? 0, lowlink.get(w) ?? 0));
285
- }
286
- else if (onStack.has(w)) {
287
- lowlink.set(v, Math.min(lowlink.get(v) ?? 0, idx.get(w) ?? 0));
288
- }
289
- }
290
- if (lowlink.get(v) === idx.get(v)) {
291
- const component = [];
292
- let w;
293
- do {
294
- w = stack.pop();
295
- onStack.delete(w);
296
- component.push(w);
297
- } while (w !== v);
298
- sccs.push(component);
388
+ relaxEdge(visitor, v, w, strongconnect);
299
389
  }
390
+ finaliseRoot(visitor, v);
300
391
  };
301
392
  for (const id of nodeIds) {
302
- if (!idx.has(id))
393
+ if (!visitor.idx.has(id))
303
394
  strongconnect(id);
304
395
  }
396
+ return visitor.sccs;
397
+ }
398
+ function summariseSccs(sccs, edges) {
305
399
  const trivial = [];
306
400
  const nonTrivial = [];
307
401
  for (const scc of sccs) {
@@ -329,11 +423,26 @@ function sameScc(sccs, a, b) {
329
423
  }
330
424
  return false;
331
425
  }
426
+ /** Critical-path FSM: three sequential phases.
427
+ * 1. `phaseTopoOrder` -- Kahn's algorithm with cycle-tolerance fallback.
428
+ * 2. `phaseLongestDist` -- DP relaxation along the topo order.
429
+ * 3. `phaseReconstruct` -- backtrack + aggregate metrics.
430
+ * Each phase has its own branch budget; the orchestrator stays linear. */
332
431
  function findCriticalPath(wus, edges) {
432
+ const locById = indexLocById(wus);
433
+ const adj = buildForwardAdjacency(wus, edges, locById);
434
+ const topoOrder = phaseTopoOrder(wus, adj);
435
+ const { dist, prev } = phaseLongestDist(wus, adj, topoOrder, locById);
436
+ return phaseReconstruct(wus, locById, topoOrder, dist, prev);
437
+ }
438
+ function indexLocById(wus) {
333
439
  const locById = new Map();
334
440
  for (const wu of wus) {
335
441
  locById.set(wu.id, wu.loc);
336
442
  }
443
+ return locById;
444
+ }
445
+ function buildForwardAdjacency(wus, edges, locById) {
337
446
  const adj = new Map();
338
447
  for (const wu of wus)
339
448
  adj.set(wu.id, []);
@@ -342,15 +451,33 @@ function findCriticalPath(wus, edges) {
342
451
  if (list)
343
452
  list.push({ to: e.to, weight: locById.get(e.to) ?? 0 });
344
453
  }
454
+ return adj;
455
+ }
456
+ function phaseTopoOrder(wus, adj) {
457
+ const indeg = computeIndegrees(wus, adj);
458
+ const queue = seedQueue(indeg);
459
+ const topoOrder = drainKahnQueue(queue, indeg, adj);
460
+ appendCycleLeftovers(wus, topoOrder);
461
+ return topoOrder;
462
+ }
463
+ function computeIndegrees(wus, adj) {
345
464
  const indeg = new Map();
346
465
  for (const wu of wus)
347
466
  indeg.set(wu.id, 0);
348
- for (const e of edges)
349
- indeg.set(e.to, (indeg.get(e.to) ?? 0) + 1);
467
+ for (const e of adj.values()) {
468
+ for (const { to } of e)
469
+ indeg.set(to, (indeg.get(to) ?? 0) + 1);
470
+ }
471
+ return indeg;
472
+ }
473
+ function seedQueue(indeg) {
350
474
  const queue = [];
351
475
  for (const [id, d] of indeg)
352
476
  if (d === 0)
353
477
  queue.push(id);
478
+ return queue;
479
+ }
480
+ function drainKahnQueue(queue, indeg, adj) {
354
481
  const topoOrder = [];
355
482
  while (queue.length > 0) {
356
483
  const n = queue.shift();
@@ -361,9 +488,15 @@ function findCriticalPath(wus, edges) {
361
488
  queue.push(to);
362
489
  }
363
490
  }
491
+ return topoOrder;
492
+ }
493
+ /** Cycle-tolerance fallback: append any wu that didn't make it through Kahn. */
494
+ function appendCycleLeftovers(wus, topoOrder) {
364
495
  for (const wu of wus)
365
496
  if (!topoOrder.includes(wu.id))
366
497
  topoOrder.push(wu.id);
498
+ }
499
+ function phaseLongestDist(wus, adj, topoOrder, locById) {
367
500
  const dist = new Map();
368
501
  const prev = new Map();
369
502
  for (const wu of wus) {
@@ -379,6 +512,26 @@ function findCriticalPath(wus, edges) {
379
512
  }
380
513
  }
381
514
  }
515
+ return { dist, prev };
516
+ }
517
+ function phaseReconstruct(wus, locById, topoOrder, dist, prev) {
518
+ const endNode = pickEndNode(topoOrder, dist);
519
+ const path = backtrackPath(endNode, prev);
520
+ const totalLoc = path.reduce((sum, id) => sum + (locById.get(id) ?? 0), 0);
521
+ const totalDeltaLoc = path.reduce((sum, id) => {
522
+ const wu = wus.find((w) => w.id === id);
523
+ return sum + (wu?.deltaLoc ?? wu?.loc ?? 0);
524
+ }, 0);
525
+ const edgeStrs = path.slice(0, -1).map((n, i) => `${n} -> ${path[i + 1]}`);
526
+ return {
527
+ nodes: path,
528
+ edges: edgeStrs,
529
+ totalLoc,
530
+ totalDeltaLoc,
531
+ rationale: `Longest path by sum of LoC across the dependency DAG; ${path.length} work units, ${totalLoc} LoC summed`
532
+ };
533
+ }
534
+ function pickEndNode(topoOrder, dist) {
382
535
  let endNode = topoOrder[0];
383
536
  let maxDist = -1;
384
537
  for (const [id, d] of dist) {
@@ -387,29 +540,26 @@ function findCriticalPath(wus, edges) {
387
540
  endNode = id;
388
541
  }
389
542
  }
543
+ return endNode;
544
+ }
545
+ function backtrackPath(endNode, prev) {
390
546
  const path = [];
391
547
  let cur = endNode;
392
548
  while (cur !== null) {
393
549
  path.unshift(cur);
394
550
  cur = prev.get(cur) ?? null;
395
551
  }
396
- const totalLoc = path.reduce((sum, id) => sum + (locById.get(id) ?? 0), 0);
397
- const totalDeltaLoc = path.reduce((sum, id) => {
398
- const wu = wus.find((w) => w.id === id);
399
- return sum + (wu?.deltaLoc ?? wu?.loc ?? 0);
400
- }, 0);
401
- const edgeStrs = path.slice(0, -1).map((n, i) => `${n} -> ${path[i + 1]}`);
402
- return {
403
- nodes: path,
404
- edges: edgeStrs,
405
- totalLoc,
406
- totalDeltaLoc,
407
- rationale: `Longest path by sum of LoC across the dependency DAG; ${path.length} work units, ${totalLoc} LoC summed`
408
- };
552
+ return path;
409
553
  }
410
554
  // =====================================================================
411
555
  // Stage 4: Min-cut
412
556
  // =====================================================================
557
+ function minCutStage(state) {
558
+ if (state.criticalPath === null)
559
+ return state;
560
+ const minCut = findMinCut(state.workUnits, state.depEdges, state.criticalPath, state.kg);
561
+ return { ...state, minCut };
562
+ }
413
563
  function findMinCut(_wus, edges, criticalPath, _kg) {
414
564
  const cpSet = new Set(criticalPath.nodes);
415
565
  const nonCpEdges = edges.filter((e) => !cpSet.has(e.from) || !cpSet.has(e.to));
@@ -440,14 +590,95 @@ function findMinCut(_wus, edges, criticalPath, _kg) {
440
590
  };
441
591
  }
442
592
  // =====================================================================
443
- // Stage 5+6: Estimate + Partition
593
+ // Stage 5+6: Estimate + Partition + Emit
444
594
  // =====================================================================
595
+ function partitionEstimateEmitStage(state) {
596
+ if (state.criticalPath === null)
597
+ return state;
598
+ const parallelBatches = partitionIntoBatches(state.workUnits, state.depEdges, state.criticalPath);
599
+ const batchesWithEstimates = parallelBatches.map((b) => ({
600
+ ...b,
601
+ slices: b.slices.map((s) => ({
602
+ ...s,
603
+ estimate: estimateSlice(s, state.kg)
604
+ }))
605
+ }));
606
+ const result = buildDecompositionResult(state, batchesWithEstimates);
607
+ return { ...state, batchesWithEstimates, result };
608
+ }
609
+ function buildDecompositionResult(state, batchesWithEstimates) {
610
+ assertReadyForEmit(state);
611
+ const totalSlices = batchesWithEstimates.reduce((sum, b) => sum + b.slices.length, 0);
612
+ const pickHint = buildPickHint(totalSlices);
613
+ const codegraphEnvelope = buildCodegraphEnvelope(state);
614
+ const understandEnvelope = buildUnderstandEnvelope(state);
615
+ return {
616
+ rid: state.rid,
617
+ generatedAt: new Date().toISOString(),
618
+ codegraph: codegraphEnvelope,
619
+ understandAnything: understandEnvelope,
620
+ workUnits: state.workUnits,
621
+ dependencyDAG: { edges: state.depEdges },
622
+ sccAnalysis: state.scc,
623
+ criticalPath: state.criticalPath,
624
+ minCutResult: state.minCut,
625
+ parallelBatches: batchesWithEstimates,
626
+ ...(pickHint !== undefined ? { pickHint } : {})
627
+ };
628
+ }
629
+ function assertReadyForEmit(state) {
630
+ if (state.cgStatus === null ||
631
+ state.criticalPath === null ||
632
+ state.scc === null ||
633
+ state.minCut === null) {
634
+ throw new Error('decomposeSlices FSM reached emit stage without completing prior stages');
635
+ }
636
+ }
637
+ function buildPickHint(totalSlices) {
638
+ if (totalSlices <= 10)
639
+ return undefined;
640
+ return `slice count is ${totalSlices} (>10); split into multiple pick sessions for operator comfort`;
641
+ }
642
+ function buildCodegraphEnvelope(state) {
643
+ const cgStatus = state.cgStatus;
644
+ if (cgStatus === null) {
645
+ throw new Error('buildCodegraphEnvelope called before validate-codegraph stage');
646
+ }
647
+ return {
648
+ nodes: cgStatus.nodes,
649
+ edges: cgStatus.edges,
650
+ dbMB: cgStatus.dbMB,
651
+ freshness: cgStatus.freshness,
652
+ affectedCrossFile: state.codegraphAffectedCrossFile,
653
+ note: state.codegraphAffectedCrossFile
654
+ ? 'cross-file dependents resolved via codegraph.affected'
655
+ : 'codegraph.affected returned 0 cross-file dependents (v0.7.10 limitation); used real import edges'
656
+ };
657
+ }
658
+ function buildUnderstandEnvelope(state) {
659
+ return {
660
+ kgNodes: state.kg?.nodes.length ?? 0,
661
+ kgEdges: state.kg?.edges.length ?? 0,
662
+ available: state.understandAvailable,
663
+ fallback: state.understandAvailable ? 'semantic' : 'structural-only',
664
+ note: state.understandAvailable
665
+ ? 'read from .understand-anything/knowledge-graph.json'
666
+ : '.understand-anything/knowledge-graph.json not found; algorithm falls back to structural cuts'
667
+ };
668
+ }
445
669
  function estimateSlice(slice, _kg) {
446
670
  const sampleSize = 0;
447
671
  const complexitySum = 0;
448
672
  return calibrate(complexitySum, slice.estimate.testCount, slice.estimate.locSum, sampleSize);
449
673
  }
450
674
  function partitionIntoBatches(wus, edges, _criticalPath) {
675
+ if (wus.length === 0)
676
+ return [];
677
+ const upstream = computeUpstream(wus, edges);
678
+ return runBatchScheduler(wus, upstream);
679
+ }
680
+ /** Build reverse adjacency: id → list of upstream wu ids that depend on it. */
681
+ function computeUpstream(wus, edges) {
451
682
  const upstream = new Map();
452
683
  for (const wu of wus)
453
684
  upstream.set(wu.id, []);
@@ -456,55 +687,60 @@ function partitionIntoBatches(wus, edges, _criticalPath) {
456
687
  if (list && !list.includes(e.from))
457
688
  list.push(e.from);
458
689
  }
690
+ return upstream;
691
+ }
692
+ /** Iterate Kahn-style batches until every wu is placed. The seed
693
+ * batch contains all wus with no upstream; subsequent batches pick
694
+ * the wus whose upstream is fully placed. */
695
+ function runBatchScheduler(wus, upstream) {
459
696
  const placed = new Set();
460
697
  const batches = [];
461
- if (wus.length === 0)
462
- return batches;
463
- let currentBatch = [];
464
- for (const wu of wus) {
465
- if ((upstream.get(wu.id) ?? []).length === 0) {
466
- currentBatch.push(wu.id);
467
- }
468
- }
469
- batches.push({
470
- batch: 1,
471
- dependsOn: [],
472
- slices: currentBatch.map((id) => wuToSlice(wus.find((w) => w.id === id))),
473
- parallelizableWithinBatch: currentBatch.length > 1
474
- });
475
- for (const id of currentBatch)
698
+ const seedIds = wus
699
+ .filter((wu) => (upstream.get(wu.id) ?? []).length === 0)
700
+ .map((wu) => wu.id);
701
+ batches.push(materialiseBatch(1, [], seedIds, wus));
702
+ for (const id of seedIds)
476
703
  placed.add(id);
477
704
  let batchNum = 2;
478
705
  let prevBatchNums = [1];
479
706
  while (placed.size < wus.length) {
480
- currentBatch = [];
481
- for (const wu of wus) {
482
- if (placed.has(wu.id))
483
- continue;
484
- const ups = upstream.get(wu.id) ?? [];
485
- if (ups.every((u) => placed.has(u))) {
486
- currentBatch.push(wu.id);
487
- }
488
- }
489
- if (currentBatch.length === 0) {
490
- for (const wu of wus) {
491
- if (!placed.has(wu.id))
492
- currentBatch.push(wu.id);
493
- }
494
- }
495
- batches.push({
496
- batch: batchNum,
497
- dependsOn: prevBatchNums,
498
- slices: currentBatch.map((id) => wuToSlice(wus.find((w) => w.id === id))),
499
- parallelizableWithinBatch: currentBatch.length > 1
500
- });
501
- for (const id of currentBatch)
707
+ const ready = pickReadyBatch(wus, upstream, placed);
708
+ batches.push(materialiseBatch(batchNum, prevBatchNums, ready, wus));
709
+ for (const id of ready)
502
710
  placed.add(id);
503
711
  prevBatchNums = [batchNum];
504
712
  batchNum++;
505
713
  }
506
714
  return batches;
507
715
  }
716
+ function pickReadyBatch(wus, upstream, placed) {
717
+ const ready = [];
718
+ for (const wu of wus) {
719
+ if (placed.has(wu.id))
720
+ continue;
721
+ const ups = upstream.get(wu.id) ?? [];
722
+ if (ups.every((u) => placed.has(u))) {
723
+ ready.push(wu.id);
724
+ }
725
+ }
726
+ if (ready.length > 0)
727
+ return ready;
728
+ // Fallback: pick whatever is unplaced (shouldn't happen on a valid DAG,
729
+ // but keeps the loop total when cycles sneak through).
730
+ for (const wu of wus) {
731
+ if (!placed.has(wu.id))
732
+ ready.push(wu.id);
733
+ }
734
+ return ready;
735
+ }
736
+ function materialiseBatch(batchNum, dependsOn, ids, wus) {
737
+ return {
738
+ batch: batchNum,
739
+ dependsOn: [...dependsOn],
740
+ slices: ids.map((id) => wuToSlice(wus.find((w) => w.id === id))),
741
+ parallelizableWithinBatch: ids.length > 1
742
+ };
743
+ }
508
744
  function wuToSlice(wu) {
509
745
  return {
510
746
  rid: `${wu.id}-slice`,