sysprom 1.21.2 → 1.22.1

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.
Files changed (82) hide show
  1. package/dist/schema.json +0 -31
  2. package/dist/src/cli/commands/plan.js +54 -3
  3. package/dist/src/cli/commands/speckit.js +0 -1
  4. package/dist/src/cli/program.js +0 -2
  5. package/dist/src/index.d.ts +2 -2
  6. package/dist/src/index.js +2 -2
  7. package/dist/src/json-to-md.js +0 -8
  8. package/dist/src/mcp/server.js +0 -1
  9. package/dist/src/md-to-json.js +0 -10
  10. package/dist/src/operations/add-node.d.ts +0 -51
  11. package/dist/src/operations/add-relationship.d.ts +0 -34
  12. package/dist/src/operations/check.d.ts +0 -17
  13. package/dist/src/operations/graph-decision.d.ts +0 -17
  14. package/dist/src/operations/graph-dependency.d.ts +0 -17
  15. package/dist/src/operations/graph-refinement.d.ts +0 -17
  16. package/dist/src/operations/graph.d.ts +0 -17
  17. package/dist/src/operations/index.d.ts +3 -5
  18. package/dist/src/operations/index.js +3 -5
  19. package/dist/src/operations/infer-completeness.d.ts +0 -17
  20. package/dist/src/operations/infer-completeness.js +0 -4
  21. package/dist/src/operations/infer-derived.d.ts +0 -17
  22. package/dist/src/operations/infer-impact.d.ts +0 -119
  23. package/dist/src/operations/infer-lifecycle.d.ts +0 -17
  24. package/dist/src/operations/init-document.d.ts +0 -17
  25. package/dist/src/operations/json-to-markdown.d.ts +0 -17
  26. package/dist/src/operations/markdown-to-json.d.ts +0 -17
  27. package/dist/src/operations/next-id.d.ts +0 -17
  28. package/dist/src/operations/node-history.d.ts +0 -17
  29. package/dist/src/operations/plan-add-task.d.ts +0 -34
  30. package/dist/src/operations/{mark-task-done.d.ts → plan-complete-task.d.ts} +4 -41
  31. package/dist/src/operations/plan-complete-task.js +18 -0
  32. package/dist/src/operations/plan-gate.d.ts +0 -17
  33. package/dist/src/operations/plan-init.d.ts +0 -17
  34. package/dist/src/operations/plan-progress.d.ts +18 -17
  35. package/dist/src/operations/plan-progress.js +6 -0
  36. package/dist/src/operations/{mark-task-undone.d.ts → plan-reopen-task.d.ts} +4 -41
  37. package/dist/src/operations/plan-reopen-task.js +18 -0
  38. package/dist/src/operations/{add-plan-task.d.ts → plan-start-task.d.ts} +4 -41
  39. package/dist/src/operations/plan-start-task.js +18 -0
  40. package/dist/src/operations/plan-status.d.ts +22 -17
  41. package/dist/src/operations/plan-status.js +8 -0
  42. package/dist/src/operations/query-node.d.ts +0 -51
  43. package/dist/src/operations/query-nodes.d.ts +0 -34
  44. package/dist/src/operations/query-relationships.d.ts +0 -17
  45. package/dist/src/operations/remove-node.d.ts +0 -51
  46. package/dist/src/operations/remove-relationship.d.ts +0 -34
  47. package/dist/src/operations/rename.d.ts +0 -34
  48. package/dist/src/operations/search.d.ts +0 -34
  49. package/dist/src/operations/speckit-diff.d.ts +0 -17
  50. package/dist/src/operations/speckit-export.d.ts +0 -17
  51. package/dist/src/operations/speckit-import.d.ts +0 -17
  52. package/dist/src/operations/speckit-sync.d.ts +0 -51
  53. package/dist/src/operations/speckit-sync.js +0 -1
  54. package/dist/src/operations/state-at.d.ts +0 -17
  55. package/dist/src/operations/stats.d.ts +0 -17
  56. package/dist/src/operations/sync.d.ts +0 -51
  57. package/dist/src/operations/timeline.d.ts +0 -17
  58. package/dist/src/operations/trace-from-node.d.ts +0 -51
  59. package/dist/src/operations/update-metadata.d.ts +0 -34
  60. package/dist/src/operations/update-node.d.ts +0 -58
  61. package/dist/src/operations/validate.d.ts +0 -17
  62. package/dist/src/operations/validate.js +71 -0
  63. package/dist/src/schema.d.ts +0 -61
  64. package/dist/src/schema.js +0 -11
  65. package/dist/src/speckit/generate.js +17 -20
  66. package/dist/src/speckit/index.d.ts +1 -1
  67. package/dist/src/speckit/index.js +1 -1
  68. package/dist/src/speckit/parse.d.ts +1 -1
  69. package/dist/src/speckit/parse.js +20 -14
  70. package/dist/src/speckit/plan.d.ts +29 -7
  71. package/dist/src/speckit/plan.js +181 -47
  72. package/package.json +1 -1
  73. package/schema.json +0 -31
  74. package/dist/src/cli/commands/task.d.ts +0 -2
  75. package/dist/src/cli/commands/task.js +0 -157
  76. package/dist/src/operations/add-plan-task.js +0 -29
  77. package/dist/src/operations/mark-task-done.js +0 -30
  78. package/dist/src/operations/mark-task-undone.js +0 -30
  79. package/dist/src/operations/task-list.d.ts +0 -288
  80. package/dist/src/operations/task-list.js +0 -49
  81. package/dist/src/operations/update-plan-task.d.ts +0 -555
  82. package/dist/src/operations/update-plan-task.js +0 -35
