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
|
@@ -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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Mermaid generation
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
function generateMermaid(nodes, rels, cluster, layout, labelMode) {
|
|
24
65
|
const lines = [];
|
|
25
|
-
lines.push(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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;
|