sysprom 1.17.0 → 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 (76) hide show
  1. package/README.md +145 -75
  2. package/dist/schema.json +2 -1
  3. package/dist/src/cli/commands/graph.d.ts +15 -0
  4. package/dist/src/cli/commands/graph.js +51 -2
  5. package/dist/src/cli/commands/init.d.ts +1 -1
  6. package/dist/src/cli/commands/json2md.d.ts +30 -1
  7. package/dist/src/cli/commands/json2md.js +42 -1
  8. package/dist/src/cli/commands/md2json.d.ts +1 -1
  9. package/dist/src/cli/commands/sync.d.ts +1 -1
  10. package/dist/src/cli/define-command.d.ts +1 -1
  11. package/dist/src/cli/define-command.js +176 -156
  12. package/dist/src/endpoint-types.js +13 -6
  13. package/dist/src/json-to-md.d.ts +32 -2
  14. package/dist/src/json-to-md.js +145 -5
  15. package/dist/src/md-to-json.js +7 -0
  16. package/dist/src/operations/add-node.d.ts +12 -9
  17. package/dist/src/operations/add-plan-task.d.ts +8 -6
  18. package/dist/src/operations/add-relationship.d.ts +11 -8
  19. package/dist/src/operations/check.d.ts +4 -3
  20. package/dist/src/operations/define-operation.d.ts +1 -1
  21. package/dist/src/operations/graph-decision.d.ts +329 -0
  22. package/dist/src/operations/graph-decision.js +96 -0
  23. package/dist/src/operations/graph-dependency.d.ts +329 -0
  24. package/dist/src/operations/graph-dependency.js +121 -0
  25. package/dist/src/operations/graph-refinement.d.ts +329 -0
  26. package/dist/src/operations/graph-refinement.js +97 -0
  27. package/dist/src/operations/graph-shared.d.ts +116 -0
  28. package/dist/src/operations/graph-shared.js +257 -0
  29. package/dist/src/operations/graph.d.ts +20 -4
  30. package/dist/src/operations/graph.js +129 -36
  31. package/dist/src/operations/index.d.ts +3 -0
  32. package/dist/src/operations/index.js +3 -0
  33. package/dist/src/operations/infer-completeness.d.ts +4 -3
  34. package/dist/src/operations/infer-derived.d.ts +4 -3
  35. package/dist/src/operations/infer-impact.d.ts +28 -21
  36. package/dist/src/operations/infer-lifecycle.d.ts +4 -3
  37. package/dist/src/operations/init-document.d.ts +4 -3
  38. package/dist/src/operations/json-to-markdown.d.ts +28 -3
  39. package/dist/src/operations/json-to-markdown.js +11 -1
  40. package/dist/src/operations/mark-task-done.d.ts +8 -6
  41. package/dist/src/operations/mark-task-undone.d.ts +8 -6
  42. package/dist/src/operations/markdown-to-json.d.ts +4 -3
  43. package/dist/src/operations/next-id.d.ts +4 -3
  44. package/dist/src/operations/node-history.d.ts +4 -3
  45. package/dist/src/operations/plan-add-task.d.ts +8 -6
  46. package/dist/src/operations/plan-gate.d.ts +4 -3
  47. package/dist/src/operations/plan-init.d.ts +4 -3
  48. package/dist/src/operations/plan-progress.d.ts +4 -3
  49. package/dist/src/operations/plan-status.d.ts +4 -3
  50. package/dist/src/operations/query-node.d.ts +24 -17
  51. package/dist/src/operations/query-nodes.d.ts +8 -6
  52. package/dist/src/operations/query-relationships.d.ts +7 -5
  53. package/dist/src/operations/remove-node.d.ts +12 -9
  54. package/dist/src/operations/remove-relationship.d.ts +10 -7
  55. package/dist/src/operations/rename.d.ts +8 -6
  56. package/dist/src/operations/search.d.ts +8 -6
  57. package/dist/src/operations/speckit-diff.d.ts +4 -3
  58. package/dist/src/operations/speckit-export.d.ts +4 -3
  59. package/dist/src/operations/speckit-import.d.ts +4 -3
  60. package/dist/src/operations/speckit-sync.d.ts +12 -9
  61. package/dist/src/operations/state-at.d.ts +4 -3
  62. package/dist/src/operations/stats.d.ts +4 -3
  63. package/dist/src/operations/sync.d.ts +12 -9
  64. package/dist/src/operations/task-list.d.ts +4 -3
  65. package/dist/src/operations/timeline.d.ts +4 -3
  66. package/dist/src/operations/trace-from-node.d.ts +12 -9
  67. package/dist/src/operations/update-metadata.d.ts +8 -6
  68. package/dist/src/operations/update-node.d.ts +11 -8
  69. package/dist/src/operations/update-plan-task.d.ts +8 -6
  70. package/dist/src/operations/validate.d.ts +4 -3
  71. package/dist/src/schema.d.ts +15 -10
  72. package/dist/src/schema.js +3 -11
  73. package/dist/src/utils/define-schema.d.ts +17 -0
  74. package/dist/src/utils/define-schema.js +21 -0
  75. package/package.json +98 -93
  76. package/schema.json +2 -1