@@ -1,4 +1,5 @@
1
1
  import { textToString } from "../text.js";
2
+ import { hasLifecycleState } from "../lifecycle-state.js";
2
3
  // ============================================================================
3
4
  // Helper functions
4
5
  // ============================================================================
@@ -279,7 +280,7 @@ export function addTask(doc, prefix, name, parentId) {
279
280
  id: changeId,
280
281
  type: "change",
281
282
  name: taskName,
282
- plan: [],
283
+ lifecycle: { proposed: true },
283
284
  };
284
285
  // Build new relationships
285
286
  const newRels = [];
@@ -353,7 +354,7 @@ function addTaskToParent(doc, protImpl, prefix, parentId, name) {
353
354
  id: changeId,
354
355
  type: "change",
355
356
  name: childName,
356
- plan: [],
357
+ lifecycle: { proposed: true },
357
358
  };
358
359
  // Build new relationships for child
359
360
  const newRels = [];
@@ -449,41 +450,144 @@ function addTaskToParent(doc, protImpl, prefix, parentId, name) {
449
450
  nodes: updatedNodes,
450
451
  };
451
452
  }
453
+ /**
454
+ * Set lifecycle state on a task (change node) within a plan implementation protocol.
455
+ *
456
+ * - start: marks introduced + in_progress true
457
+ * - complete: marks complete true and clears in_progress
458
+ * - reopen: clears complete and marks in_progress true
459
+ */
460
+ export function setTaskLifecycle(doc, prefix, taskId, action) {
461
+ const protImpl = findNode(doc, `${prefix}-PROT-IMPL`);
462
+ if (!protImpl?.subsystem) {
463
+ throw new Error(`Node ${prefix}-PROT-IMPL not found`);
464
+ }
465
+ function updateInSubsystem(subsystem) {
466
+ let found = false;
467
+ const updatedNodes = subsystem.nodes.map((node) => {
468
+ if (node.id === taskId) {
469
+ if (node.type !== "change") {
470
+ throw new Error(`Task node ${taskId} is not a change node`);
471
+ }
472
+ found = true;
473
+ const lifecycle = { ...(node.lifecycle ?? {}) };
474
+ if (action === "start") {
475
+ lifecycle.introduced = lifecycle.introduced ?? true;
476
+ lifecycle.in_progress = true;
477
+ }
478
+ else if (action === "complete") {
479
+ lifecycle.complete = true;
480
+ delete lifecycle.in_progress;
481
+ }
482
+ else {
483
+ delete lifecycle.complete;
484
+ lifecycle.in_progress = true;
485
+ }
486
+ return {
487
+ ...node,
488
+ lifecycle,
489
+ };
490
+ }
491
+ if (!node.subsystem) {
492
+ return node;
493
+ }
494
+ const child = updateInSubsystem(node.subsystem);
495
+ if (child.found) {
496
+ found = true;
497
+ return {
498
+ ...node,
499
+ subsystem: child.updated,
500
+ };
501
+ }
502
+ return node;
503
+ });
504
+ return {
505
+ found,
506
+ updated: {
507
+ ...(subsystem.metadata ? { metadata: subsystem.metadata } : {}),
508
+ nodes: updatedNodes,
509
+ ...(subsystem.relationships
510
+ ? { relationships: subsystem.relationships }
511
+ : {}),
512
+ ...(subsystem.external_references
513
+ ? { external_references: subsystem.external_references }
514
+ : {}),
515
+ },
516
+ };
517
+ }
518
+ const updated = updateInSubsystem(protImpl.subsystem);
519
+ if (!updated.found) {
520
+ throw new Error(`Task ${taskId} not found in ${prefix}-PROT-IMPL`);
521
+ }
522
+ const updatedNodes = doc.nodes.map((node) => node.id === protImpl.id
523
+ ? { ...protImpl, subsystem: updated.updated }
524
+ : node);
525
+ return {
526
+ ...doc,
527
+ nodes: updatedNodes,
528
+ };
529
+ }
452
530
  // ============================================================================
