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.
@@ -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();