@@ -0,0 +1,257 @@
1
+ import { NODE_FILE_MAP } from "../schema.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Mermaid ID sanitisation
4
+ // ---------------------------------------------------------------------------
5
+ /**
6
+ * Sanitise an identifier for use in Mermaid diagrams by replacing
7
+ * non-alphanumeric/underscore characters with an underscore.
8
+ * @param id - Identifier to sanitise
9
+ * @example
10
+ * // sanitiseMermaidId('I1:Name') // 'I1_Name'
11
+ */
12
+ export function sanitiseMermaidId(id) {
13
+ return id.replace(/[^\w]/g, "_");
14
+ }
15
+ export const NODE_CATEGORIES = [
16
+ {
17
+ name: "intent",
18
+ label: "Intent",
19
+ types: NODE_FILE_MAP.INTENT,
20
+ colour: "#4A90D9",
21
+ textColour: "#fff",
22
+ },
23
+ {
24
+ name: "state",
25
+ label: "State",
26
+ types: NODE_FILE_MAP.STATE,
27
+ colour: "#67A86B",
28
+ textColour: "#fff",
29
+ },
30
+ {
31
+ name: "invariant",
32
+ label: "Invariants",
33
+ types: NODE_FILE_MAP.INVARIANTS,
34
+ colour: "#E8913A",
35
+ textColour: "#fff",
36
+ },
37
+ {
38
+ name: "decision",
39
+ label: "Decisions",
40
+ types: NODE_FILE_MAP.DECISIONS,
41
+ colour: "#9B59B6",
42
+ textColour: "#fff",
43
+ },
44
+ {
45
+ name: "change",
46
+ label: "Changes",
47
+ types: NODE_FILE_MAP.CHANGES,
48
+ colour: "#E74C3C",
49
+ textColour: "#fff",
50
+ },
51
+ {
52
+ name: "meta",
53
+ label: "Meta",
54
+ types: ["view", "milestone", "version"],
55
+ colour: "#95A5A6",
56
+ textColour: "#fff",
57
+ },
58
+ ];
59
+ /**
60
+ * Return the category for a given node. Falls back to the last category
61
+ * if the node type is unknown.
62
+ * @param node - Node to obtain category for
63
+ * @example
64
+ * // categoryForNode(node).name
65
+ */
66
+ export function categoryForNode(node) {
67
+ return (NODE_CATEGORIES.find((c) => c.types.includes(node.type)) ??
68
+ NODE_CATEGORIES[NODE_CATEGORIES.length - 1]);
69
+ }
70
+ /**
71
+ * Return the category for a node type.
72
+ * @param type - Node type string
73
+ * @example
74
+ * // categoryForType('intent').name // 'intent'
75
+ */
76
+ export function categoryForType(type) {
77
+ return (NODE_CATEGORIES.find((c) => c.types.includes(type)) ??
78
+ NODE_CATEGORIES[NODE_CATEGORIES.length - 1]);
79
+ }
80
+ const NODE_TYPE_SHAPES = {
81
+ intent: "rounded",
82
+ concept: "rounded",
83
+ capability: "rounded",
84
+ element: "rectangle",
85
+ realisation: "rectangle",
86
+ invariant: "parallelogram",
87
+ principle: "parallelogram",
88
+ policy: "parallelogram",
89
+ protocol: "rectangle",
90
+ stage: "rectangle",
91
+ role: "rectangle",
92
+ gate: "rectangle",
93
+ mode: "rectangle",
94
+ artefact: "rectangle",
95
+ artefact_flow: "rectangle",
96
+ decision: "rhombus",
97
+ change: "rectangle",
98
+ view: "rectangle",
99
+ milestone: "rectangle",
100
+ version: "rectangle",
101
+ };
102
+ /**
103
+ * Map a node to its Mermaid shape.
104
+ * @param node - Node to determine shape for
105
+ * @example
106
+ */
107
+ export function mermaidShapeForNode(node) {
108
+ return NODE_TYPE_SHAPES[node.type] ?? "rectangle";
109
+ }
110
+ /**
111
+ * Render a Mermaid node definition for a node id/name/shape.
112
+ * @param id - Node id
113
+ * @param name - Node name
114
+ * @param shape - Shape to render
115
+ * @param mode - Label mode, friendly shows id and name, compact shows id only
116
+ * @example
117
+ */
118
+ export function renderMermaidNode(id, name, shape, mode = "friendly") {
119
+ const safeId = sanitiseMermaidId(id);
120
+ const label = mode === "compact" ? id : `${id}: ${name}`;
121
+ switch (shape) {
122
+ case "rounded":
123
+ return `${safeId}([${label}])`;
124
+ case "rhombus":
125
+ return `${safeId}{{${label}}}`;
126
+ case "parallelogram":
127
+ return `${safeId}[/${label}/]`;
128
+ case "rectangle":
129
+ default:
130
+ return `${safeId}[${label}]`;
131
+ }
132
+ }
133
+ /**
134
+ * Render a human-friendly relationship label including polarity and strength
135
+ * @param rel
136
+ * @example
137
+ */
138
+ export function renderRelationshipLabel(rel) {
139
+ const parts = [];
140
+ if (rel.polarity)
141
+ parts.push(rel.polarity);
142
+ if (rel.strength !== undefined)
143
+ parts.push(rel.strength.toFixed(2));
144
+ if (parts.length === 0)
145
+ return rel.type;
146
+ return `${rel.type} (${parts.join(", ")})`;
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // Mermaid classDef generation
150
+ // ---------------------------------------------------------------------------
151
+ /**
152
+ * @example
153
+ */
154
+ export function renderMermaidClassDefs() {
155
+ return NODE_CATEGORIES.map((c) => `classDef ${c.name} fill:${c.colour},color:${c.textColour}`);
156
+ }
157
+ /**
158
+ * @param node
159
+ * @example
160
+ */
161
+ export function mermaidClassForNode(node) {
162
+ return categoryForNode(node).name;
163
+ }
164
+ // ---------------------------------------------------------------------------
165
+ // DOT attribute helpers
166
+ // ---------------------------------------------------------------------------
167
+ /**
168
+ * @param node
169
+ * @example
170
+ */
171
+ export function dotNodeAttrs(node) {
172
+ const cat = categoryForNode(node);
173
+ const shape = dotShapeForNode(node);
174
+ return `[label="${node.id}\\n${node.name}" shape=${shape} style=filled fillcolor="${cat.colour}" fontcolor="${cat.textColour}"]`;
175
+ }
176
+ /**
177
+ * Format a node label according to a mode. "friendly" shows id and name,
178
+ * "compact" shows only the id.
179
+ * @param node
180
+ * @param mode
181
+ * @example
182
+ */
183
+ export function formatNodeLabel(node, mode) {
184
+ if (mode === "compact")
185
+ return node.id;
186
+ return `${node.id}: ${node.name}`;
187
+ }
188
+ // Backwards-compatible wrapper to allow optional labelMode parameter
189
+ /**
190
+ * @param node
191
+ * @param mode
192
+ * @example
193
+ */
194
+ export function dotNodeAttrsWithMode(node, mode = "friendly") {
195
+ const cat = categoryForNode(node);
196
+ const shape = dotShapeForNode(node);
197
+ const label = mode === "compact" ? node.id : `${node.id}\\n${node.name}`;
198
+ return `[label="${label}" shape=${shape} style=filled fillcolor="${cat.colour}" fontcolor="${cat.textColour}"]`;
199
+ }
200
+ function dotShapeForNode(node) {
201
+ const s = mermaidShapeForNode(node);
202
+ switch (s) {
203
+ case "rounded":
204
+ return "box";
205
+ case "rhombus":
206
+ return "diamond";
207
+ case "parallelogram":
208
+ return "parallelogram";
209
+ case "rectangle":
210
+ default:
211
+ return "box";
212
+ }
213
+ }
214
+ /**
215
+ * @param nodes
216
+ * @param opts
217
+ * @example
218
+ */
219
+ export function filterNodes(nodes, opts) {
220
+ const { nodeTypes, nodeIds } = opts;
221
+ if (nodeTypes && nodeTypes.length > 0) {
222
+ nodes = nodes.filter((n) => nodeTypes.includes(n.type));
223
+ }
224
+ if (nodeIds && nodeIds.length > 0) {
225
+ nodes = nodes.filter((n) => nodeIds.includes(n.id));
226
+ }
227
+ return nodes;
228
+ }
229
+ /**
230
+ * @param rels
231
+ * @param opts
232
+ * @param allowedNodeIds
233
+ * @example
234
+ */
235
+ export function filterRelationships(rels, opts, allowedNodeIds) {
236
+ const { relTypes } = opts;
237
+ if (relTypes && relTypes.length > 0) {
238
+ rels = rels.filter((r) => relTypes.includes(r.type));
239
+ }
240
+ if (allowedNodeIds) {
241
+ rels = rels.filter((r) => allowedNodeIds.has(r.from) && allowedNodeIds.has(r.to));
242
+ }
243
+ return rels;
244
+ }
245
+ /**
246
+ * @param nodes
247
+ * @param rels
248
+ * @example
249
+ */
250
+ export function applyConnectedOnly(nodes, rels) {
251
+ const connectedIds = new Set();
252
+ for (const r of rels) {
253
+ connectedIds.add(r.from);
254
+ connectedIds.add(r.to);
255
+ }
256
+ return nodes.filter((n) => connectedIds.has(n.id));
257
+ }
@@ -1,5 +1,5 @@
1
1
  import * as z from "zod";
2
- /** Generate a graph of a SysProM document in Mermaid or DOT format, with optional filtering by relationship type. */
2
+ /** Generate a graph of a SysProM document in Mermaid or DOT format, with optional filtering. */
3
3
  export declare const graphOp: import("./define-operation.js").DefinedOperation<z.ZodObject<{
4
4
  doc: z.ZodObject<{
5
5
  $schema: z.ZodOptional<z.ZodString>;
@@ -185,8 +185,9 @@ export declare const graphOp: import("./define-operation.js").DefinedOperation<z
185
185
  requires: "requires";
186
186
  disables: "disables";
187
187
  influence: "influence";
188
+ justifies: "justifies";
188
189
  }> & {
189
- is(value: unknown): value is "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
190
+ is(value: unknown): value is "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
190
191
  };
191
192
  description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
192
193
  is(value: unknown): value is string | string[];
@@ -205,7 +206,7 @@ export declare const graphOp: import("./define-operation.js").DefinedOperation<z
205
206
  [x: string]: unknown;
206
207
  from: string;
207
208
  to: string;
208
- type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
209
+ type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
209
210
  description?: string | string[] | undefined;
210
211
  polarity?: "positive" | "negative" | "neutral" | "uncertain" | undefined;
211
212
  strength?: number | undefined;
@@ -296,7 +297,7 @@ export declare const graphOp: import("./define-operation.js").DefinedOperation<z
296
297
  [x: string]: unknown;
297
298
  from: string;
298
299
  to: string;
299
- type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
300
+ type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
300
301
  description?: string | string[] | undefined;
301
302
  polarity?: "positive" | "negative" | "neutral" | "uncertain" | undefined;
302
303
  strength?: number | undefined;
@@ -315,4 +316,19 @@ export declare const graphOp: import("./define-operation.js").DefinedOperation<z
315
316
  mermaid: "mermaid";
316
317
  }>>;
317
318
  typeFilter: z.ZodOptional<z.ZodString>;
319
+ nodeTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
320
+ nodeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
321
+ relTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
322
+ layout: z.ZodDefault<z.ZodEnum<{
323
+ TD: "TD";
324
+ BT: "BT";
325
+ RL: "RL";
326
+ LR: "LR";
327
+ }>>;
328
+ cluster: z.ZodDefault<z.ZodBoolean>;
329
+ labelMode: z.ZodDefault<z.ZodEnum<{
330
+ friendly: "friendly";
331
+ compact: "compact";
332
+ }>>;
333
+ connectedOnly: z.ZodDefault<z.ZodBoolean>;
318
334
  }, z.core.$strip>, z.ZodString>;
