deepflow 0.1.91 → 0.1.92
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/bin/ratchet.js +67 -2
- package/bin/ratchet.test.js +308 -4
- package/bin/wave-runner.js +259 -0
- package/bin/wave-runner.test.js +556 -0
- package/package.json +1 -1
- package/src/commands/df/execute.md +83 -8
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deepflow wave-runner
|
|
4
|
+
* Parses PLAN.md, resolves dependency DAG, outputs tasks grouped by execution wave.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node bin/wave-runner.js [--plan <path>] [--recalc --failed T{N}[,T{N}...]]
|
|
8
|
+
*
|
|
9
|
+
* Output (plain text):
|
|
10
|
+
* Wave 1: T1 — description, T4 — description
|
|
11
|
+
* Wave 2: T2 — description
|
|
12
|
+
* ...
|
|
13
|
+
*
|
|
14
|
+
* Exit codes: 0=success, 1=parse error
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// CLI arg parsing
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const args = { plan: 'PLAN.md', recalc: false, failed: [] };
|
|
28
|
+
let i = 2;
|
|
29
|
+
while (i < argv.length) {
|
|
30
|
+
const arg = argv[i];
|
|
31
|
+
if (arg === '--plan' && argv[i + 1]) {
|
|
32
|
+
args.plan = argv[++i];
|
|
33
|
+
} else if (arg === '--recalc') {
|
|
34
|
+
args.recalc = true;
|
|
35
|
+
} else if (arg === '--failed' && argv[i + 1]) {
|
|
36
|
+
// Accept comma-separated: --failed T3,T5 or space-separated: --failed T3 --failed T5
|
|
37
|
+
const raw = argv[++i];
|
|
38
|
+
args.failed.push(...raw.split(',').map(s => s.trim()).filter(Boolean));
|
|
39
|
+
}
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// PLAN.md parser
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract pending tasks from PLAN.md text.
|
|
51
|
+
* Returns array of { id, description, blockedBy: string[] }
|
|
52
|
+
*
|
|
53
|
+
* Recognises lines like:
|
|
54
|
+
* - [ ] **T5**: Some description
|
|
55
|
+
* - [ ] **T5** [TAG]: Some description
|
|
56
|
+
*
|
|
57
|
+
* And subsequent annotation lines:
|
|
58
|
+
* - Blocked by: T3, T7
|
|
59
|
+
* - Blocked by: T3
|
|
60
|
+
*/
|
|
61
|
+
function parsePlan(text) {
|
|
62
|
+
const lines = text.split('\n');
|
|
63
|
+
const tasks = [];
|
|
64
|
+
let current = null;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const line = lines[i];
|
|
68
|
+
|
|
69
|
+
// Match pending task header: - [ ] **T{N}**...
|
|
70
|
+
const taskMatch = line.match(/^\s*-\s+\[\s+\]\s+\*\*T(\d+)\*\*(?:\s+\[[^\]]*\])?[:\s]*(.*)/);
|
|
71
|
+
if (taskMatch) {
|
|
72
|
+
current = {
|
|
73
|
+
id: `T${taskMatch[1]}`,
|
|
74
|
+
num: parseInt(taskMatch[1], 10),
|
|
75
|
+
description: taskMatch[2].trim(),
|
|
76
|
+
blockedBy: [],
|
|
77
|
+
};
|
|
78
|
+
tasks.push(current);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Match completed task — reset current so we don't attach annotations to wrong task
|
|
83
|
+
const doneMatch = line.match(/^\s*-\s+\[x\]\s+/i);
|
|
84
|
+
if (doneMatch) {
|
|
85
|
+
current = null;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Match "Blocked by:" annotation under current pending task
|
|
90
|
+
if (current) {
|
|
91
|
+
const blockedMatch = line.match(/^\s+-\s+Blocked\s+by:\s+(.+)/i);
|
|
92
|
+
if (blockedMatch) {
|
|
93
|
+
const deps = blockedMatch[1]
|
|
94
|
+
.split(/[,\s]+/)
|
|
95
|
+
.map(s => s.trim())
|
|
96
|
+
.filter(s => /^T\d+$/.test(s));
|
|
97
|
+
current.blockedBy.push(...deps);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return tasks;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// DAG → waves (Kahn's algorithm)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Topological sort into waves.
|
|
111
|
+
* Tasks with no unmet deps form wave 1; their dependents (once all deps resolved) form wave 2, etc.
|
|
112
|
+
*
|
|
113
|
+
* @param {Array} tasks — full task list (all pending)
|
|
114
|
+
* @param {Set<string>} stuckIds — IDs to treat as "not ready" regardless of deps
|
|
115
|
+
* @returns {Array<Array>} waves — each element is an array of task objects
|
|
116
|
+
*/
|
|
117
|
+
function buildWaves(tasks, stuckIds) {
|
|
118
|
+
// Index by id for quick lookup
|
|
119
|
+
const byId = new Map(tasks.map(t => [t.id, t]));
|
|
120
|
+
|
|
121
|
+
// Only consider deps that actually exist in the pending task list
|
|
122
|
+
// (completed tasks are already satisfied by definition)
|
|
123
|
+
// For stuck tasks: they remain unresolved, blocking dependents
|
|
124
|
+
const pendingIds = new Set(tasks.map(t => t.id));
|
|
125
|
+
|
|
126
|
+
// Compute in-degree considering only pending→pending edges
|
|
127
|
+
// Stuck tasks have their in-degree treated as unresolvable
|
|
128
|
+
const inDeg = new Map();
|
|
129
|
+
const dependents = new Map(); // id → list of tasks that depend on it
|
|
130
|
+
|
|
131
|
+
for (const t of tasks) {
|
|
132
|
+
if (!inDeg.has(t.id)) inDeg.set(t.id, 0);
|
|
133
|
+
if (!dependents.has(t.id)) dependents.set(t.id, []);
|
|
134
|
+
|
|
135
|
+
for (const dep of t.blockedBy) {
|
|
136
|
+
if (pendingIds.has(dep)) {
|
|
137
|
+
// dep is still pending — this is a real blocking edge
|
|
138
|
+
inDeg.set(t.id, (inDeg.get(t.id) || 0) + 1);
|
|
139
|
+
if (!dependents.has(dep)) dependents.set(dep, []);
|
|
140
|
+
dependents.get(dep).push(t.id);
|
|
141
|
+
}
|
|
142
|
+
// If dep is not in pending list (i.e., completed), edge is already satisfied
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Mark stuck tasks: treat as if they can never be resolved
|
|
147
|
+
// Their transitive dependents will not appear in any wave
|
|
148
|
+
const blocked = new Set(stuckIds);
|
|
149
|
+
|
|
150
|
+
// BFS to find all transitive dependents of stuck tasks
|
|
151
|
+
const stuckQueue = [...stuckIds].filter(id => pendingIds.has(id));
|
|
152
|
+
const visited = new Set(stuckQueue);
|
|
153
|
+
let qi = 0;
|
|
154
|
+
while (qi < stuckQueue.length) {
|
|
155
|
+
const sid = stuckQueue[qi++];
|
|
156
|
+
for (const dep of (dependents.get(sid) || [])) {
|
|
157
|
+
if (!visited.has(dep)) {
|
|
158
|
+
visited.add(dep);
|
|
159
|
+
blocked.add(dep);
|
|
160
|
+
stuckQueue.push(dep);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Kahn's BFS for remaining (non-blocked) tasks
|
|
166
|
+
const waves = [];
|
|
167
|
+
// Ready = in-degree 0 and not blocked
|
|
168
|
+
let ready = tasks.filter(t => !blocked.has(t.id) && inDeg.get(t.id) === 0);
|
|
169
|
+
|
|
170
|
+
// Sort deterministically within each wave by task number
|
|
171
|
+
ready.sort((a, b) => a.num - b.num);
|
|
172
|
+
|
|
173
|
+
const resolved = new Set();
|
|
174
|
+
|
|
175
|
+
while (ready.length > 0) {
|
|
176
|
+
waves.push([...ready]);
|
|
177
|
+
const nextReady = [];
|
|
178
|
+
|
|
179
|
+
for (const t of ready) {
|
|
180
|
+
resolved.add(t.id);
|
|
181
|
+
for (const depId of (dependents.get(t.id) || [])) {
|
|
182
|
+
if (blocked.has(depId)) continue;
|
|
183
|
+
const newDeg = (inDeg.get(depId) || 0) - 1;
|
|
184
|
+
inDeg.set(depId, newDeg);
|
|
185
|
+
if (newDeg === 0) {
|
|
186
|
+
nextReady.push(byId.get(depId));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
nextReady.sort((a, b) => a.num - b.num);
|
|
192
|
+
ready = nextReady;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return waves;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Output formatter
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
function formatWaves(waves) {
|
|
203
|
+
if (waves.length === 0) {
|
|
204
|
+
return '(no pending tasks)';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const lines = [];
|
|
208
|
+
for (let i = 0; i < waves.length; i++) {
|
|
209
|
+
const waveNum = i + 1;
|
|
210
|
+
const taskParts = waves[i].map(t => {
|
|
211
|
+
const desc = t.description ? ` — ${t.description}` : '';
|
|
212
|
+
return `${t.id}${desc}`;
|
|
213
|
+
});
|
|
214
|
+
lines.push(`Wave ${waveNum}: ${taskParts.join(', ')}`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Main
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function main() {
|
|
224
|
+
const args = parseArgs(process.argv);
|
|
225
|
+
|
|
226
|
+
// Resolve plan path: relative to cwd
|
|
227
|
+
const planPath = path.resolve(process.cwd(), args.plan);
|
|
228
|
+
|
|
229
|
+
if (!fs.existsSync(planPath)) {
|
|
230
|
+
process.stderr.write(`wave-runner: PLAN.md not found at ${planPath}\n`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let text;
|
|
235
|
+
try {
|
|
236
|
+
text = fs.readFileSync(planPath, 'utf8');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
process.stderr.write(`wave-runner: failed to read ${planPath}: ${err.message}\n`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let tasks;
|
|
243
|
+
try {
|
|
244
|
+
tasks = parsePlan(text);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
process.stderr.write(`wave-runner: parse error: ${err.message}\n`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const stuckIds = new Set(args.recalc ? args.failed : []);
|
|
251
|
+
|
|
252
|
+
const waves = buildWaves(tasks, stuckIds);
|
|
253
|
+
const output = formatWaves(waves);
|
|
254
|
+
|
|
255
|
+
process.stdout.write(output + '\n');
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
main();
|