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.
- package/README.md +147 -75
- package/dist/schema.json +2 -1
- package/dist/src/cli/commands/add.js +52 -36
- package/dist/src/cli/commands/graph.d.ts +15 -0
- package/dist/src/cli/commands/graph.js +51 -2
- package/dist/src/cli/commands/infer.js +44 -25
- package/dist/src/cli/commands/init.d.ts +1 -1
- package/dist/src/cli/commands/json2md.d.ts +30 -1
- package/dist/src/cli/commands/json2md.js +42 -1
- package/dist/src/cli/commands/md2json.d.ts +1 -1
- package/dist/src/cli/commands/query.js +35 -37
- package/dist/src/cli/commands/speckit.js +81 -77
- package/dist/src/cli/commands/stats.js +4 -4
- package/dist/src/cli/commands/sync.d.ts +1 -1
- package/dist/src/cli/commands/update.js +33 -20
- package/dist/src/cli/define-command.d.ts +1 -1
- package/dist/src/cli/define-command.js +176 -156
- package/dist/src/endpoint-types.js +13 -6
- package/dist/src/io.js +59 -8
- package/dist/src/json-to-md.d.ts +32 -2
- package/dist/src/json-to-md.js +145 -5
- package/dist/src/mcp/server.js +269 -112
- package/dist/src/md-to-json.js +7 -0
- package/dist/src/operations/add-node.d.ts +12 -9
- package/dist/src/operations/add-plan-task.d.ts +8 -6
- package/dist/src/operations/add-relationship.d.ts +11 -8
- package/dist/src/operations/check.d.ts +4 -3
- package/dist/src/operations/define-operation.d.ts +1 -1
- package/dist/src/operations/graph-decision.d.ts +329 -0
- package/dist/src/operations/graph-decision.js +96 -0
- package/dist/src/operations/graph-dependency.d.ts +329 -0
- package/dist/src/operations/graph-dependency.js +121 -0
- package/dist/src/operations/graph-refinement.d.ts +329 -0
- package/dist/src/operations/graph-refinement.js +97 -0
- package/dist/src/operations/graph-shared.d.ts +116 -0
- package/dist/src/operations/graph-shared.js +257 -0
- package/dist/src/operations/graph.d.ts +20 -4
- package/dist/src/operations/graph.js +129 -36
- package/dist/src/operations/index.d.ts +3 -0
- package/dist/src/operations/index.js +3 -0
- package/dist/src/operations/infer-completeness.d.ts +4 -3
- package/dist/src/operations/infer-derived.d.ts +4 -3
- package/dist/src/operations/infer-impact.d.ts +28 -21
- package/dist/src/operations/infer-lifecycle.d.ts +4 -3
- package/dist/src/operations/init-document.d.ts +4 -3
- package/dist/src/operations/json-to-markdown.d.ts +28 -3
- package/dist/src/operations/json-to-markdown.js +11 -1
- package/dist/src/operations/mark-task-done.d.ts +8 -6
- package/dist/src/operations/mark-task-undone.d.ts +8 -6
- package/dist/src/operations/markdown-to-json.d.ts +4 -3
- package/dist/src/operations/next-id.d.ts +4 -3
- package/dist/src/operations/node-history.d.ts +4 -3
- package/dist/src/operations/plan-add-task.d.ts +8 -6
- package/dist/src/operations/plan-gate.d.ts +4 -3
- package/dist/src/operations/plan-init.d.ts +4 -3
- package/dist/src/operations/plan-progress.d.ts +4 -3
- package/dist/src/operations/plan-status.d.ts +4 -3
- package/dist/src/operations/query-node.d.ts +24 -17
- package/dist/src/operations/query-nodes.d.ts +8 -6
- package/dist/src/operations/query-relationships.d.ts +7 -5
- package/dist/src/operations/remove-node.d.ts +12 -9
- package/dist/src/operations/remove-relationship.d.ts +10 -7
- package/dist/src/operations/rename.d.ts +8 -6
- package/dist/src/operations/search.d.ts +8 -6
- package/dist/src/operations/speckit-diff.d.ts +4 -3
- package/dist/src/operations/speckit-export.d.ts +4 -3
- package/dist/src/operations/speckit-import.d.ts +4 -3
- package/dist/src/operations/speckit-sync.d.ts +12 -9
- package/dist/src/operations/state-at.d.ts +4 -3
- package/dist/src/operations/stats.d.ts +4 -3
- package/dist/src/operations/sync.d.ts +12 -9
- package/dist/src/operations/task-list.d.ts +4 -3
- package/dist/src/operations/timeline.d.ts +4 -3
- package/dist/src/operations/trace-from-node.d.ts +12 -9
- package/dist/src/operations/update-metadata.d.ts +8 -6
- package/dist/src/operations/update-node.d.ts +11 -8
- package/dist/src/operations/update-plan-task.d.ts +8 -6
- package/dist/src/operations/validate.d.ts +4 -3
- package/dist/src/schema.d.ts +15 -10
- package/dist/src/schema.js +3 -11
- package/dist/src/utils/define-schema.d.ts +17 -0
- package/dist/src/utils/define-schema.js +21 -0
- package/package.json +98 -93
- 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
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
221
|
+
incompleteText);
|
|
205
222
|
// Lifecycle
|
|
206
223
|
console.log(pc.bold("\n=== Lifecycle ===\n"));
|
|
207
224
|
const lifecycle = inferLifecycleOp({ doc });
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.");
|