sysprom 1.16.1 → 1.18.0

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 (84) hide show
  1. package/README.md +147 -75
  2. package/dist/schema.json +2 -1
  3. package/dist/src/cli/commands/add.js +52 -36
  4. package/dist/src/cli/commands/graph.d.ts +15 -0
  5. package/dist/src/cli/commands/graph.js +51 -2
  6. package/dist/src/cli/commands/infer.js +44 -25
  7. package/dist/src/cli/commands/init.d.ts +1 -1
  8. package/dist/src/cli/commands/json2md.d.ts +30 -1
  9. package/dist/src/cli/commands/json2md.js +42 -1
  10. package/dist/src/cli/commands/md2json.d.ts +1 -1
  11. package/dist/src/cli/commands/query.js +35 -37
  12. package/dist/src/cli/commands/speckit.js +81 -77
  13. package/dist/src/cli/commands/stats.js +4 -4
  14. package/dist/src/cli/commands/sync.d.ts +1 -1
  15. package/dist/src/cli/commands/update.js +33 -20
  16. package/dist/src/cli/define-command.d.ts +1 -1
  17. package/dist/src/cli/define-command.js +176 -156
  18. package/dist/src/endpoint-types.js +13 -6
  19. package/dist/src/io.js +59 -8
  20. package/dist/src/json-to-md.d.ts +32 -2
  21. package/dist/src/json-to-md.js +145 -5
  22. package/dist/src/mcp/server.js +269 -112
  23. package/dist/src/md-to-json.js +7 -0
  24. package/dist/src/operations/add-node.d.ts +12 -9
  25. package/dist/src/operations/add-plan-task.d.ts +8 -6
  26. package/dist/src/operations/add-relationship.d.ts +11 -8
  27. package/dist/src/operations/check.d.ts +4 -3
  28. package/dist/src/operations/define-operation.d.ts +1 -1
  29. package/dist/src/operations/graph-decision.d.ts +329 -0
  30. package/dist/src/operations/graph-decision.js +96 -0
  31. package/dist/src/operations/graph-dependency.d.ts +329 -0
  32. package/dist/src/operations/graph-dependency.js +121 -0
  33. package/dist/src/operations/graph-refinement.d.ts +329 -0
  34. package/dist/src/operations/graph-refinement.js +97 -0
  35. package/dist/src/operations/graph-shared.d.ts +116 -0
  36. package/dist/src/operations/graph-shared.js +257 -0
  37. package/dist/src/operations/graph.d.ts +20 -4
  38. package/dist/src/operations/graph.js +129 -36
  39. package/dist/src/operations/index.d.ts +3 -0
  40. package/dist/src/operations/index.js +3 -0
  41. package/dist/src/operations/infer-completeness.d.ts +4 -3
  42. package/dist/src/operations/infer-derived.d.ts +4 -3
  43. package/dist/src/operations/infer-impact.d.ts +28 -21
  44. package/dist/src/operations/infer-lifecycle.d.ts +4 -3
  45. package/dist/src/operations/init-document.d.ts +4 -3
  46. package/dist/src/operations/json-to-markdown.d.ts +28 -3
  47. package/dist/src/operations/json-to-markdown.js +11 -1
  48. package/dist/src/operations/mark-task-done.d.ts +8 -6
  49. package/dist/src/operations/mark-task-undone.d.ts +8 -6
  50. package/dist/src/operations/markdown-to-json.d.ts +4 -3
  51. package/dist/src/operations/next-id.d.ts +4 -3
  52. package/dist/src/operations/node-history.d.ts +4 -3
  53. package/dist/src/operations/plan-add-task.d.ts +8 -6
  54. package/dist/src/operations/plan-gate.d.ts +4 -3
  55. package/dist/src/operations/plan-init.d.ts +4 -3
  56. package/dist/src/operations/plan-progress.d.ts +4 -3
  57. package/dist/src/operations/plan-status.d.ts +4 -3
  58. package/dist/src/operations/query-node.d.ts +24 -17
  59. package/dist/src/operations/query-nodes.d.ts +8 -6
  60. package/dist/src/operations/query-relationships.d.ts +7 -5
  61. package/dist/src/operations/remove-node.d.ts +12 -9
  62. package/dist/src/operations/remove-relationship.d.ts +10 -7
  63. package/dist/src/operations/rename.d.ts +8 -6
  64. package/dist/src/operations/search.d.ts +8 -6
  65. package/dist/src/operations/speckit-diff.d.ts +4 -3
  66. package/dist/src/operations/speckit-export.d.ts +4 -3
  67. package/dist/src/operations/speckit-import.d.ts +4 -3
  68. package/dist/src/operations/speckit-sync.d.ts +12 -9
  69. package/dist/src/operations/state-at.d.ts +4 -3
  70. package/dist/src/operations/stats.d.ts +4 -3
  71. package/dist/src/operations/sync.d.ts +12 -9
  72. package/dist/src/operations/task-list.d.ts +4 -3
  73. package/dist/src/operations/timeline.d.ts +4 -3
  74. package/dist/src/operations/trace-from-node.d.ts +12 -9
  75. package/dist/src/operations/update-metadata.d.ts +8 -6
  76. package/dist/src/operations/update-node.d.ts +11 -8
  77. package/dist/src/operations/update-plan-task.d.ts +8 -6
  78. package/dist/src/operations/validate.d.ts +4 -3
  79. package/dist/src/schema.d.ts +15 -10
  80. package/dist/src/schema.js +3 -11
  81. package/dist/src/utils/define-schema.d.ts +17 -0
  82. package/dist/src/utils/define-schema.js +21 -0
  83. package/package.json +98 -93
  84. package/schema.json +2 -1
