@grainulation/orchard 1.0.1 → 1.0.2
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/CONTRIBUTING.md +6 -0
- package/README.md +21 -20
- package/bin/orchard.js +227 -80
- package/lib/assignments.js +19 -17
- package/lib/conflicts.js +177 -29
- package/lib/dashboard.js +100 -47
- package/lib/decompose.js +268 -0
- package/lib/doctor.js +48 -32
- package/lib/export.js +72 -44
- package/lib/farmer.js +54 -38
- package/lib/hackathon.js +349 -0
- package/lib/planner.js +150 -21
- package/lib/server.js +395 -165
- package/lib/sync.js +31 -25
- package/lib/tracker.js +52 -40
- package/package.json +7 -3
package/lib/planner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const path = require(
|
|
3
|
+
const path = require("node:path");
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Build an adjacency list from sprint dependencies.
|
|
@@ -66,8 +66,12 @@ function topoSort(config) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (order.length !== sprints.length) {
|
|
69
|
-
const remaining = sprints
|
|
70
|
-
|
|
69
|
+
const remaining = sprints
|
|
70
|
+
.map((s) => s.path)
|
|
71
|
+
.filter((p) => !order.includes(p));
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Dependency cycle detected involving: ${remaining.join(", ")}`,
|
|
74
|
+
);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
return order;
|
|
@@ -92,14 +96,14 @@ function printDependencyGraph(config, root) {
|
|
|
92
96
|
const sprints = config.sprints || [];
|
|
93
97
|
|
|
94
98
|
if (sprints.length === 0) {
|
|
95
|
-
console.log(
|
|
99
|
+
console.log("No sprints configured in orchard.json.");
|
|
96
100
|
return;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
console.log(
|
|
103
|
+
console.log("");
|
|
100
104
|
console.log(` Sprint Dependency Graph (${sprints.length} sprints)`);
|
|
101
|
-
console.log(
|
|
102
|
-
console.log(
|
|
105
|
+
console.log(" " + "=".repeat(50));
|
|
106
|
+
console.log("");
|
|
103
107
|
|
|
104
108
|
// Build lookup
|
|
105
109
|
const byPath = new Map();
|
|
@@ -137,20 +141,23 @@ function printDependencyGraph(config, root) {
|
|
|
137
141
|
|
|
138
142
|
for (let level = 0; level <= maxLevel; level++) {
|
|
139
143
|
const items = levels.get(level) || [];
|
|
140
|
-
const prefix = level === 0 ?
|
|
144
|
+
const prefix = level === 0 ? "ROOT" : `L${level} `;
|
|
141
145
|
|
|
142
146
|
for (const p of items) {
|
|
143
147
|
const s = byPath.get(p);
|
|
144
148
|
const name = path.basename(p);
|
|
145
|
-
const status = s.status ||
|
|
146
|
-
const assignee = s.assigned_to ||
|
|
147
|
-
const indent =
|
|
149
|
+
const status = s.status || "unknown";
|
|
150
|
+
const assignee = s.assigned_to || "unassigned";
|
|
151
|
+
const indent = " ".repeat(level + 1);
|
|
148
152
|
|
|
149
153
|
const marker =
|
|
150
|
-
status ===
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
status === "active"
|
|
155
|
+
? "[*]"
|
|
156
|
+
: status === "done"
|
|
157
|
+
? "[x]"
|
|
158
|
+
: status === "blocked"
|
|
159
|
+
? "[!]"
|
|
160
|
+
: "[ ]";
|
|
154
161
|
|
|
155
162
|
console.log(`${indent}${marker} ${name} (${assignee})`);
|
|
156
163
|
|
|
@@ -161,28 +168,149 @@ function printDependencyGraph(config, root) {
|
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
if (level < maxLevel) {
|
|
164
|
-
console.log(
|
|
171
|
+
console.log(" " + " ".repeat(level * 2) + " |");
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
console.log(
|
|
175
|
+
console.log("");
|
|
169
176
|
|
|
170
177
|
// Show deadline summary
|
|
171
178
|
const withDeadlines = sprints.filter((s) => s.deadline);
|
|
172
179
|
if (withDeadlines.length > 0) {
|
|
173
|
-
console.log(
|
|
180
|
+
console.log(" Deadlines:");
|
|
174
181
|
withDeadlines
|
|
175
182
|
.sort((a, b) => a.deadline.localeCompare(b.deadline))
|
|
176
183
|
.forEach((s) => {
|
|
177
184
|
const name = path.basename(s.path);
|
|
178
185
|
const days = daysUntil(s.deadline);
|
|
179
|
-
const urgency =
|
|
186
|
+
const urgency =
|
|
187
|
+
days < 0 ? "OVERDUE" : days <= 3 ? "URGENT" : `${days}d`;
|
|
180
188
|
console.log(` ${name}: ${s.deadline} (${urgency})`);
|
|
181
189
|
});
|
|
182
|
-
console.log(
|
|
190
|
+
console.log("");
|
|
183
191
|
}
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Generate a Mermaid diagram string for the sprint dependency graph.
|
|
196
|
+
* Color-coded nodes: green (done/compiled), blue (active), amber (blocked), red (cycle).
|
|
197
|
+
* Highlights critical path with thick edges.
|
|
198
|
+
*/
|
|
199
|
+
function generateMermaid(config) {
|
|
200
|
+
const sprints = config.sprints || [];
|
|
201
|
+
if (sprints.length === 0) return 'graph TD\n empty["No sprints configured"]';
|
|
202
|
+
|
|
203
|
+
const lines = ["graph TD"];
|
|
204
|
+
|
|
205
|
+
// Style definitions for status colors
|
|
206
|
+
lines.push(" classDef done fill:#22c55e,stroke:#16a34a,color:#fff");
|
|
207
|
+
lines.push(" classDef compiled fill:#22c55e,stroke:#16a34a,color:#fff");
|
|
208
|
+
lines.push(" classDef active fill:#3b82f6,stroke:#2563eb,color:#fff");
|
|
209
|
+
lines.push(" classDef blocked fill:#f59e0b,stroke:#d97706,color:#fff");
|
|
210
|
+
lines.push(" classDef notstarted fill:#6b7280,stroke:#4b5563,color:#fff");
|
|
211
|
+
lines.push(" classDef conflict fill:#ef4444,stroke:#dc2626,color:#fff");
|
|
212
|
+
|
|
213
|
+
const byPath = new Map();
|
|
214
|
+
for (const s of sprints) byPath.set(s.path, s);
|
|
215
|
+
|
|
216
|
+
// Sanitize path into a valid Mermaid node ID
|
|
217
|
+
function nodeId(p) {
|
|
218
|
+
return p.replace(/[^a-zA-Z0-9]/g, "_");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function nodeLabel(s) {
|
|
222
|
+
const name = path.basename(s.path);
|
|
223
|
+
const status = s.status || "unknown";
|
|
224
|
+
const assignee = s.assigned_to ? ` (${s.assigned_to})` : "";
|
|
225
|
+
return `${name}${assignee}\\n${status}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Detect cycles for marking
|
|
229
|
+
const cycleSet = new Set(detectCycles(config));
|
|
230
|
+
|
|
231
|
+
// Compute critical path (longest path lengths)
|
|
232
|
+
let criticalEdges = new Set();
|
|
233
|
+
try {
|
|
234
|
+
const order = topoSort(config);
|
|
235
|
+
const dist = new Map();
|
|
236
|
+
const prev = new Map();
|
|
237
|
+
for (const p of order) dist.set(p, 0);
|
|
238
|
+
|
|
239
|
+
for (const p of order) {
|
|
240
|
+
const s = byPath.get(p);
|
|
241
|
+
for (const dep of s.depends_on || []) {
|
|
242
|
+
if (dist.has(dep) && dist.get(dep) + 1 > dist.get(p)) {
|
|
243
|
+
dist.set(p, dist.get(dep) + 1);
|
|
244
|
+
prev.set(p, dep);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Find the node with max distance — that's the end of critical path
|
|
250
|
+
let maxNode = null;
|
|
251
|
+
let maxDist = 0;
|
|
252
|
+
for (const [p, d] of dist) {
|
|
253
|
+
if (d >= maxDist) {
|
|
254
|
+
maxDist = d;
|
|
255
|
+
maxNode = p;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Walk back to build critical path edges
|
|
260
|
+
let cur = maxNode;
|
|
261
|
+
while (cur && prev.has(cur)) {
|
|
262
|
+
const from = prev.get(cur);
|
|
263
|
+
criticalEdges.add(`${nodeId(from)}-->${nodeId(cur)}`);
|
|
264
|
+
cur = from;
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// cycles — no critical path
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Node declarations
|
|
271
|
+
for (const s of sprints) {
|
|
272
|
+
const id = nodeId(s.path);
|
|
273
|
+
const label = nodeLabel(s);
|
|
274
|
+
lines.push(` ${id}["${label}"]`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Edges
|
|
278
|
+
for (const s of sprints) {
|
|
279
|
+
for (const dep of s.depends_on || []) {
|
|
280
|
+
if (!byPath.has(dep)) continue;
|
|
281
|
+
const edgeKey = `${nodeId(dep)}-->${nodeId(s.path)}`;
|
|
282
|
+
if (criticalEdges.has(edgeKey)) {
|
|
283
|
+
lines.push(` ${nodeId(dep)} ==> ${nodeId(s.path)}`);
|
|
284
|
+
} else {
|
|
285
|
+
lines.push(` ${nodeId(dep)} --> ${nodeId(s.path)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Apply classes
|
|
291
|
+
for (const s of sprints) {
|
|
292
|
+
const id = nodeId(s.path);
|
|
293
|
+
if (cycleSet.length > 0 && cycleSet.has(s.path)) {
|
|
294
|
+
lines.push(` class ${id} conflict`);
|
|
295
|
+
} else {
|
|
296
|
+
const status = s.status || "unknown";
|
|
297
|
+
const cls =
|
|
298
|
+
status === "done"
|
|
299
|
+
? "done"
|
|
300
|
+
: status === "compiled"
|
|
301
|
+
? "compiled"
|
|
302
|
+
: status === "active"
|
|
303
|
+
? "active"
|
|
304
|
+
: status === "blocked"
|
|
305
|
+
? "blocked"
|
|
306
|
+
: "notstarted";
|
|
307
|
+
lines.push(` class ${id} ${cls}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return lines.join("\n");
|
|
312
|
+
}
|
|
313
|
+
|
|
186
314
|
function daysUntil(dateStr) {
|
|
187
315
|
const target = new Date(dateStr);
|
|
188
316
|
const now = new Date();
|
|
@@ -194,5 +322,6 @@ module.exports = {
|
|
|
194
322
|
topoSort,
|
|
195
323
|
detectCycles,
|
|
196
324
|
printDependencyGraph,
|
|
325
|
+
generateMermaid,
|
|
197
326
|
daysUntil,
|
|
198
327
|
};
|