453
531
  // isTaskDone
454
532
  // ============================================================================
455
533
  /**
456
- * Check if a change node's task is complete.
457
- *
458
- * If no subsystem or no change children in subsystem:
459
- * - All items in node.plan must have done === true AND at least one item must exist.
460
- * If subsystem has change children:
461
- * - All children must be recursively done AND own plan items (if any) must be done.
534
+ * Check if a task change node is complete.
462
535
  * @param node - The change node to evaluate.
463
- * @returns Whether all tasks in the node's plan are complete.
536
+ * @returns Whether the task lifecycle includes complete.
464
537
  * @example
465
538
  * ```ts
466
- * isTaskDone(changeNode); // => true if all plan items done
539
+ * isTaskDone(changeNode); // => true when lifecycle.complete is reached
467
540
  * ```
468
541
  */
469
542
  export function isTaskDone(node) {
470
- // If the node has a subsystem with change children, check those recursively
471
- if (node.subsystem) {
472
- const changeChildren = node.subsystem.nodes.filter((n) => n.type === "change");
473
- if (changeChildren.length > 0) {
474
- // All change children must be done, and own plan items must be done
475
- const allChildrenDone = changeChildren.every((child) => isTaskDone(child));
476
- const ownPlanDone = (node.plan ?? []).length === 0 ||
477
- (node.plan ?? []).every((item) => item.done === true);
478
- return allChildrenDone && ownPlanDone;
543
+ return hasLifecycleState(node, "complete");
544
+ }
545
+ function isGateReady(node) {
546
+ if (node.type !== "gate")
547
+ return false;
548
+ const lifecycle = node.lifecycle ?? {};
549
+ const values = Object.values(lifecycle);
550
+ if (values.length === 0)
551
+ return false;
552
+ return values.every((value) => value === true || typeof value === "string");
553
+ }
554
+ function collectNodes(subsystem) {
555
+ if (!subsystem)
556
+ return [];
557
+ const nodes = [];
558
+ for (const node of subsystem.nodes) {
559
+ nodes.push(node);
560
+ if (node.subsystem) {
561
+ nodes.push(...collectNodes(node.subsystem));
479
562
  }
480
563
  }
481
- // No subsystem or no change children: check own plan
482
- const planItems = node.plan ?? [];
483
- if (planItems.length === 0) {
484
- return false;
564
+ return nodes;
565
+ }
566
+ function taskBlockageReasons(doc, subsystem, taskNode) {
567
+ if (taskNode.type !== "change")
568
+ return [];
569
+ const relationships = subsystem?.relationships ?? [];
570
+ const scopedNodeMap = new Map((subsystem?.nodes ?? []).map((n) => [n.id, n]));
571
+ const globalNodeMap = new Map(doc.nodes.map((n) => [n.id, n]));
572
+ const reasons = [];
573
+ for (const rel of relationships) {
574
+ if (rel.from !== taskNode.id || rel.type !== "depends_on")
575
+ continue;
576
+ const dependency = scopedNodeMap.get(rel.to) ?? globalNodeMap.get(rel.to);
577
+ const dependencyComplete = dependency?.type === "change" && isTaskDone(dependency);
578
+ if (!dependencyComplete) {
579
+ reasons.push({ kind: "dependency_unmet", nodeId: rel.to });
580
+ }
485
581
  }
486
- return planItems.every((item) => item.done === true);
582
+ for (const rel of relationships) {
583
+ if (rel.from !== taskNode.id || rel.type !== "constrained_by")
584
+ continue;
585
+ const gate = scopedNodeMap.get(rel.to) ?? globalNodeMap.get(rel.to);
586
+ if (!gate || !isGateReady(gate)) {
587
+ reasons.push({ kind: "gate_not_ready", nodeId: rel.to });
588
+ }
589
+ }
590
+ return reasons;
487
591
  }
488
592
  // ============================================================================
489
593
  // countTasks
@@ -501,22 +605,49 @@ export function isTaskDone(node) {
501
605
  * ```
502
606
  */
503
607
  export function countTasks(node) {
608
+ if (node.type !== "change") {
609
+ return { total: 0, done: 0, blocked: 0, blockedTasks: [] };
610
+ }
611
+ const localDoc = {
612
+ nodes: [node, ...collectNodes(node.subsystem)],
613
+ relationships: [],
614
+ };
615
+ return countTasksInSubsystem(localDoc, {
616
+ nodes: [node],
617
+ relationships: [],
618
+ });
619
+ }
620
+ function countTasksInSubsystem(doc, subsystem) {
504
621
  let total = 0;
505
622
  let done = 0;
506
- // Count own plan items
507
- const ownPlan = node.plan ?? [];
508
- total += ownPlan.length;
509
- done += ownPlan.filter((item) => item.done === true).length;
510
- // Recursively count from change children in subsystem
511
- if (node.subsystem) {
512
- const changeChildren = node.subsystem.nodes.filter((n) => n.type === "change");
513
- for (const child of changeChildren) {
514
- const childCount = countTasks(child);
515
- total += childCount.total;
516
- done += childCount.done;
623
+ let blocked = 0;
624
+ const blockedTasks = [];
625
+ if (!subsystem) {
626
+ return { total, done, blocked, blockedTasks };
627
+ }
628
+ for (const node of subsystem.nodes) {
629
+ if (node.type !== "change")
630
+ continue;
631
+ const childChanges = (node.subsystem?.nodes ?? []).filter((child) => child.type === "change");
632
+ if (childChanges.length > 0) {
633
+ const childCounts = countTasksInSubsystem(doc, node.subsystem);
634
+ total += childCounts.total;
635
+ done += childCounts.done;
636
+ blocked += childCounts.blocked;
637
+ blockedTasks.push(...childCounts.blockedTasks);
638
+ continue;
639
+ }
640
+ total += 1;
641
+ if (isTaskDone(node)) {
642
+ done += 1;
643
+ }
644
+ const reasons = taskBlockageReasons(doc, subsystem, node);
645
+ if (reasons.length > 0) {
646
+ blocked += 1;
647
+ blockedTasks.push({ taskId: node.id, reasons });
517
648
  }
518
649
  }
519
- return { total, done };
650
+ return { total, done, blocked, blockedTasks };
520
651
  }
521
652
  // ============================================================================
522
653
  // planStatus
@@ -543,15 +674,10 @@ export function planStatus(doc, prefix) {
543
674
  .map((us) => us.id);
544
675
  // Count phases (top-level change nodes)
545
676
  const phaseCount = (protImpl?.subsystem?.nodes ?? []).filter((n) => n.type === "change").length;
546
- // Count tasks using the helper
547
- let totalTasks = 0;
548
- let doneTasks = 0;
549
- const changeNodes = (protImpl?.subsystem?.nodes ?? []).filter((n) => n.type === "change");
550
- for (const change of changeNodes) {
551
- const taskCount = countTasks(change);
552
- totalTasks += taskCount.total;
553
- doneTasks += taskCount.done;
554
- }
677
+ const taskCounts = countTasksInSubsystem(doc, protImpl?.subsystem);
678
+ const totalTasks = taskCounts.total;
679
+ const doneTasks = taskCounts.done;
680
+ const blockedTasks = taskCounts.blocked;
555
681
  // Checklist stats
556
682
  const checklistLifecycle = checklist?.lifecycle ?? {};
557
683
  const checklistItemCount = Object.keys(checklistLifecycle).length;
@@ -614,6 +740,8 @@ export function planStatus(doc, prefix) {
614
740
  tasks: {
615
741
  total: totalTasks,
616
742
  done: doneTasks,
743
+ blocked: blockedTasks,
744
+ blockedTasks: taskCounts.blockedTasks,
617
745
  },
618
746
  checklist: {
619
747
  defined: checklist !== null,
@@ -649,17 +777,23 @@ export function planProgress(doc, prefix) {
649
777
  for (let i = 0; i < sortedTasks.length; i++) {
650
778
  const task = sortedTasks[i];
651
779
  const taskNum = i + 1;
652
- // Count tasks for this change node
653
- const taskCount = countTasks(task);
780
+ const taskCount = countTasksInSubsystem(doc, {
781
+ nodes: [task],
782
+ relationships: task.subsystem?.relationships ?? [],
783
+ });
784
+ const reasons = taskBlockageReasons(doc, subsystem, task);
654
785
  const percent = taskCount.total === 0
655
786
  ? 0
656
787
  : Math.round((taskCount.done / taskCount.total) * 100);
657
788
  result.push({
658
789
  phase: taskNum,
790
+ id: task.id,
659
791
  name: task.name,
660
792
  done: taskCount.done,
661
793
  total: taskCount.total,
662
794
  percent,
795
+ blocked: reasons.length > 0,
796
+ blockageReasons: reasons,
663
797
  });
664
798
  }
665
799
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sysprom",
3
- "version": "1.21.2",
3
+ "version": "1.22.1",
4
4
  "description": "SysProM — System Provenance Model CLI and library",
5
5
  "author": "ExaDev",
6
6
  "homepage": "https://exadev.github.io/SysProM",
package/schema.json CHANGED
@@ -302,37 +302,6 @@
302
302
  },
303
303
  "type": "array"
304
304
  },
305
- "plan": {
306
- "description": "Execution plan as a sequence of tasks. Applicable to change nodes.",
307
- "items": {
308
- "additionalProperties": {},
309
- "description": "A single task within a change's execution plan.",
310
- "properties": {
311
- "description": {
312
- "anyOf": [
313
- {
314
- "type": "string"
315
- },
316
- {
317
- "items": {
318
- "type": "string"
319
- },
320
- "type": "array"
321
- }
322
- ]
323
- },
324
- "done": {
325
- "default": false,
326
- "type": "boolean"
327
- }
328
- },
329
- "required": [
330
- "description"
331
- ],
332
- "type": "object"
333
- },
334
- "type": "array"
335
- },
336
305
  "propagation": {
337
306
  "additionalProperties": {
338
307
  "type": "boolean"
@@ -1,2 +0,0 @@
1
- import type { CommandDef } from "../define-command.js";
2
- export declare const taskCommand: CommandDef;
@@ -1,157 +0,0 @@
1
- import * as z from "zod";
2
- import { loadDoc, mutationOpts, persistDoc } from "../shared.js";
3
- import { addPlanTaskOp, markTaskDoneOp, markTaskUndoneOp, taskListOp, } from "../../operations/index.js";
4
- // ============================================================================
5
- // Subcommands
6
- // ============================================================================
7
- const listOpts = mutationOpts.pick({ path: true, json: true }).extend({
8
- change: z.string().optional().describe("Filter by change ID"),
9
- pending: z.boolean().optional().describe("Show only pending tasks"),
10
- });
11
- const listSubcommand = {
12
- name: "list",
13
- description: taskListOp.def.description,
14
- apiLink: taskListOp.def.name,
15
- opts: listOpts,
16
- action(_args, opts) {
17
- const { doc } = loadDoc(opts.path);
18
- try {
19
- const rows = taskListOp({
20
- doc,
21
- changeId: opts.change,
22
- pendingOnly: opts.pending,
23
- });
24
- if (opts.json) {
25
- console.log(JSON.stringify(rows, null, 2));
26
- return;
27
- }
28
- const header = `${"Change".padEnd(12)} ${"#".padEnd(4)} ${"Done".padEnd(6)} Description`;
29
- const divider = `${"-".repeat(12)} ${"-".repeat(4)} ${"-".repeat(6)} ${"-".repeat(30)}`;
30
- console.log(header);
31
- console.log(divider);
32
- for (const row of rows) {
33
- const doneStr = row.done ? "[x]" : "[ ]";
34
- console.log(`${row.changeId.padEnd(12)} ${String(row.index).padEnd(4)} ${doneStr.padEnd(6)} ${row.description}`);
35
- }
36
- console.log(`\n${String(rows.length)} task(s)`);
37
- }
38
- catch (err) {
39
- console.error(err instanceof Error ? err.message : String(err));
40
- process.exit(1);
41
- }
42
- },
43
- };
44
- const addArgs = z.object({
45
- changeId: z.string().describe("Change node ID"),
46
- description: z.string().describe("Task description"),
47
- });
48
- const addOpts = mutationOpts.pick({ path: true });
49
- const addSubcommand = {
50
- name: "add",
51
- description: addPlanTaskOp.def.description,
52
- apiLink: addPlanTaskOp.def.name,
53
- args: addArgs,
54
- opts: addOpts,
55
- action(args, opts) {
56
- const loaded = loadDoc(opts.path);
57
- const { doc } = loaded;
58
- try {
59
- const newDoc = addPlanTaskOp({
60
- doc,
61
- changeId: args.changeId,
62
- description: args.description,
63
- });
64
- persistDoc(newDoc, loaded, { ...opts, json: false, dryRun: false });
65
- const node = newDoc.nodes.find((n) => n.id === args.changeId);
66
- if (!node)
67
- throw new Error(`Node ${args.changeId} not found`);
68
- const newIndex = (node.plan?.length ?? 1) - 1;
69
- console.log(`Added task ${String(newIndex)} to ${args.changeId}`);
70
- }
71
- catch (err) {
72
- console.error(err instanceof Error ? err.message : String(err));
73
- process.exit(1);
74
- }
75
- },
76
- };
77
- const doneArgs = z.object({
78
- changeId: z.string().describe("Change node ID"),
79
- taskIndex: z.string().describe("Task index"),
80
- });
81
- const doneOpts = mutationOpts.pick({ path: true });
82
- const doneSubcommand = {
83
- name: "done",
84
- description: markTaskDoneOp.def.description,
85
- apiLink: markTaskDoneOp.def.name,
86
- args: doneArgs,
87
- opts: doneOpts,
88
- action(args, opts) {
89
- const loaded = loadDoc(opts.path);
90
- const { doc } = loaded;
91
- const taskIndex = parseInt(args.taskIndex, 10);
92
- if (isNaN(taskIndex) || taskIndex < 0) {
93
- console.error(`Invalid task index: ${args.taskIndex}`);
94
- process.exit(1);
95
- }
96
- try {
97
- const newDoc = markTaskDoneOp({
98
- doc,
99
- changeId: args.changeId,
100
- taskIndex,
101
- });
102
- persistDoc(newDoc, loaded, { ...opts, json: false, dryRun: false });
103
- console.log(`Marked task ${String(taskIndex)} done on ${args.changeId}`);
104
- }
105
- catch (err) {
106
- console.error(err instanceof Error ? err.message : String(err));
107
- process.exit(1);
108
- }
109
- },
110
- };
111
- const undoneArgs = z.object({
112
- changeId: z.string().describe("Change node ID"),
113
- taskIndex: z.string().describe("Task index"),
114
- });
115
- const undoneOpts = mutationOpts.pick({ path: true });
116
- const undoneSubcommand = {
117
- name: "undone",
118
- description: markTaskUndoneOp.def.description,
119
- apiLink: markTaskUndoneOp.def.name,
120
- args: undoneArgs,
121
- opts: undoneOpts,
122
- action(args, opts) {
123
- const loaded = loadDoc(opts.path);
124
- const { doc } = loaded;
125
- const taskIndex = parseInt(args.taskIndex, 10);
126
- if (isNaN(taskIndex) || taskIndex < 0) {
127
- console.error(`Invalid task index: ${args.taskIndex}`);
128
- process.exit(1);
129
- }
130
- try {
131
- const newDoc = markTaskUndoneOp({
132
- doc,
133
- changeId: args.changeId,
134
- taskIndex,
135
- });
136
- persistDoc(newDoc, loaded, { ...opts, json: false, dryRun: false });
137
- console.log(`Marked task ${String(taskIndex)} undone on ${args.changeId}`);
138
- }
139
- catch (err) {
140
- console.error(err instanceof Error ? err.message : String(err));
141
- process.exit(1);
142
- }
143
- },
144
- };
145
- // ============================================================================
146
- // Main command
147
- // ============================================================================
148
- export const taskCommand = {
149
- name: "task",
150
- description: "Manage tasks within change nodes",
151
- subcommands: [
152
- listSubcommand,
153
- addSubcommand,
154
- doneSubcommand,
155
- undoneSubcommand,
156
- ],
157
- };
@@ -1,29 +0,0 @@
1
- import * as z from "zod";
2
- import { defineOperation } from "./define-operation.js";
3
- import { SysProMDocument } from "../schema.js";
4
- /**
5
- * Append a new task to a change node's plan array. Returns a new document with the task added.
6
- * @throws {Error} If the change node is not found.
7
- */
8
- export const addPlanTaskOp = defineOperation({
9
- name: "addPlanTask",
10
- description: "Append a new task to a change node's plan array. Returns a new document.",
11
- input: z.object({
12
- doc: SysProMDocument,
13
- changeId: z.string(),
14
- description: z.string(),
15
- }),
16
- output: SysProMDocument,
17
- fn({ doc, changeId, description }) {
18
- const node = doc.nodes.find((n) => n.id === changeId);
19
- if (!node) {
20
- throw new Error(`Node not found: ${changeId}`);
21
- }
22
- const newTask = { description, done: false };
23
- const updatedNode = { ...node, plan: [...(node.plan ?? []), newTask] };
24
- const newNodes = [...doc.nodes];
25
- const idx = newNodes.findIndex((n) => n.id === changeId);
26
- newNodes[idx] = updatedNode;
27
- return { ...doc, nodes: newNodes };
28
- },
29
- });
@@ -1,30 +0,0 @@
1
- import * as z from "zod";
2
- import { defineOperation } from "./define-operation.js";
3
- import { SysProMDocument } from "../schema.js";
4
- /**
5
- * Mark a task as done within a change node's plan.
6
- * @throws {Error} If the change node is not found or the task index is out of range.
7
- */
8
- export const markTaskDoneOp = defineOperation({
9
- name: "markTaskDone",
10
- description: "Mark a task as done",
11
- input: z.object({
12
- doc: SysProMDocument,
13
- changeId: z.string().describe("ID of the change node"),
14
- taskIndex: z.number().describe("Zero-based index of the task in the plan"),
15
- }),
16
- output: SysProMDocument,
17
- fn: (input) => {
18
- const node = input.doc.nodes.find((n) => n.id === input.changeId);
19
- if (!node)
20
- throw new Error(`Node not found: ${input.changeId}`);
21
- const plan = node.plan ?? [];
22
- if (input.taskIndex < 0 || input.taskIndex >= plan.length) {
23
- throw new Error(`Task index ${String(input.taskIndex)} out of range (plan has ${String(plan.length)} task(s))`);
24
- }
25
- const newPlan = [...plan];
26
- newPlan[input.taskIndex] = { ...newPlan[input.taskIndex], done: true };
27
- const newNodes = input.doc.nodes.map((n) => n.id === input.changeId ? { ...n, plan: newPlan } : n);
28
- return { ...input.doc, nodes: newNodes };
29
- },
30
- });
@@ -1,30 +0,0 @@
1
- import * as z from "zod";
2
- import { defineOperation } from "./define-operation.js";
3
- import { SysProMDocument } from "../schema.js";
4
- /**
5
- * Mark a task as undone (incomplete) within a change node's plan.
6
- * @throws {Error} If the change node is not found or the task index is out of range.
7
- */
8
- export const markTaskUndoneOp = defineOperation({
9
- name: "markTaskUndone",
10
- description: "Mark a task as undone",
11
- input: z.object({
12
- doc: SysProMDocument,
13
- changeId: z.string().describe("ID of the change node"),
14
- taskIndex: z.number().describe("Zero-based index of the task in the plan"),
15
- }),
16
- output: SysProMDocument,
17
- fn: (input) => {
18
- const node = input.doc.nodes.find((n) => n.id === input.changeId);
19
- if (!node)
20
- throw new Error(`Node not found: ${input.changeId}`);
21
- const plan = node.plan ?? [];
22
- if (input.taskIndex < 0 || input.taskIndex >= plan.length) {
23
- throw new Error(`Task index ${String(input.taskIndex)} out of range (plan has ${String(plan.length)} task(s))`);
24
- }
25
- const newPlan = [...plan];
26
- newPlan[input.taskIndex] = { ...newPlan[input.taskIndex], done: false };
27
- const newNodes = input.doc.nodes.map((n) => n.id === input.changeId ? { ...n, plan: newPlan } : n);
28
- return { ...input.doc, nodes: newNodes };
29
- },
30
- });