@@ -5,9 +5,17 @@ import { inferCompletenessOp, inferLifecycleOp, inferImpactOp, inferDerivedOp, }
5
5
  // ---------------------------------------------------------------------------
6
6
  // Presentation helpers
7
7
  // ---------------------------------------------------------------------------
8
+ function getScoreColour(score) {
9
+ if (score === 1)
10
+ return pc.green;
11
+ if (score >= 0.5)
12
+ return pc.yellow;
13
+ return pc.red;
14
+ }
8
15
  function printCompletenessNode(r) {
9
- const scoreColour = r.score === 1 ? pc.green : r.score >= 0.5 ? pc.yellow : pc.red;
10
- console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)} ${scoreColour(`[${(r.score * 100).toFixed(0)}%]`)}`);
16
+ const scoreColour = getScoreColour(r.score);
17
+ const scoreText = `[${(r.score * 100).toFixed(0)}%]`;
18
+ console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)} ${scoreColour(scoreText)}`);
11
19
  for (const issue of r.issues) {
12
20
  console.log(` ${pc.dim("•")} ${pc.red(issue)}`);
13
21
  }
@@ -21,7 +29,8 @@ function printLifecycleNode(r) {
21
29
  unknown: pc.dim,
22
30
  };
23
31
  const colour = phaseColours[r.inferredPhase] ?? pc.dim;
24
- console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)} ${colour(`[${r.inferredPhase}]`)} ${pc.dim(r.inferredState)}`);
32
+ const phaseLabel = colour(`[${r.inferredPhase}]`);
33
+ console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)} ${phaseLabel} ${pc.dim(r.inferredState)}`);
25
34
  }
