@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/lib/planner.js CHANGED
@@ -1,6 +1,6 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const path = require('node:path');
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.map((s) => s.path).filter((p) => !order.includes(p));
70
- throw new Error(`Dependency cycle detected involving: ${remaining.join(', ')}`);
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('No sprints configured in orchard.json.');
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(' ' + '='.repeat(50));
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 ? 'ROOT' : `L${level} `;
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 || 'unknown';
146
- const assignee = s.assigned_to || 'unassigned';
147
- const indent = ' '.repeat(level + 1);
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 === 'active' ? '[*]' :
151
- status === 'done' ? '[x]' :
152
- status === 'blocked' ? '[!]' :
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(' ' + ' '.repeat(level * 2) + ' |');
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(' Deadlines:');
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 = days < 0 ? 'OVERDUE' : days <= 3 ? 'URGENT' : `${days}d`;
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
  };