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.
- package/CHANGELOG.md +82 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +47 -27
- package/dist/cli/commands/_register.js +4 -2
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/code-review-commands.d.ts +2 -30
- package/dist/cli/commands/code-review-commands.js +43 -65
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/lint-commands.d.ts +21 -0
- package/dist/cli/commands/lint-commands.js +146 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/security-audit-commands.d.ts +1 -1
- package/dist/cli/commands/security-audit-commands.js +1 -1
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/code-review/ecc-bridge.d.ts +2 -7
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-service.d.ts +2 -19
- package/dist/services/config/config-service.js +2 -56
- package/dist/services/config/config-types.d.ts +4 -24
- package/dist/services/config/config-types.js +1 -10
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.d.ts +10 -0
- package/dist/services/lint/detect-eslint.js +62 -0
- package/dist/services/lint/detect-ocr-18.d.ts +10 -0
- package/dist/services/lint/detect-ocr-18.js +40 -0
- package/dist/services/lint/eslint-runner.d.ts +57 -0
- package/dist/services/lint/eslint-runner.js +370 -0
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
- package/dist/services/lint/ocr-multilang-adapter.js +143 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +27 -1
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +8 -12
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
- package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
- package/skills/peaks-code/SKILL.md +47 -1
- package/dist/services/code-review/ocr-service.d.ts +0 -129
- package/dist/services/code-review/ocr-service.js +0 -372
- 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
|
-
//
|
|
90
|
+
// Stage 0: validate codegraph index
|
|
25
91
|
// =====================================================================
|
|
26
|
-
|
|
27
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
170
|
+
// Stage 2: dependency DAG
|
|
194
171
|
// =====================================================================
|
|
195
|
-
function
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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`,
|