@@ -1,72 +1,165 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument } from "../schema.js";
4
- function sanitiseMermaidId(id) {
5
- return id.replace(/[^a-zA-Z0-9_]/g, "_");
6
- }
7
- function generateDot(doc, rels) {
4
+ import { sanitiseMermaidId, NODE_CATEGORIES, mermaidShapeForNode, renderMermaidNode, renderMermaidClassDefs, mermaidClassForNode, dotNodeAttrsWithMode, filterNodes, filterRelationships, applyConnectedOnly, } from "./graph-shared.js";
5
+ import { renderRelationshipLabel } from "./graph-shared.js";
6
+ // ---------------------------------------------------------------------------
7
+ // DOT generation
8
+ // ---------------------------------------------------------------------------
9
+ function generateDot(nodes, rels, cluster, layout, labelMode) {
8
10
  const lines = [];
9
11
  lines.push("digraph SysProM {");
10
- lines.push(" rankdir=LR;");
11
- // Add node labels
12
- for (const node of doc.nodes) {
13
- const label = `${node.id}\\n${node.name}`;
14
- lines.push(` "${node.id}" [label="${label}"];`);
12
+ // Map mermaid layout to DOT rankdir where needed
13
+ const rankdir = layout === "TD"
14
+ ? "TB"
15
+ : layout === "BT"
16
+ ? "BT"
17
+ : layout === "RL"
18
+ ? "RL"
19
+ : "LR";
20
+ lines.push(` rankdir=${rankdir};`);
21
+ lines.push(" node [style=filled];");
22
+ if (cluster) {
23
+ for (const cat of NODE_CATEGORIES) {
24
+ const catNodes = nodes.filter((n) => cat.types.includes(n.type));
25
+ if (catNodes.length === 0)
26
+ continue;
27
+ lines.push(` subgraph cluster_${cat.name} {`);
28
+ lines.push(` label="${cat.label}";`);
29
+ lines.push(` style=filled;`);
30
+ lines.push(` color="${cat.colour}22";`);
31
+ for (const node of catNodes) {
32
+ lines.push(` "${node.id}" ${dotNodeAttrsWithMode(node, labelMode)};`);
33
+ }
34
+ lines.push(" }");
35
+ }
36
+ const clusteredIds = new Set(NODE_CATEGORIES.flatMap((c) => nodes.filter((n) => c.types.includes(n.type)).map((n) => n.id)));
37
+ for (const node of nodes) {
38
+ if (!clusteredIds.has(node.id)) {
39
+ lines.push(` "${node.id}" ${dotNodeAttrsWithMode(node, labelMode)};`);
40
+ }
41
+ }
42
+ }
43
+ else {
44
+ for (const node of nodes) {
45
+ lines.push(` "${node.id}" ${dotNodeAttrsWithMode(node, labelMode)};`);
46
+ }
15
47
  }
16
- // Add relationships
17
48
  for (const rel of rels ?? []) {
18
- lines.push(` "${rel.from}" -> "${rel.to}" [label="${rel.type}"];`);
49
+ const label = renderRelationshipLabel(rel);
50
+ const attrs = [`label="${label}"`];
51
+ if (rel.polarity === "negative")
52
+ attrs.push("style=dashed");
53
+ if (rel.strength !== undefined && rel.strength >= 0.8) {
54
+ attrs.push("penwidth=2");
55
+ }
56
+ lines.push(` "${rel.from}" -> "${rel.to}" [${attrs.join(" ")}];`);
19
57
  }
20
58
  lines.push("}");
21
59
  return lines.join("\n");
22
60
  }
23
- function generateMermaid(doc, rels) {
61
+ // ---------------------------------------------------------------------------
62
+ // Mermaid generation
63
+ // ---------------------------------------------------------------------------
64
+ function generateMermaid(nodes, rels, cluster, layout, labelMode) {
24
65
  const lines = [];
25
- lines.push("graph LR");
26
- // Add node definitions with type-specific shapes
27
- for (const node of doc.nodes) {
28
- const id = sanitiseMermaidId(node.id);
29
- let shape;
30
- if (node.type === "decision") {
31
- shape = `{${node.id}: ${node.name}}`;
66
+ lines.push(`graph ${layout}`);
67
+ for (const def of renderMermaidClassDefs()) {
68
+ lines.push(` ${def}`);
69
+ }
70
+ lines.push("");
71
+ const nodeIds = new Set(nodes.map((n) => n.id));
72
+ if (cluster) {
73
+ for (const cat of NODE_CATEGORIES) {
74
+ const catNodes = nodes.filter((n) => cat.types.includes(n.type));
75
+ if (catNodes.length === 0)
76
+ continue;
77
+ lines.push(` subgraph ${cat.name} ["${cat.label}"]`);
78
+ for (const node of catNodes) {
79
+ const shape = mermaidShapeForNode(node);
80
+ const cls = mermaidClassForNode(node);
81
+ lines.push(` ${renderMermaidNode(node.id, node.name, shape, labelMode)}:::${cls}`);
82
+ }
83
+ lines.push(" end");
84
+ lines.push("");
32
85
  }
33
- else if (node.type === "invariant") {
34
- shape = `[/${node.id}: ${node.name}/]`;
86
+ const categorizedTypes = new Set(NODE_CATEGORIES.flatMap((c) => c.types));
87
+ for (const node of nodes) {
88
+ if (categorizedTypes.has(node.type))
89
+ continue;
90
+ const shape = mermaidShapeForNode(node);
91
+ const cls = mermaidClassForNode(node);
92
+ lines.push(` ${renderMermaidNode(node.id, node.name, shape, labelMode)}:::${cls}`);
35
93
  }
36
- else {
37
- shape = `[${node.id}: ${node.name}]`;
94
+ }
95
+ else {
96
+ for (const node of nodes) {
97
+ const shape = mermaidShapeForNode(node);
98
+ const cls = mermaidClassForNode(node);
99
+ lines.push(` ${renderMermaidNode(node.id, node.name, shape, labelMode)}:::${cls}`);
38
100
  }
39
- lines.push(` ${id}${shape}`);
40
101
  }
41
- // Add relationships
102
+ lines.push("");
42
103
  for (const rel of rels ?? []) {
104
+ if (!nodeIds.has(rel.from) || !nodeIds.has(rel.to))
105
+ continue;
43
106
  const fromId = sanitiseMermaidId(rel.from);
44
107
  const toId = sanitiseMermaidId(rel.to);
45
- lines.push(` ${fromId} -->|${rel.type}| ${toId}`);
108
+ const label = renderRelationshipLabel(rel);
109
+ let edge = `${fromId} -->|${label}| ${toId}`;
110
+ if (rel.polarity === "negative") {
111
+ edge = `${fromId} -.->|${label}| ${toId}`;
112
+ }
113
+ lines.push(` ${edge}`);
46
114
  }
47
115
  return lines.join("\n");
48
116
  }
49
- function generateGraph(doc, format, typeFilter) {
50
- let rels = doc.relationships ?? [];
51
- if (typeFilter) {
52
- rels = rels.filter((r) => r.type === typeFilter);
117
+ // ---------------------------------------------------------------------------
118
+ // Graph generation
119
+ // ---------------------------------------------------------------------------
120
+ function generateGraph(doc, format, filterOpts, layout, cluster, labelMode) {
121
+ let nodes = filterNodes(doc.nodes, filterOpts);
122
+ const nodeIds = new Set(nodes.map((n) => n.id));
123
+ let rels = filterRelationships(doc.relationships ?? [], filterOpts, nodeIds);
124
+ if (filterOpts.connectedOnly) {
125
+ nodes = applyConnectedOnly(nodes, rels);
126
+ rels = filterRelationships(rels, {}, new Set(nodes.map((n) => n.id)));
53
127
  }
54
128
  if (format === "dot") {
55
- return generateDot(doc, rels);
129
+ // Historically DOT output used left-right rankdir by default regardless
130
+ // of the Mermaid default layout. Preserve that behaviour: if the
131
+ // requested layout is the Mermaid default "TD", map it to "LR" for DOT
132
+ // so existing tests and callers keep expecting LR unless another layout
133
+ // was explicitly chosen.
134
+ const dotLayout = layout === "TD" ? "LR" : layout;
135
+ return generateDot(nodes, rels, cluster, dotLayout, labelMode);
56
136
  }
57
- return generateMermaid(doc, rels);
137
+ return generateMermaid(nodes, rels, cluster, layout, labelMode);
58
138
  }
59
- /** Generate a graph of a SysProM document in Mermaid or DOT format, with optional filtering by relationship type. */
139
+ /** Generate a graph of a SysProM document in Mermaid or DOT format, with optional filtering. */
60
140
  export const graphOp = defineOperation({
61
141
  name: "graph",
62
- description: "Generate a graph of the SysProM document in Mermaid or DOT format, with optional filtering by relationship type.",
142
+ description: "Generate a graph of the SysProM document in Mermaid or DOT format, with optional filtering by node type, node ID, or relationship type.",
63
143
  input: z.object({
64
144
  doc: SysProMDocument,
65
145
  format: z.enum(["mermaid", "dot"]).default("mermaid"),
66
146
  typeFilter: z.string().optional(),
147
+ nodeTypes: z.array(z.string()).optional(),
148
+ nodeIds: z.array(z.string()).optional(),
149
+ relTypes: z.array(z.string()).optional(),
150
+ layout: z.enum(["LR", "TD", "RL", "BT"]).default("TD"),
151
+ cluster: z.boolean().default(true),
152
+ labelMode: z.enum(["friendly", "compact"]).default("friendly"),
153
+ connectedOnly: z.boolean().default(false),
67
154
  }),
68
155
  output: z.string(),
69
- fn({ doc, format, typeFilter }) {
70
- return generateGraph(doc, format, typeFilter);
156
+ fn({ doc, format, typeFilter, nodeTypes, nodeIds, relTypes, layout, cluster, labelMode, connectedOnly, }) {
157
+ const filterOpts = {
158
+ nodeTypes,
159
+ nodeIds,
160
+ relTypes: relTypes ?? (typeFilter ? [typeFilter] : undefined),
161
+ connectedOnly,
162
+ };
163
+ return generateGraph(doc, format, filterOpts, layout, cluster, labelMode);
71
164
  },
72
165
  });
@@ -29,6 +29,9 @@ export { statsOp, type DocumentStats } from "./stats.js";
29
29
  export { searchOp } from "./search.js";
30
30
  export { checkOp } from "./check.js";
31
31
  export { graphOp } from "./graph.js";
32
+ export { graphRefinementOp } from "./graph-refinement.js";
33
+ export { graphDecisionOp } from "./graph-decision.js";
34
+ export { graphDependencyOp } from "./graph-dependency.js";
32
35
  export { renameOp } from "./rename.js";
33
36
  export { jsonToMarkdownOp } from "./json-to-markdown.js";
34
37
  export { markdownToJsonOp } from "./markdown-to-json.js";
@@ -34,6 +34,9 @@ export { statsOp } from "./stats.js";
34
34
  export { searchOp } from "./search.js";
35
35
  export { checkOp } from "./check.js";
36
36
  export { graphOp } from "./graph.js";
37
+ export { graphRefinementOp } from "./graph-refinement.js";
38
+ export { graphDecisionOp } from "./graph-decision.js";
39
+ export { graphDependencyOp } from "./graph-dependency.js";
37
40
  export { renameOp } from "./rename.js";
38
41
  // Conversion operations
39
42
  export { jsonToMarkdownOp } from "./json-to-markdown.js";
@@ -265,8 +265,9 @@ export declare const inferCompletenessOp: import("./define-operation.js").Define
265
265
  requires: "requires";
266
266
  disables: "disables";
267
267
  influence: "influence";
268
+ justifies: "justifies";
268
269
  }> & {
269
- is(value: unknown): value is "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
270
+ is(value: unknown): value is "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
270
271
  };
271
272
  description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
272
273
  is(value: unknown): value is string | string[];
@@ -285,7 +286,7 @@ export declare const inferCompletenessOp: import("./define-operation.js").Define
285
286
  [x: string]: unknown;
286
287
  from: string;
287
288
  to: string;
288
- type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
289
+ type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
289
290
  description?: string | string[] | undefined;
290
291
  polarity?: "positive" | "negative" | "neutral" | "uncertain" | undefined;
291
292
  strength?: number | undefined;
@@ -376,7 +377,7 @@ export declare const inferCompletenessOp: import("./define-operation.js").Define
376
377
  [x: string]: unknown;
377
378
  from: string;
378
379
  to: string;
379
- type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
380
+ type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
380
381
  description?: string | string[] | undefined;
381
382
  polarity?: "positive" | "negative" | "neutral" | "uncertain" | undefined;
382
383
  strength?: number | undefined;
@@ -238,8 +238,9 @@ export declare const inferDerivedOp: import("./define-operation.js").DefinedOper
238
238
  requires: "requires";
239
239
  disables: "disables";
240
240
  influence: "influence";
241
+ justifies: "justifies";
241
242
  }> & {
242
- is(value: unknown): value is "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
243
+ is(value: unknown): value is "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
243
244
  };
244
245
  description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
245
246
  is(value: unknown): value is string | string[];
@@ -258,7 +259,7 @@ export declare const inferDerivedOp: import("./define-operation.js").DefinedOper
258
259
  [x: string]: unknown;
259
260
  from: string;
260
261
  to: string;
261
- type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
262
+ type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
262
263
  description?: string | string[] | undefined;
263
264
  polarity?: "positive" | "negative" | "neutral" | "uncertain" | undefined;
264
265
  strength?: number | undefined;
@@ -349,7 +350,7 @@ export declare const inferDerivedOp: import("./define-operation.js").DefinedOper
349
350
  [x: string]: unknown;
350
351
  from: string;
351
352
  to: string;
352
- type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence";
353
+ type: "refines" | "realises" | "implements" | "depends_on" | "constrained_by" | "affects" | "supersedes" | "must_preserve" | "performs" | "part_of" | "precedes" | "must_follow" | "blocks" | "routes_to" | "governed_by" | "modifies" | "triggered_by" | "applies_to" | "produces" | "consumes" | "transforms_into" | "selects" | "requires" | "disables" | "influence" | "justifies";
353
354
  description?: string | string[] | undefined;
354
355
  polarity?: "positive" | "negative" | "neutral" | "uncertain" | undefined;
355
356
  strength?: number | undefined;