26
35
  function printImpactNode(r) {
27
36
  const typeColours = {
@@ -32,7 +41,9 @@ function printImpactNode(r) {
32
41
  const colour = typeColours[r.impactType] ?? pc.dim;
33
42
  const nodeName = r.node ? r.node.name : "(unknown)";
34
43
  const indent = " ".repeat(r.distance);
35
- console.log(`${indent}${pc.cyan(r.id)} ${pc.dim(`(${String(r.distance)})`)} ${colour(`[${r.impactType}]`)} ${pc.bold(nodeName)}`);
44
+ const distanceLabel = pc.dim(`(${String(r.distance)})`);
45
+ const typeLabel = colour(`[${r.impactType}]`);
46
+ console.log(`${indent}${pc.cyan(r.id)} ${distanceLabel} ${typeLabel} ${pc.bold(nodeName)}`);
36
47
  }
37
48
  function printDerivedRelationship(r) {
38
49
  const typeColours = {
@@ -41,7 +52,8 @@ function printDerivedRelationship(r) {
41
52
  inverse: pc.green,
42
53
  };
43
54
  const colour = typeColours[r.derivationType] ?? pc.dim;
44
- console.log(`${pc.cyan(r.from.padEnd(12))} ${colour(r.type.padEnd(20))} ${pc.cyan(r.to)} ${pc.dim(`[${r.derivationType}]`)}`);
55
+ const derivationLabel = pc.dim(`[${r.derivationType}]`);
56
+ console.log(`${pc.cyan(r.from.padEnd(12))} ${colour(r.type.padEnd(20))} ${pc.cyan(r.to)} ${derivationLabel}`);
45
57
  }
46
58
  // ---------------------------------------------------------------------------
47
59
  // Arg/opt schemas
@@ -79,12 +91,13 @@ const completenessSubcommand = {
79
91
  console.log(JSON.stringify(result, null, 2));
80
92
  }
81
93
  else {
82
- console.log(pc.bold("\nCompleteness Analysis\n") +
83
- pc.dim(`Average score: ${(result.averageScore * 100).toFixed(1)}% | `) +
84
- pc.green(`${String(result.completeNodes)} complete`) +
85
- pc.dim(" | ") +
86
- pc.red(`${String(result.incompleteNodes)} incomplete`) +
87
- "\n");
94
+ const avgScore = (result.averageScore * 100).toFixed(1);
95
+ const completeText = pc.green(`${String(result.completeNodes)} complete`);
96
+ const incompleteText = pc.red(`${String(result.incompleteNodes)} incomplete`);
97
+ const scorePrefix = pc.dim(`Average score: ${avgScore}% | `);
98
+ const separator = pc.dim(" | ");
99
+ const summary = scorePrefix + completeText + separator + incompleteText;
100
+ console.log(pc.bold("\nCompleteness Analysis\n") + summary + "\n");
88
101
  // Show incomplete nodes first
89
102
  const incomplete = result.nodes.filter((n) => n.score < 1);
90
103
  const complete = result.nodes.filter((n) => n.score === 1);
@@ -111,9 +124,9 @@ const lifecycleSubcommand = {
111
124
  console.log(JSON.stringify(result, null, 2));
112
125
  }
113
126
  else {
114
- console.log(pc.bold("\nLifecycle Analysis\n") +
115
- pc.dim(`Early: ${String(result.summary.early)} | Middle: ${String(result.summary.middle)} | Late: ${String(result.summary.late)} | Terminal: ${String(result.summary.terminal)} | Unknown: ${String(result.summary.unknown)}`) +
116
- "\n");
127
+ const { early, middle, late, terminal, unknown } = result.summary;
128
+ const summary = pc.dim(`Early: ${String(early)} | Middle: ${String(middle)} | Late: ${String(late)} | Terminal: ${String(terminal)} | Unknown: ${String(unknown)}`);
129
+ console.log(pc.bold("\nLifecycle Analysis\n") + summary + "\n");
117
130
  for (const n of result.nodes)
118
131
  printLifecycleNode(n);
119
132
  }
@@ -149,9 +162,10 @@ const impactSubcommand = {
149
162
  ? ` [depth: ${String(args.maxDepth)}]`
150
163
  : "";
151
164
  const filterLabel = args.filter ? ` [filter: ${args.filter}]` : "";
152
- console.log(pc.bold(`\nImpact Analysis from ${args.id}${directionLabel}${depthLabel}${filterLabel}\n`) +
153
- pc.dim(`Direct: ${String(result.summary.direct)} | Transitive: ${String(result.summary.transitive)} | Potential: ${String(result.summary.potential)} | Total: ${String(result.summary.total)}`) +
154
- "\n");
165
+ const title = `\nImpact Analysis from ${args.id}${directionLabel}${depthLabel}${filterLabel}\n`;
166
+ const { direct, transitive, potential, total } = result.summary;
167
+ const summary = pc.dim(`Direct: ${String(direct)} | Transitive: ${String(transitive)} | Potential: ${String(potential)} | Total: ${String(total)}`);
168
+ console.log(pc.bold(title) + summary + "\n");
155
169
  if (result.impactedNodes.length === 0) {
156
170
  console.log(pc.dim("No impacted nodes found"));
157
171
  }
@@ -175,9 +189,9 @@ const derivedSubcommand = {
175
189
  console.log(JSON.stringify(result, null, 2));
176
190
  }
177
191
  else {
178
- console.log(pc.bold("\nDerived Relationships\n") +
179
- pc.dim(`Transitive: ${String(result.summary.transitive)} | Composite: ${String(result.summary.composite)} | Inverse: ${String(result.summary.inverse)} | Total: ${String(result.summary.total)}`) +
180
- "\n");
192
+ const { transitive, composite, inverse, total } = result.summary;
193
+ const summary = pc.dim(`Transitive: ${String(transitive)} | Composite: ${String(composite)} | Inverse: ${String(inverse)} | Total: ${String(total)}`);
194
+ console.log(pc.bold("\nDerived Relationships\n") + summary + "\n");
181
195
  if (result.derivedRelationships.length === 0) {
182
196
  console.log(pc.dim("No derived relationships found"));
183
197
  }
@@ -198,18 +212,23 @@ const allSubcommand = {
198
212
  // Completeness
199
213
  console.log(pc.bold("\n=== Completeness ===\n"));
200
214
  const completeness = inferCompletenessOp({ doc });
201
- console.log(pc.dim(`Average score: ${(completeness.averageScore * 100).toFixed(1)}% | `) +
202
- pc.green(`${String(completeness.completeNodes)} complete`) +
215
+ const completeScore = (completeness.averageScore * 100).toFixed(1);
216
+ const completeText = pc.green(`${String(completeness.completeNodes)} complete`);
217
+ const incompleteText = pc.red(`${String(completeness.incompleteNodes)} incomplete`);
218
+ console.log(pc.dim(`Average score: ${completeScore}% | `) +
219
+ completeText +
203
220
  pc.dim(" | ") +
204
- pc.red(`${String(completeness.incompleteNodes)} incomplete`));
221
+ incompleteText);
205
222
  // Lifecycle
206
223
  console.log(pc.bold("\n=== Lifecycle ===\n"));
207
224
  const lifecycle = inferLifecycleOp({ doc });
208
- console.log(pc.dim(`Early: ${String(lifecycle.summary.early)} | Middle: ${String(lifecycle.summary.middle)} | Late: ${String(lifecycle.summary.late)} | Terminal: ${String(lifecycle.summary.terminal)} | Unknown: ${String(lifecycle.summary.unknown)}`));
225
+ const { early, middle, late, terminal, unknown } = lifecycle.summary;
226
+ console.log(pc.dim(`Early: ${String(early)} | Middle: ${String(middle)} | Late: ${String(late)} | Terminal: ${String(terminal)} | Unknown: ${String(unknown)}`));
209
227
  // Derived
210
228
  console.log(pc.bold("\n=== Derived Relationships ===\n"));
211
229
  const derived = inferDerivedOp({ doc });
212
- console.log(pc.dim(`Transitive: ${String(derived.summary.transitive)} | Composite: ${String(derived.summary.composite)} | Inverse: ${String(derived.summary.inverse)} | Total: ${String(derived.summary.total)}`));
230
+ const { transitive, composite, inverse, total } = derived.summary;
231
+ console.log(pc.dim(`Transitive: ${String(transitive)} | Composite: ${String(composite)} | Inverse: ${String(inverse)} | Total: ${String(total)}`));
213
232
  if (opts.json) {
214
233
  console.log(JSON.stringify({
215
234
  completeness,
@@ -10,5 +10,5 @@ declare const optsSchema: z.ZodObject<{
10
10
  dir: "dir";
11
11
  }>>;
12
12
  }, z.core.$strict>;
13
- export declare const initCommand: CommandDef<z.ZodObject<z.ZodRawShape>, typeof optsSchema>;
13
+ export declare const initCommand: CommandDef<z.ZodObject, typeof optsSchema>;
14
14
  export {};
@@ -4,6 +4,35 @@ declare const optsSchema: z.ZodObject<{
4
4
  input: z.ZodString;
5
5
  output: z.ZodString;
6
6
  singleFile: z.ZodOptional<z.ZodBoolean>;
7
+ embedDiagrams: z.ZodOptional<z.ZodBoolean>;
8
+ labelMode: z.ZodOptional<z.ZodEnum<{
9
+ friendly: "friendly";
10
+ compact: "compact";
11
+ }>>;
12
+ relationshipLayout: z.ZodOptional<z.ZodEnum<{
13
+ TD: "TD";
14
+ BT: "BT";
15
+ RL: "RL";
16
+ LR: "LR";
17
+ }>>;
18
+ refinementLayout: z.ZodOptional<z.ZodEnum<{
19
+ TD: "TD";
20
+ BT: "BT";
21
+ RL: "RL";
22
+ LR: "LR";
23
+ }>>;
24
+ decisionLayout: z.ZodOptional<z.ZodEnum<{
25
+ TD: "TD";
26
+ BT: "BT";
27
+ RL: "RL";
28
+ LR: "LR";
29
+ }>>;
30
+ dependencyLayout: z.ZodOptional<z.ZodEnum<{
31
+ TD: "TD";
32
+ BT: "BT";
33
+ RL: "RL";
34
+ LR: "LR";
35
+ }>>;
7
36
  }, z.core.$strict>;
8
- export declare const json2mdCommand: CommandDef<z.ZodObject<z.ZodRawShape>, typeof optsSchema>;
37
+ export declare const json2mdCommand: CommandDef<z.ZodObject, typeof optsSchema>;
9
38
  export {};
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import * as z from "zod";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { resolve } from "node:path";
@@ -12,6 +13,30 @@ const optsSchema = z
12
13
  .boolean()
13
14
  .optional()
14
15
  .describe("Force single-file output format"),
16
+ embedDiagrams: z
17
+ .boolean()
18
+ .optional()
19
+ .describe("Embed Mermaid diagrams in the output"),
20
+ labelMode: z
21
+ .enum(["friendly", "compact"])
22
+ .optional()
23
+ .describe("Node label mode for embedded diagrams"),
24
+ relationshipLayout: z
25
+ .enum(["LR", "TD", "RL", "BT"])
26
+ .optional()
27
+ .describe("Override layout for relationship diagrams"),
28
+ refinementLayout: z
29
+ .enum(["LR", "TD", "RL", "BT"])
30
+ .optional()
31
+ .describe("Override layout for refinement diagrams"),
32
+ decisionLayout: z
33
+ .enum(["LR", "TD", "RL", "BT"])
34
+ .optional()
35
+ .describe("Override layout for decision diagrams"),
36
+ dependencyLayout: z
37
+ .enum(["LR", "TD", "RL", "BT"])
38
+ .optional()
39
+ .describe("Override layout for dependency diagrams"),
15
40
  })
16
41
  .strict();
17
42
  export const json2mdCommand = {
@@ -23,6 +48,7 @@ export const json2mdCommand = {
23
48
  const inputPath = resolve(opts.input);
24
49
  const outputPath = resolve(opts.output);
25
50
  const raw = JSON.parse(readFileSync(inputPath, "utf8"));
51
+ // Use the attached type guard for clearer intent and narrower runtime checks
26
52
  if (!SysProMDocument.is(raw)) {
27
53
  const result = SysProMDocument.safeParse(raw);
28
54
  if (!result.success) {
@@ -36,7 +62,22 @@ export const json2mdCommand = {
36
62
  const form = opts.singleFile || outputPath.endsWith(".md")
37
63
  ? "single-file"
38
64
  : "multi-doc";
39
- jsonToMarkdown(raw, outputPath, { form });
65
+ // Forward per-diagram layout overrides via environment-like flags.
66
+ // We accept flags on the command line for common diagram layouts but also
67
+ // preserve sensible per-diagram defaults in jsonToMarkdown when not set.
68
+ // For now we expose a single --label-mode flag and keep per-diagram layout
69
+ // defaults internal; a future enhancement could expose per-diagram
70
+ // flags such as --relationship-layout, --dependency-layout, etc.
71
+ jsonToMarkdown(raw, outputPath, {
72
+ form,
73
+ embedDiagrams: opts.embedDiagrams,
74
+ // forward labelMode for embedded diagrams (default friendly)
75
+ labelMode: opts.labelMode ?? "friendly",
76
+ relationshipLayout: opts.relationshipLayout,
77
+ refinementLayout: opts.refinementLayout,
78
+ decisionLayout: opts.decisionLayout,
79
+ dependencyLayout: opts.dependencyLayout,
80
+ });
40
81
  if (form === "single-file") {
41
82
  console.log(`Written to ${outputPath}`);
42
83
  }
@@ -4,5 +4,5 @@ declare const optsSchema: z.ZodObject<{
4
4
  input: z.ZodString;
5
5
  output: z.ZodString;
6
6
  }, z.core.$strict>;
7
- export declare const md2jsonCommand: CommandDef<z.ZodObject<z.ZodRawShape>, typeof optsSchema>;
7
+ export declare const md2jsonCommand: CommandDef<z.ZodObject, typeof optsSchema>;
8
8
  export {};
@@ -7,43 +7,41 @@ import { NodeType, NodeStatus } from "../../schema.js";
7
7
  // ---------------------------------------------------------------------------
8
8
  // Presentation helpers
9
9
  // ---------------------------------------------------------------------------
10
- function printNode(r, verbose) {
11
- if (verbose) {
12
- console.log(`${pc.cyan(r.id)}${pc.bold(r.name)}`);
13
- console.log(` ${pc.dim("Type")}: ${r.type}`);
14
- if (r.status)
15
- console.log(` ${pc.dim("Status")}: ${pc.yellow(r.status)}`);
16
- if (r.description)
17
- console.log(` ${pc.dim("Description")}: ${textToString(r.description)}`);
18
- if (r.context)
19
- console.log(` ${pc.dim("Context")}: ${textToString(r.context)}`);
20
- if (r.rationale)
21
- console.log(` ${pc.dim("Rationale")}: ${textToString(r.rationale)}`);
22
- if (r.selected)
23
- console.log(` ${pc.dim("Selected")}: ${r.selected}`);
24
- if (r.options) {
25
- console.log(` ${pc.dim("Options")}:`);
26
- for (const o of r.options)
27
- console.log(` ${o.id}: ${textToString(o.description)}`);
28
- }
29
- if (r.scope)
30
- console.log(` ${pc.dim("Scope")}: ${r.scope.join(", ")}`);
31
- if (r.lifecycle) {
32
- const states = Object.entries(r.lifecycle)
33
- .map(([k, v]) => `${k}=${String(v)}`)
34
- .join(", ");
35
- console.log(` ${pc.dim("Lifecycle")}: ${states}`);
36
- }
37
- if (r.includes)
38
- console.log(` ${pc.dim("Includes")}: ${r.includes.join(", ")}`);
39
- console.log("");
10
+ function printNodeCompact(r) {
11
+ const desc = r.description
12
+ ? " " + textToString(r.description).slice(0, 60)
13
+ : "";
14
+ console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)}${desc}`);
15
+ }
16
+ function printNodeVerbose(r) {
17
+ console.log(`${pc.cyan(r.id)} ${pc.bold(r.name)}`);
18
+ console.log(` ${pc.dim("Type")}: ${r.type}`);
19
+ if (r.status)
20
+ console.log(` ${pc.dim("Status")}: ${pc.yellow(r.status)}`);
21
+ if (r.description)
22
+ console.log(` ${pc.dim("Description")}: ${textToString(r.description)}`);
23
+ if (r.context)
24
+ console.log(` ${pc.dim("Context")}: ${textToString(r.context)}`);
25
+ if (r.rationale)
26
+ console.log(` ${pc.dim("Rationale")}: ${textToString(r.rationale)}`);
27
+ if (r.selected)
28
+ console.log(` ${pc.dim("Selected")}: ${r.selected}`);
29
+ if (r.options) {
30
+ console.log(` ${pc.dim("Options")}:`);
31
+ for (const o of r.options)
32
+ console.log(` ${o.id}: ${textToString(o.description)}`);
40
33
  }
41
- else {
42
- const desc = r.description
43
- ? " — " + textToString(r.description).slice(0, 60)
44
- : "";
45
- console.log(`${pc.cyan(r.id.padEnd(12))} ${pc.dim(r.type.padEnd(16))} ${pc.bold(r.name)}${desc}`);
34
+ if (r.scope)
35
+ console.log(` ${pc.dim("Scope")}: ${r.scope.join(", ")}`);
36
+ if (r.lifecycle) {
37
+ const states = Object.entries(r.lifecycle)
38
+ .map(([k, v]) => `${k}=${String(v)}`)
39
+ .join(", ");
40
+ console.log(` ${pc.dim("Lifecycle")}: ${states}`);
46
41
  }
42
+ if (r.includes)
43
+ console.log(` ${pc.dim("Includes")}: ${r.includes.join(", ")}`);
44
+ console.log("");
47
45
  }
48
46
  function isTraceTreeNode(value) {
49
47
  if (typeof value !== "object" || value === null)
@@ -103,7 +101,7 @@ const nodesSubcommand = {
103
101
  }
104
102
  else {
105
103
  for (const n of nodes)
106
- printNode(n, false);
104
+ printNodeCompact(n);
107
105
  console.log(`\n${String(nodes.length)} node(s)`);
108
106
  }
109
107
  },
@@ -127,7 +125,7 @@ const nodeSubcommand = {
127
125
  console.log(JSON.stringify(result.node, null, 2));
128
126
  }
129
127
  else {
130
- printNode(result.node, true);
128
+ printNodeVerbose(result.node);
131
129
  if (result.outgoing.length > 0) {
132
130
  console.log(`${pc.dim("Outgoing relationships")}:`);
133
131
  for (const r of result.outgoing)
@@ -18,6 +18,82 @@ function detectFormat(outputPath) {
18
18
  }
19
19
  return "json"; // default
20
20
  }
21
+ /**
22
+ * Validate that both paths exist, exits with error if not.
23
+ * @param inputPath - Path to SysProM document
24
+ * @param specKitDir - Path to Spec-Kit directory
25
+ * @example
26
+ * ```ts
27
+ * validatePaths(inputPath, specKitDir);
28
+ * ```
29
+ */
30
+ function validatePaths(inputPath, specKitDir) {
31
+ if (!existsSync(inputPath)) {
32
+ console.error(`Error: Input file does not exist: ${inputPath}`);
33
+ process.exit(1);
34
+ }
35
+ if (!existsSync(specKitDir)) {
36
+ console.error(`Error: Spec-Kit directory does not exist: ${specKitDir}`);
37
+ process.exit(1);
38
+ }
39
+ }
40
+ /**
41
+ * Search up to 5 parent directories for Spec-Kit constitution file.
42
+ * @param specKitDir - Path to Spec-Kit directory to start search from
43
+ * @returns Path to constitution file if found, undefined otherwise
44
+ * @example
45
+ * ```ts
46
+ * const path = findConstitutionPath(specKitDir);
47
+ * ```
48
+ */
49
+ function findConstitutionPath(specKitDir) {
50
+ let searchDir = dirname(specKitDir);
51
+ for (let i = 0; i < 5; i++) {
52
+ const project = detectSpecKitProject(searchDir);
53
+ if (project.constitutionPath)
54
+ return project.constitutionPath;
55
+ const parent = dirname(searchDir);
56
+ if (parent === searchDir)
57
+ break;
58
+ searchDir = parent;
59
+ }
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Print diff results in human-readable format.
64
+ * @param diff - The diff structure containing added, modified, and removed nodes
65
+ * @example
66
+ * ```ts
67
+ * printDiffResults(diff);
68
+ * ```
69
+ */
70
+ function printDiffResults(diff) {
71
+ console.log(`Diff between SysProM document and Spec-Kit directory:`);
72
+ if (diff.added.length === 0 &&
73
+ diff.modified.length === 0 &&
74
+ diff.removed.length === 0) {
75
+ console.log(` (no changes)`);
76
+ return;
77
+ }
78
+ if (diff.added.length > 0) {
79
+ console.log(` Added: ${String(diff.added.length)} node(s)`);
80
+ for (const node of diff.added) {
81
+ console.log(` - ${node.id}: ${node.name}`);
82
+ }
83
+ }
84
+ if (diff.modified.length > 0) {
85
+ console.log(` Modified: ${String(diff.modified.length)} node(s)`);
86
+ for (const { old } of diff.modified) {
87
+ console.log(` - ${old.id}: ${old.name}`);
88
+ }
89
+ }
90
+ if (diff.removed.length > 0) {
91
+ console.log(` Removed: ${String(diff.removed.length)} node(s)`);
92
+ for (const node of diff.removed) {
93
+ console.log(` - ${node.id}: ${node.name}`);
94
+ }
95
+ }
96
+ }
21
97
  function compareDocuments(oldDoc, newDoc) {
22
98
  const oldNodes = new Map(oldDoc.nodes.map((n) => [n.id, n]));
23
99
  const newNodes = new Map(newDoc.nodes.map((n) => [n.id, n]));
@@ -134,32 +210,10 @@ const syncSubcommand = {
134
210
  action(_args, opts) {
135
211
  const inputPath = resolve(opts.input);
136
212
  const specKitDir = resolve(opts.speckitDir);
137
- if (!existsSync(inputPath)) {
138
- console.error(`Error: Input file does not exist: ${inputPath}`);
139
- process.exit(1);
140
- }
141
- if (!existsSync(specKitDir)) {
142
- console.error(`Error: Spec-Kit directory does not exist: ${specKitDir}`);
143
- process.exit(1);
144
- }
145
- // Determine the prefix: use flag if provided, otherwise use directory name
213
+ validatePaths(inputPath, specKitDir);
146
214
  const idPrefix = opts.prefix ?? specKitDir.split("/").pop() ?? "FEAT";
147
- // Load SysProM document
148
215
  const { doc: syspromDoc, format } = loadDocument(inputPath);
149
- // Find constitution file
150
- let constitutionPath;
151
- let searchDir = dirname(specKitDir);
152
- for (let i = 0; i < 5; i++) {
153
- const project = detectSpecKitProject(searchDir);
154
- if (project.constitutionPath) {
155
- constitutionPath = project.constitutionPath;
156
- break;
157
- }
158
- const parent = dirname(searchDir);
159
- if (parent === searchDir)
160
- break;
161
- searchDir = parent;
162
- }
216
+ const constitutionPath = findConstitutionPath(specKitDir);
163
217
  // Parse Spec-Kit feature
164
218
  const specKitDoc = parseSpecKitFeature(specKitDir, idPrefix, constitutionPath);
165
219
  // Compare documents
@@ -232,63 +286,13 @@ const diffSubcommand = {
232
286
  action(_args, opts) {
233
287
  const inputPath = resolve(opts.input);
234
288
  const specKitDir = resolve(opts.speckitDir);
235
- if (!existsSync(inputPath)) {
236
- console.error(`Error: Input file does not exist: ${inputPath}`);
237
- process.exit(1);
238
- }
239
- if (!existsSync(specKitDir)) {
240
- console.error(`Error: Spec-Kit directory does not exist: ${specKitDir}`);
241
- process.exit(1);
242
- }
243
- // Determine the prefix: use flag if provided, otherwise use directory name
289
+ validatePaths(inputPath, specKitDir);
244
290
  const idPrefix = opts.prefix ?? specKitDir.split("/").pop() ?? "FEAT";
245
- // Load SysProM document
246
291
  const { doc: syspromDoc } = loadDocument(inputPath);
247
- // Find constitution file
248
- let constitutionPath;
249
- let searchDir = dirname(specKitDir);
250
- for (let i = 0; i < 5; i++) {
251
- const project = detectSpecKitProject(searchDir);
252
- if (project.constitutionPath) {
253
- constitutionPath = project.constitutionPath;
254
- break;
255
- }
256
- const parent = dirname(searchDir);
257
- if (parent === searchDir)
258
- break;
259
- searchDir = parent;
260
- }
261
- // Parse Spec-Kit feature
292
+ const constitutionPath = findConstitutionPath(specKitDir);
262
293
  const specKitDoc = parseSpecKitFeature(specKitDir, idPrefix, constitutionPath);
263
- // Compare documents
264
294
  const diff = compareDocuments(syspromDoc, specKitDoc);
265
- // Print what would change (read-only)
266
- console.log(`Diff between SysProM document and Spec-Kit directory:`);
267
- if (diff.added.length === 0 &&
268
- diff.modified.length === 0 &&
269
- diff.removed.length === 0) {
270
- console.log(` (no changes)`);
271
- }
272
- else {
273
- if (diff.added.length > 0) {
274
- console.log(` Added: ${String(diff.added.length)} node(s)`);
275
- for (const node of diff.added) {
276
- console.log(` - ${node.id}: ${node.name}`);
277
- }
278
- }
279
- if (diff.modified.length > 0) {
280
- console.log(` Modified: ${String(diff.modified.length)} node(s)`);
281
- for (const { old } of diff.modified) {
282
- console.log(` - ${old.id}: ${old.name}`);
283
- }
284
- }
285
- if (diff.removed.length > 0) {
286
- console.log(` Removed: ${String(diff.removed.length)} node(s)`);
287
- for (const node of diff.removed) {
288
- console.log(` - ${node.id}: ${node.name}`);
289
- }
290
- }
291
- }
295
+ printDiffResults(diff);
292
296
  },
293
297
  };
294
298
  // ============================================================================
@@ -12,13 +12,13 @@ export const statsCommand = {
12
12
  console.log(`${pc.bold("SysProM Document")}: ${s.title}`);
13
13
  console.log("");
14
14
  console.log(pc.bold("Nodes by type:"));
15
- for (const [type, count] of Object.entries(s.nodesByType).sort()) {
15
+ for (const [type, count] of Object.entries(s.nodesByType).sort(([a], [b]) => a.localeCompare(b))) {
16
16
  console.log(` ${type.padEnd(20)} ${pc.cyan(String(count))}`);
17
17
  }
18
18
  console.log(` ${"TOTAL".padEnd(20)} ${pc.cyan(String(s.totalNodes))}`);
19
19
  console.log("");
20
20
  console.log(pc.bold("Relationships by type:"));
21
- for (const [type, count] of Object.entries(s.relationshipsByType).sort()) {
21
+ for (const [type, count] of Object.entries(s.relationshipsByType).sort(([a], [b]) => a.localeCompare(b))) {
22
22
  console.log(` ${type.padEnd(20)} ${pc.cyan(String(count))}`);
23
23
  }
24
24
  console.log(` ${"TOTAL".padEnd(20)} ${pc.cyan(String(s.totalRelationships))}`);
@@ -30,14 +30,14 @@ export const statsCommand = {
30
30
  if (Object.keys(s.decisionLifecycle).length > 0) {
31
31
  console.log("");
32
32
  console.log(pc.bold("Decision lifecycle:"));
33
- for (const [state, count] of Object.entries(s.decisionLifecycle).sort()) {
33
+ for (const [state, count] of Object.entries(s.decisionLifecycle).sort(([a], [b]) => a.localeCompare(b))) {
34
34
  console.log(` ${state.padEnd(20)} ${pc.cyan(String(count))}`);
35
35
  }
36
36
  }
37
37
  if (Object.keys(s.changeLifecycle).length > 0) {
38
38
  console.log("");
39
39
  console.log(pc.bold("Change lifecycle:"));
40
- for (const [state, count] of Object.entries(s.changeLifecycle).sort()) {
40
+ for (const [state, count] of Object.entries(s.changeLifecycle).sort(([a], [b]) => a.localeCompare(b))) {
41
41
  console.log(` ${state.padEnd(20)} ${pc.cyan(String(count))}`);
42
42
  }
43
43
  }
@@ -23,5 +23,5 @@ declare const syncOpts: z.ZodObject<{
23
23
  dryRun: z.ZodOptional<z.ZodBoolean>;
24
24
  report: z.ZodOptional<z.ZodBoolean>;
25
25
  }, z.core.$strict>;
26
- export declare const syncCommandDef: CommandDef<z.ZodObject<z.ZodRawShape>, typeof syncOpts>;
26
+ export declare const syncCommandDef: CommandDef<z.ZodObject, typeof syncOpts>;
27
27
  export {};
@@ -6,21 +6,44 @@ import { mutationOpts, loadDoc, persistDoc } from "../shared.js";
6
6
  // CLI helper functions
7
7
  // ---------------------------------------------------------------------------
8
8
  function parseLifecycleValue(rawVal) {
9
- if (rawVal === "true") {
9
+ if (rawVal === "true")
10
10
  return true;
11
- }
12
- if (rawVal === "false") {
11
+ if (rawVal === "false")
13
12
  return false;
14
- }
15
- if (/^\d{4}-\d{2}-\d{2}/.test(rawVal)) {
16
- return rawVal; // ISO date string
17
- }
13
+ if (/^\d{4}-\d{2}-\d{2}/.test(rawVal))
14
+ return rawVal;
18
15
  return rawVal;
19
16
  }
20
17
  function parseMetaValue(val) {
21
18
  const numVal = Number(val);
22
19
  return Number.isFinite(numVal) && val === String(numVal) ? numVal : val;
23
20
  }
21
+ /**
22
+ * Parse lifecycle field updates from key=value format.
23
+ * @param nodeLifecycle - Optional existing lifecycle object from the node
24
+ * @param lifecycleArgs - Array of key=value strings to parse
25
+ * @returns Lifecycle object with parsed values, or null if no args
26
+ * @example
27
+ * ```ts
28
+ * const lifecycle = parseLifecycleFields(node.lifecycle, ["created=2026-01-01"]);
29
+ * ```
30
+ */
31
+ function parseLifecycleFields(nodeLifecycle, lifecycleArgs) {
32
+ if (!lifecycleArgs.length)
33
+ return null;
34
+ const lifecycle = { ...nodeLifecycle };
35
+ for (const kv of lifecycleArgs) {
36
+ const eqIdx = kv.indexOf("=");
37
+ if (eqIdx < 0) {
38
+ console.error(`Invalid --lifecycle format: ${kv} (expected key=value)`);
39
+ process.exit(1);
40
+ }
41
+ const key = kv.slice(0, eqIdx);
42
+ const rawVal = kv.slice(eqIdx + 1);
43
+ lifecycle[key] = parseLifecycleValue(rawVal);
44
+ }
45
+ return lifecycle;
46
+ }
24
47
  // ---------------------------------------------------------------------------
25
48
  // Arg/opt schemas
26
49
  // ---------------------------------------------------------------------------
@@ -80,19 +103,9 @@ const nodeSubcommand = {
80
103
  fields.context = opts.context;
81
104
  if (opts.rationale !== undefined)
82
105
  fields.rationale = opts.rationale;
83
- if (opts.lifecycle && opts.lifecycle.length > 0) {
84
- const lifecycle = { ...node.lifecycle };
85
- for (const kv of opts.lifecycle) {
86
- const eqIdx = kv.indexOf("=");
87
- if (eqIdx < 0) {
88
- console.error(`Invalid --lifecycle format: ${kv} (expected key=value)`);
89
- process.exit(1);
90
- }
91
- const key = kv.slice(0, eqIdx);
92
- const rawVal = kv.slice(eqIdx + 1);
93
- lifecycle[key] = parseLifecycleValue(rawVal);
94
- }
95
- fields.lifecycle = lifecycle;
106
+ const lifecycleFields = parseLifecycleFields(node.lifecycle, opts.lifecycle ?? []);
107
+ if (lifecycleFields) {
108
+ fields.lifecycle = lifecycleFields;
96
109
  }
97
110
  if (Object.keys(fields).length === 0) {
98
111
  console.error("No fields specified to update.");