sysprom 1.2.6 → 1.3.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.
@@ -0,0 +1,20 @@
1
+ import { NodeType, RelationshipType } from "./schema.js";
2
+ /**
3
+ * Defines which node types are valid for the source and target endpoints
4
+ * of each relationship type. Used for semantic validation of graph mutations.
5
+ *
6
+ * Note: Permissive by design to allow diverse relationship patterns while
7
+ * catching obvious semantic errors. The model is flexible across abstraction layers.
8
+ */
9
+ export declare const RELATIONSHIP_ENDPOINT_TYPES: Record<RelationshipType, {
10
+ from: NodeType[];
11
+ to: NodeType[];
12
+ }>;
13
+ /**
14
+ * Check if a relationship type is valid for the given endpoint node types.
15
+ * @param relType The relationship type
16
+ * @param fromType The source node type
17
+ * @param toType The target node type
18
+ * @returns true if the endpoint types are valid for this relationship
19
+ */
20
+ export declare function isValidEndpointPair(relType: RelationshipType, fromType: NodeType, toType: NodeType): boolean;
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Defines which node types are valid for the source and target endpoints
3
+ * of each relationship type. Used for semantic validation of graph mutations.
4
+ *
5
+ * Note: Permissive by design to allow diverse relationship patterns while
6
+ * catching obvious semantic errors. The model is flexible across abstraction layers.
7
+ */
8
+ export const RELATIONSHIP_ENDPOINT_TYPES = {
9
+ // Refines — used across all node types, represents specification refinement
10
+ refines: {
11
+ from: [
12
+ "intent",
13
+ "concept",
14
+ "capability",
15
+ "element",
16
+ "realisation",
17
+ "principle",
18
+ "policy",
19
+ ],
20
+ to: [
21
+ "intent",
22
+ "concept",
23
+ "capability",
24
+ "element",
25
+ "realisation",
26
+ "principle",
27
+ "policy",
28
+ ],
29
+ },
30
+ // Realises — implementation hierarchy
31
+ realises: {
32
+ from: ["capability", "element", "realisation"],
33
+ to: ["capability", "element", "realisation"],
34
+ },
35
+ // Implements — operationalisation
36
+ implements: {
37
+ from: ["element", "realisation", "change", "stage"],
38
+ to: ["capability", "element", "realisation", "decision", "change"],
39
+ },
40
+ // Depends on — broad dependency across all node types
41
+ depends_on: {
42
+ from: [
43
+ "intent",
44
+ "concept",
45
+ "capability",
46
+ "element",
47
+ "realisation",
48
+ "invariant",
49
+ "principle",
50
+ "policy",
51
+ "protocol",
52
+ "stage",
53
+ "role",
54
+ "gate",
55
+ "mode",
56
+ "artefact",
57
+ "artefact_flow",
58
+ "decision",
59
+ "change",
60
+ ],
61
+ to: [
62
+ "intent",
63
+ "concept",
64
+ "capability",
65
+ "element",
66
+ "realisation",
67
+ "invariant",
68
+ "principle",
69
+ "policy",
70
+ "protocol",
71
+ "stage",
72
+ "role",
73
+ "gate",
74
+ "mode",
75
+ "artefact",
76
+ "artefact_flow",
77
+ "decision",
78
+ "change",
79
+ ],
80
+ },
81
+ // Constrained by — constraints and governance
82
+ constrained_by: {
83
+ from: [
84
+ "intent",
85
+ "concept",
86
+ "capability",
87
+ "element",
88
+ "realisation",
89
+ "decision",
90
+ "change",
91
+ "invariant",
92
+ ],
93
+ to: ["invariant", "principle", "policy", "protocol", "concept"],
94
+ },
95
+ // Requires — explicit requirements
96
+ requires: {
97
+ from: [
98
+ "intent",
99
+ "concept",
100
+ "capability",
101
+ "element",
102
+ "realisation",
103
+ "stage",
104
+ "change",
105
+ ],
106
+ to: [
107
+ "intent",
108
+ "concept",
109
+ "capability",
110
+ "element",
111
+ "realisation",
112
+ "artefact",
113
+ ],
114
+ },
115
+ // Affects — broad impact relationships
116
+ affects: {
117
+ from: ["decision", "change", "artefact", "stage"],
118
+ to: [
119
+ "intent",
120
+ "concept",
121
+ "capability",
122
+ "element",
123
+ "realisation",
124
+ "invariant",
125
+ "principle",
126
+ "policy",
127
+ "protocol",
128
+ "decision",
129
+ "change",
130
+ "artefact",
131
+ ],
132
+ },
133
+ // Must preserve — invariant protection
134
+ must_preserve: {
135
+ from: ["decision"],
136
+ to: ["invariant", "principle", "policy", "concept"],
137
+ },
138
+ // Supersedes — replacement and obsolescence
139
+ supersedes: {
140
+ from: [
141
+ "intent",
142
+ "concept",
143
+ "capability",
144
+ "element",
145
+ "realisation",
146
+ "decision",
147
+ "change",
148
+ "version",
149
+ ],
150
+ to: [
151
+ "intent",
152
+ "concept",
153
+ "capability",
154
+ "element",
155
+ "realisation",
156
+ "decision",
157
+ "change",
158
+ "version",
159
+ ],
160
+ },
161
+ // Performs — process enactment
162
+ performs: {
163
+ from: ["stage", "role"],
164
+ to: ["capability", "artefact", "artefact_flow"],
165
+ },
166
+ // Precedes — temporal ordering
167
+ precedes: {
168
+ from: ["stage", "gate", "milestone"],
169
+ to: ["stage", "gate", "milestone"],
170
+ },
171
+ // Must follow — strong sequential ordering
172
+ must_follow: {
173
+ from: ["stage", "gate"],
174
+ to: ["stage", "gate"],
175
+ },
176
+ // Part of — structural decomposition
177
+ part_of: {
178
+ from: [
179
+ "intent",
180
+ "concept",
181
+ "capability",
182
+ "element",
183
+ "realisation",
184
+ "artefact",
185
+ "stage",
186
+ "policy",
187
+ "principle",
188
+ "role",
189
+ "gate",
190
+ "mode",
191
+ ],
192
+ to: [
193
+ "intent",
194
+ "concept",
195
+ "capability",
196
+ "element",
197
+ "realisation",
198
+ "artefact",
199
+ "stage",
200
+ "policy",
201
+ "principle",
202
+ "protocol",
203
+ "role",
204
+ "gate",
205
+ "mode",
206
+ ],
207
+ },
208
+ // Blocks — impediments and constraints
209
+ blocks: {
210
+ from: ["invariant", "policy", "decision", "principle"],
211
+ to: ["decision", "change", "stage"],
212
+ },
213
+ // Routes to — data/process flow
214
+ routes_to: {
215
+ from: ["artefact_flow"],
216
+ to: ["artefact_flow", "stage", "artefact"],
217
+ },
218
+ // Governed by — governance relationships
219
+ governed_by: {
220
+ from: [
221
+ "intent",
222
+ "concept",
223
+ "capability",
224
+ "element",
225
+ "realisation",
226
+ "stage",
227
+ "change",
228
+ "policy",
229
+ ],
230
+ to: ["policy", "protocol", "role", "principle", "invariant", "concept"],
231
+ },
232
+ // Modifies — mutation and change
233
+ modifies: {
234
+ from: ["change", "artefact_flow", "stage"],
235
+ to: [
236
+ "intent",
237
+ "concept",
238
+ "capability",
239
+ "element",
240
+ "realisation",
241
+ "artefact",
242
+ ],
243
+ },
244
+ // Transforms into — metamorphosis
245
+ transforms_into: {
246
+ from: ["artefact"],
247
+ to: ["artefact"],
248
+ },
249
+ // Triggered by — causation
250
+ triggered_by: {
251
+ from: ["stage", "gate", "artefact_flow", "change"],
252
+ to: ["decision", "artefact", "gate", "stage"],
253
+ },
254
+ // Disables — negation/reversal
255
+ disables: {
256
+ from: ["decision", "change"],
257
+ to: ["capability", "realisation"],
258
+ },
259
+ // Applies to — applicability and scope
260
+ applies_to: {
261
+ from: ["policy", "principle", "mode", "protocol"],
262
+ to: [
263
+ "intent",
264
+ "concept",
265
+ "capability",
266
+ "element",
267
+ "realisation",
268
+ "stage",
269
+ "decision",
270
+ "change",
271
+ ],
272
+ },
273
+ // Produces — generation
274
+ produces: {
275
+ from: ["stage", "artefact_flow", "realisation"],
276
+ to: ["artefact"],
277
+ },
278
+ // Consumes — usage
279
+ consumes: {
280
+ from: ["stage", "artefact_flow", "realisation"],
281
+ to: ["artefact"],
282
+ },
283
+ // Selects — choice and instantiation
284
+ selects: {
285
+ from: ["decision", "mode"],
286
+ to: ["capability", "stage"],
287
+ },
288
+ };
289
+ /**
290
+ * Check if a relationship type is valid for the given endpoint node types.
291
+ * @param relType The relationship type
292
+ * @param fromType The source node type
293
+ * @param toType The target node type
294
+ * @returns true if the endpoint types are valid for this relationship
295
+ */
296
+ export function isValidEndpointPair(relType, fromType, toType) {
297
+ const endpoints = RELATIONSHIP_ENDPOINT_TYPES[relType];
298
+ if (!endpoints) {
299
+ // Unknown relationship type — should be caught by schema validation
300
+ return false;
301
+ }
302
+ return (endpoints.from.includes(fromType) && endpoints.to.includes(toType));
303
+ }
@@ -9,6 +9,7 @@ export { SysProMDocument, Node, Relationship, NodeType, NodeStatus, Relationship
9
9
  export { defineOperation, type OperationDef, type DefinedOperation, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, updateMetadataOp, nextIdOp, initDocumentOp, addPlanTaskOp, updatePlanTaskOp, markTaskDoneOp, markTaskUndoneOp, taskListOp, planInitOp, planAddTaskOp, planStatusOp, planProgressOp, planGateOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, timelineOp, nodeHistoryOp, stateAtOp, validateOp, statsOp, searchOp, checkOp, graphOp, renameOp, jsonToMarkdownOp, markdownToJsonOp, speckitImportOp, speckitExportOp, speckitSyncOp, speckitDiffOp, type RemoveResult, type ValidationResult, type DocumentStats, type NodeDetail, type TraceNode, type TimelineEvent, type NodeState, type PlanStatusResult, type PhaseProgressResult, type GateResultOutput, type SyncResult, type DiffResult, } from "./operations/index.js";
10
10
  export { jsonToMarkdownSingle, jsonToMarkdownMultiDoc, jsonToMarkdown, type ConvertOptions, } from "./json-to-md.js";
11
11
  export { markdownSingleToJson, markdownMultiDocToJson, markdownToJson, } from "./md-to-json.js";
12
+ export { RELATIONSHIP_ENDPOINT_TYPES, isValidEndpointPair, } from "./endpoint-types.js";
12
13
  export { canonicalise, type FormatOptions } from "./canonical-json.js";
13
14
  export { textToString, textToLines, textToMarkdown, markdownToText, } from "./text.js";
14
15
  export { loadDocument, saveDocument, type Format, type LoadedDocument, } from "./io.js";
package/dist/src/index.js CHANGED
@@ -12,6 +12,8 @@ export { defineOperation, addNodeOp, removeNodeOp, updateNodeOp, addRelationship
12
12
  // Conversion
13
13
  export { jsonToMarkdownSingle, jsonToMarkdownMultiDoc, jsonToMarkdown, } from "./json-to-md.js";
14
14
  export { markdownSingleToJson, markdownMultiDocToJson, markdownToJson, } from "./md-to-json.js";
15
+ // Validation
16
+ export { RELATIONSHIP_ENDPOINT_TYPES, isValidEndpointPair, } from "./endpoint-types.js";
15
17
  // Utilities
16
18
  export { canonicalise } from "./canonical-json.js";
17
19
  export { textToString, textToLines, textToMarkdown, markdownToText, } from "./text.js";
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  /**
3
3
  * Add a relationship to a SysProM document. Returns a new document with the relationship appended.
4
- * @throws {Error} If either endpoint node does not exist in the document.
4
+ * @throws {Error} If either endpoint node does not exist, endpoint types are invalid, or the relationship is a duplicate.
5
5
  */
6
6
  export declare const addRelationshipOp: import("./define-operation.js").DefinedOperation<z.ZodObject<{
7
7
  doc: z.ZodObject<{
@@ -1,26 +1,38 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument, Relationship } from "../schema.js";
4
+ import { isValidEndpointPair } from "../endpoint-types.js";
4
5
  /**
5
6
  * Add a relationship to a SysProM document. Returns a new document with the relationship appended.
6
- * @throws {Error} If either endpoint node does not exist in the document.
7
+ * @throws {Error} If either endpoint node does not exist, endpoint types are invalid, or the relationship is a duplicate.
7
8
  */
8
9
  export const addRelationshipOp = defineOperation({
9
10
  name: "addRelationship",
10
- description: "Add a relationship to the document. Throws if either endpoint node does not exist.",
11
+ description: "Add a relationship to the document. Throws if either endpoint node does not exist, endpoint types are invalid, or the relationship is a duplicate.",
11
12
  input: z.object({
12
13
  doc: SysProMDocument,
13
14
  rel: Relationship,
14
15
  }),
15
16
  output: SysProMDocument,
16
17
  fn({ doc, rel }) {
17
- const ids = new Set(doc.nodes.map((n) => n.id));
18
- if (!ids.has(rel.from)) {
18
+ const nodeMap = new Map(doc.nodes.map((n) => [n.id, n]));
19
+ const fromNode = nodeMap.get(rel.from);
20
+ const toNode = nodeMap.get(rel.to);
21
+ if (!fromNode) {
19
22
  throw new Error(`Node not found: ${rel.from}`);
20
23
  }
21
- if (!ids.has(rel.to)) {
24
+ if (!toNode) {
22
25
  throw new Error(`Node not found: ${rel.to}`);
23
26
  }
27
+ // Validate endpoint types for this relationship
28
+ if (!isValidEndpointPair(rel.type, fromNode.type, toNode.type)) {
29
+ throw new Error(`Invalid endpoint types for ${rel.type}: ${fromNode.type} → ${toNode.type}`);
30
+ }
31
+ // Check for duplicate relationship
32
+ const isDuplicate = (doc.relationships ?? []).some((r) => r.from === rel.from && r.to === rel.to && r.type === rel.type);
33
+ if (isDuplicate) {
34
+ throw new Error(`Duplicate relationship already exists: ${rel.from} --${rel.type}--> ${rel.to}`);
35
+ }
24
36
  return {
25
37
  ...doc,
26
38
  relationships: [...(doc.relationships ?? []), rel],
@@ -302,7 +302,7 @@ export declare const RemoveResult: z.ZodObject<{
302
302
  export type RemoveResult = z.infer<typeof RemoveResult>;
303
303
  /**
304
304
  * Remove a node and all relationships involving it. Also removes the node from
305
- * view includes and external references. Warns if scope or operation references remain.
305
+ * view includes and external references. Cleans up scope and operation references.
306
306
  * @throws {Error} If the node ID is not found.
307
307
  */
308
308
  export declare const removeNodeOp: import("./define-operation.js").DefinedOperation<z.ZodObject<{
@@ -8,12 +8,12 @@ export const RemoveResult = z.object({
8
8
  });
9
9
  /**
10
10
  * Remove a node and all relationships involving it. Also removes the node from
11
- * view includes and external references. Warns if scope or operation references remain.
11
+ * view includes and external references. Cleans up scope and operation references.
12
12
  * @throws {Error} If the node ID is not found.
13
13
  */
14
14
  export const removeNodeOp = defineOperation({
15
15
  name: "removeNode",
16
- description: "Remove a node and all relationships involving it. Also removes the node from view includes and external references.",
16
+ description: "Remove a node and all relationships involving it. Cleans up all references in scopes, operations, views, and external references.",
17
17
  input: z.object({
18
18
  doc: SysProMDocument,
19
19
  id: z.string(),
@@ -27,34 +27,50 @@ export const removeNodeOp = defineOperation({
27
27
  const warnings = [];
28
28
  // Remove the node
29
29
  const newNodes = doc.nodes.filter((n) => n.id !== id);
30
- // Update view includes
31
- const nodesWithIncludes = newNodes.map((n) => {
30
+ // Clean up all references to the removed node
31
+ const cleanedNodes = newNodes.map((n) => {
32
+ let updated = n;
33
+ // Remove from view includes
32
34
  if (n.includes?.includes(id)) {
33
35
  const newIncludes = n.includes.filter((i) => i !== id);
34
- return {
35
- ...n,
36
+ updated = {
37
+ ...updated,
36
38
  includes: newIncludes.length > 0 ? newIncludes : undefined,
37
39
  };
38
40
  }
39
- return n;
40
- });
41
- // Remove relationships involving this node
42
- const newRelationships = (doc.relationships ?? []).filter((r) => r.from !== id && r.to !== id);
43
- // Check for scope and operation references
44
- for (const n of nodesWithIncludes) {
41
+ // Remove from scope
45
42
  if (n.scope?.includes(id)) {
43
+ const newScope = n.scope.filter((s) => s !== id);
46
44
  warnings.push(`${n.id} scope still references ${id}`);
45
+ updated = {
46
+ ...updated,
47
+ scope: newScope.length > 0 ? newScope : undefined,
48
+ };
47
49
  }
48
- if (n.operations?.some((op) => op.target === id)) {
50
+ // Remove from operations
51
+ const opsWithTarget = n.operations?.some((op) => op.target === id);
52
+ if (opsWithTarget) {
53
+ const newOps = n.operations?.filter((op) => op.target !== id);
49
54
  warnings.push(`${n.id} operations still reference ${id}`);
55
+ updated = {
56
+ ...updated,
57
+ operations: newOps && newOps.length > 0 ? newOps : undefined,
58
+ };
50
59
  }
60
+ return updated;
61
+ });
62
+ // Remove relationships involving this node
63
+ const oldRelCount = (doc.relationships ?? []).length;
64
+ const newRelationships = (doc.relationships ?? []).filter((r) => r.from !== id && r.to !== id);
65
+ if (newRelationships.length < oldRelCount) {
66
+ warnings.push(`Removed relationships involving ${id}`);
51
67
  }
52
68
  // Remove from external references
53
69
  const newExternalRefs = (doc.external_references ?? []).filter((ref) => ref.node_id !== id);
54
70
  return {
55
71
  doc: {
56
72
  ...doc,
57
- nodes: nodesWithIncludes,
73
+ nodes: cleanedNodes,
58
74
  relationships: newRelationships.length > 0 ? newRelationships : undefined,
59
75
  external_references: newExternalRefs.length > 0 ? newExternalRefs : undefined,
60
76
  },
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { defineOperation } from "./define-operation.js";
3
3
  import { SysProMDocument } from "../schema.js";
4
+ import { isValidEndpointPair } from "../endpoint-types.js";
4
5
  /** Zod schema for the result of validating a SysProM document. */
5
6
  export const ValidationResult = z.object({
6
7
  valid: z.boolean(),
@@ -49,6 +50,48 @@ export const validateOp = defineOperation({
49
50
  issues.push(`Relationship references unknown target: ${r.to}`);
50
51
  }
51
52
  }
53
+ // Duplicate relationships
54
+ const relSet = new Set();
55
+ for (const r of input.doc.relationships ?? []) {
56
+ const key = `${r.from}:${r.type}:${r.to}`;
57
+ if (relSet.has(key)) {
58
+ issues.push(`Duplicate relationship: ${r.from} --${r.type}--> ${r.to}`);
59
+ }
60
+ relSet.add(key);
61
+ }
62
+ // Endpoint type validation
63
+ const nodeMap = new Map(input.doc.nodes.map((n) => [n.id, n]));
64
+ for (const r of input.doc.relationships ?? []) {
65
+ const fromNode = nodeMap.get(r.from);
66
+ const toNode = nodeMap.get(r.to);
67
+ if (fromNode &&
68
+ toNode &&
69
+ !isValidEndpointPair(r.type, fromNode.type, toNode.type)) {
70
+ issues.push(`Invalid endpoint types for ${r.type}: ${fromNode.type} → ${toNode.type} (${r.from} → ${r.to})`);
71
+ }
72
+ }
73
+ // Operational relationships to retired nodes
74
+ const OPERATIONAL_REL_TYPES = new Set([
75
+ "depends_on",
76
+ "constrained_by",
77
+ "requires",
78
+ "affects",
79
+ "must_preserve",
80
+ "performs",
81
+ "must_follow",
82
+ "part_of",
83
+ "governed_by",
84
+ "modifies",
85
+ "applies_to",
86
+ "produces",
87
+ "consumes",
88
+ ]);
89
+ for (const r of input.doc.relationships ?? []) {
90
+ const toNode = nodeMap.get(r.to);
91
+ if (toNode?.status === "retired" && OPERATIONAL_REL_TYPES.has(r.type)) {
92
+ issues.push(`Operational relationship ${r.type} targets retired node ${r.to}`);
93
+ }
94
+ }
52
95
  // INV2: Changes must reference at least one decision
53
96
  const decisionIds = new Set(input.doc.nodes.filter((n) => n.type === "decision").map((n) => n.id));
54
97
  for (const n of input.doc.nodes.filter((n) => n.type === "change")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sysprom",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "SysProM — System Provenance Model CLI and library",
5
5
  "author": "ExaDev",
6
6
  "homepage": "https://exadev.github.io/SysProM",