sysprom 1.2.6 → 1.4.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/dist/src/cli/commands/update.js +2 -2
- package/dist/src/endpoint-types.d.ts +20 -0
- package/dist/src/endpoint-types.js +303 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/operations/add-relationship.d.ts +1 -1
- package/dist/src/operations/add-relationship.js +17 -5
- package/dist/src/operations/remove-node.d.ts +4 -1
- package/dist/src/operations/remove-node.js +82 -18
- package/dist/src/operations/remove-relationship.d.ts +239 -234
- package/dist/src/operations/remove-relationship.js +56 -5
- package/dist/src/operations/validate.js +43 -0
- package/package.json +1 -1
|
@@ -147,13 +147,13 @@ const removeRelSubcommand = {
|
|
|
147
147
|
const opts = removeRelOpts.parse(rawOpts);
|
|
148
148
|
const loaded = loadDoc(opts.path);
|
|
149
149
|
const { doc } = loaded;
|
|
150
|
-
const
|
|
150
|
+
const result = removeRelationshipOp({
|
|
151
151
|
doc,
|
|
152
152
|
from: args.from,
|
|
153
153
|
type: args.type,
|
|
154
154
|
to: args.to,
|
|
155
155
|
});
|
|
156
|
-
persistDoc(
|
|
156
|
+
persistDoc(result.doc, loaded, opts);
|
|
157
157
|
if (opts.json) {
|
|
158
158
|
console.log(JSON.stringify({ from: args.from, type: args.type, to: args.to }, null, 2));
|
|
159
159
|
}
|
|
@@ -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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
18
|
-
|
|
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 (!
|
|
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.
|
|
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<{
|
|
@@ -602,6 +602,9 @@ export declare const removeNodeOp: import("./define-operation.js").DefinedOperat
|
|
|
602
602
|
};
|
|
603
603
|
};
|
|
604
604
|
id: z.ZodString;
|
|
605
|
+
hard: z.ZodOptional<z.ZodBoolean>;
|
|
606
|
+
recursive: z.ZodOptional<z.ZodBoolean>;
|
|
607
|
+
repair: z.ZodOptional<z.ZodBoolean>;
|
|
605
608
|
}, z.core.$strip>, z.ZodObject<{
|
|
606
609
|
doc: z.ZodObject<{
|
|
607
610
|
$schema: z.ZodOptional<z.ZodString>;
|
|
@@ -8,53 +8,117 @@ 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.
|
|
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.
|
|
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(),
|
|
20
|
+
hard: z.boolean().optional(),
|
|
21
|
+
recursive: z.boolean().optional(),
|
|
22
|
+
repair: z.boolean().optional(),
|
|
20
23
|
}),
|
|
21
24
|
output: RemoveResult,
|
|
22
|
-
fn({ doc, id }) {
|
|
25
|
+
fn({ doc, id, hard, recursive, repair }) {
|
|
23
26
|
const nodeIdx = doc.nodes.findIndex((n) => n.id === id);
|
|
24
27
|
if (nodeIdx === -1) {
|
|
25
28
|
throw new Error(`Node not found: ${id}`);
|
|
26
29
|
}
|
|
30
|
+
const nodeToRemove = doc.nodes[nodeIdx];
|
|
27
31
|
const warnings = [];
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// Check recursive guard for hard delete
|
|
33
|
+
if (hard && nodeToRemove.subsystem) {
|
|
34
|
+
if (!recursive) {
|
|
35
|
+
throw new Error(`Cannot hard delete node ${id} with subsystem without --recursive flag`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let newNodes;
|
|
39
|
+
let newRelationships = doc.relationships ?? [];
|
|
40
|
+
if (hard) {
|
|
41
|
+
// Hard delete: physically remove the node
|
|
42
|
+
newNodes = doc.nodes.filter((n) => n.id !== id);
|
|
43
|
+
// Handle must_follow chain repair if requested
|
|
44
|
+
if (repair) {
|
|
45
|
+
const incomingChains = newRelationships.filter((r) => r.to === id && r.type === "must_follow");
|
|
46
|
+
const outgoingChains = newRelationships.filter((r) => r.from === id && r.type === "must_follow");
|
|
47
|
+
// Remove all relationships involving the deleted node
|
|
48
|
+
newRelationships = newRelationships.filter((r) => r.from !== id && r.to !== id);
|
|
49
|
+
// Repair chains by connecting incoming to outgoing
|
|
50
|
+
// Only repair if there are both incoming AND outgoing chains
|
|
51
|
+
if (incomingChains.length > 0 && outgoingChains.length > 0) {
|
|
52
|
+
for (const incoming of incomingChains) {
|
|
53
|
+
for (const outgoing of outgoingChains) {
|
|
54
|
+
// Only add if not already connected
|
|
55
|
+
const exists = newRelationships.some((r) => r.from === incoming.from &&
|
|
56
|
+
r.to === outgoing.to &&
|
|
57
|
+
r.type === "must_follow");
|
|
58
|
+
if (!exists) {
|
|
59
|
+
newRelationships.push({
|
|
60
|
+
from: incoming.from,
|
|
61
|
+
to: outgoing.to,
|
|
62
|
+
type: "must_follow",
|
|
63
|
+
});
|
|
64
|
+
warnings.push(`Repaired chain: ${incoming.from} → ${outgoing.to}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Without repair, just remove all relationships
|
|
72
|
+
const oldRelCount = newRelationships.length;
|
|
73
|
+
newRelationships = newRelationships.filter((r) => r.from !== id && r.to !== id);
|
|
74
|
+
if (newRelationships.length < oldRelCount) {
|
|
75
|
+
warnings.push(`Removed relationships involving ${id}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Soft delete: mark as retired and preserve relationships
|
|
81
|
+
newNodes = doc.nodes.map((n) => n.id === id ? { ...n, status: "retired" } : n);
|
|
82
|
+
// Don't remove relationships in soft delete
|
|
83
|
+
}
|
|
84
|
+
// Clean up all references to the removed node (both soft and hard)
|
|
85
|
+
const cleanedNodes = newNodes.map((n) => {
|
|
86
|
+
let updated = n;
|
|
87
|
+
// Remove from view includes
|
|
32
88
|
if (n.includes?.includes(id)) {
|
|
33
89
|
const newIncludes = n.includes.filter((i) => i !== id);
|
|
34
|
-
|
|
35
|
-
...
|
|
90
|
+
updated = {
|
|
91
|
+
...updated,
|
|
36
92
|
includes: newIncludes.length > 0 ? newIncludes : undefined,
|
|
37
93
|
};
|
|
38
94
|
}
|
|
39
|
-
|
|
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) {
|
|
95
|
+
// Remove from scope
|
|
45
96
|
if (n.scope?.includes(id)) {
|
|
97
|
+
const newScope = n.scope.filter((s) => s !== id);
|
|
46
98
|
warnings.push(`${n.id} scope still references ${id}`);
|
|
99
|
+
updated = {
|
|
100
|
+
...updated,
|
|
101
|
+
scope: newScope.length > 0 ? newScope : undefined,
|
|
102
|
+
};
|
|
47
103
|
}
|
|
48
|
-
|
|
104
|
+
// Remove from operations
|
|
105
|
+
const opsWithTarget = n.operations?.some((op) => op.target === id);
|
|
106
|
+
if (opsWithTarget) {
|
|
107
|
+
const newOps = n.operations?.filter((op) => op.target !== id);
|
|
49
108
|
warnings.push(`${n.id} operations still reference ${id}`);
|
|
109
|
+
updated = {
|
|
110
|
+
...updated,
|
|
111
|
+
operations: newOps && newOps.length > 0 ? newOps : undefined,
|
|
112
|
+
};
|
|
50
113
|
}
|
|
51
|
-
|
|
114
|
+
return updated;
|
|
115
|
+
});
|
|
52
116
|
// Remove from external references
|
|
53
117
|
const newExternalRefs = (doc.external_references ?? []).filter((ref) => ref.node_id !== id);
|
|
54
118
|
return {
|
|
55
119
|
doc: {
|
|
56
120
|
...doc,
|
|
57
|
-
nodes:
|
|
121
|
+
nodes: cleanedNodes,
|
|
58
122
|
relationships: newRelationships.length > 0 ? newRelationships : undefined,
|
|
59
123
|
external_references: newExternalRefs.length > 0 ? newExternalRefs : undefined,
|
|
60
124
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
/**
|
|
3
3
|
* Remove a relationship matching from, type, and to. Returns a new document without it.
|
|
4
|
+
* Optionally repairs must_follow chains when removal would break them.
|
|
4
5
|
* @throws {Error} If no matching relationship is found.
|
|
5
6
|
*/
|
|
6
7
|
export declare const removeRelationshipOp: import("./define-operation.js").DefinedOperation<z.ZodObject<{
|
|
@@ -329,130 +330,206 @@ export declare const removeRelationshipOp: import("./define-operation.js").Defin
|
|
|
329
330
|
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";
|
|
330
331
|
};
|
|
331
332
|
to: z.ZodString;
|
|
333
|
+
repair: z.ZodOptional<z.ZodBoolean>;
|
|
332
334
|
}, z.core.$strip>, z.ZodObject<{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
is(value: unknown): value is {
|
|
342
|
-
[x: string]: unknown;
|
|
343
|
-
title?: string | undefined;
|
|
344
|
-
doc_type?: string | undefined;
|
|
345
|
-
scope?: string | undefined;
|
|
346
|
-
status?: string | undefined;
|
|
347
|
-
version?: string | number | undefined;
|
|
348
|
-
};
|
|
349
|
-
}>;
|
|
350
|
-
nodes: z.ZodArray<z.ZodObject<{
|
|
351
|
-
id: z.ZodString;
|
|
352
|
-
type: z.ZodEnum<{
|
|
353
|
-
intent: "intent";
|
|
354
|
-
concept: "concept";
|
|
355
|
-
capability: "capability";
|
|
356
|
-
element: "element";
|
|
357
|
-
realisation: "realisation";
|
|
358
|
-
invariant: "invariant";
|
|
359
|
-
principle: "principle";
|
|
360
|
-
policy: "policy";
|
|
361
|
-
protocol: "protocol";
|
|
362
|
-
stage: "stage";
|
|
363
|
-
role: "role";
|
|
364
|
-
gate: "gate";
|
|
365
|
-
mode: "mode";
|
|
366
|
-
artefact: "artefact";
|
|
367
|
-
artefact_flow: "artefact_flow";
|
|
368
|
-
decision: "decision";
|
|
369
|
-
change: "change";
|
|
370
|
-
view: "view";
|
|
371
|
-
milestone: "milestone";
|
|
372
|
-
version: "version";
|
|
373
|
-
}> & {
|
|
374
|
-
is(value: unknown): value is "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
|
|
375
|
-
};
|
|
376
|
-
name: z.ZodString;
|
|
377
|
-
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
378
|
-
is(value: unknown): value is string | string[];
|
|
379
|
-
}>;
|
|
380
|
-
status: z.ZodOptional<z.ZodEnum<{
|
|
381
|
-
deprecated: "deprecated";
|
|
382
|
-
proposed: "proposed";
|
|
383
|
-
accepted: "accepted";
|
|
384
|
-
active: "active";
|
|
385
|
-
implemented: "implemented";
|
|
386
|
-
adopted: "adopted";
|
|
387
|
-
defined: "defined";
|
|
388
|
-
introduced: "introduced";
|
|
389
|
-
in_progress: "in_progress";
|
|
390
|
-
complete: "complete";
|
|
391
|
-
consolidated: "consolidated";
|
|
392
|
-
experimental: "experimental";
|
|
393
|
-
retired: "retired";
|
|
394
|
-
superseded: "superseded";
|
|
395
|
-
abandoned: "abandoned";
|
|
396
|
-
deferred: "deferred";
|
|
397
|
-
}> & {
|
|
398
|
-
is(value: unknown): value is "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred";
|
|
399
|
-
}>;
|
|
400
|
-
lifecycle: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
|
|
401
|
-
context: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
402
|
-
is(value: unknown): value is string | string[];
|
|
403
|
-
}>;
|
|
404
|
-
options: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
405
|
-
id: z.ZodString;
|
|
406
|
-
description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
407
|
-
is(value: unknown): value is string | string[];
|
|
408
|
-
};
|
|
335
|
+
doc: z.ZodObject<{
|
|
336
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
337
|
+
metadata: z.ZodOptional<z.ZodObject<{
|
|
338
|
+
title: z.ZodOptional<z.ZodString>;
|
|
339
|
+
doc_type: z.ZodOptional<z.ZodString>;
|
|
340
|
+
scope: z.ZodOptional<z.ZodString>;
|
|
341
|
+
status: z.ZodOptional<z.ZodString>;
|
|
342
|
+
version: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodInt]>>;
|
|
409
343
|
}, z.core.$loose> & {
|
|
410
344
|
is(value: unknown): value is {
|
|
411
345
|
[x: string]: unknown;
|
|
412
|
-
|
|
413
|
-
|
|
346
|
+
title?: string | undefined;
|
|
347
|
+
doc_type?: string | undefined;
|
|
348
|
+
scope?: string | undefined;
|
|
349
|
+
status?: string | undefined;
|
|
350
|
+
version?: string | number | undefined;
|
|
414
351
|
};
|
|
415
|
-
}>>;
|
|
416
|
-
selected: z.ZodOptional<z.ZodString>;
|
|
417
|
-
rationale: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
418
|
-
is(value: unknown): value is string | string[];
|
|
419
352
|
}>;
|
|
420
|
-
|
|
421
|
-
|
|
353
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
354
|
+
id: z.ZodString;
|
|
422
355
|
type: z.ZodEnum<{
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
356
|
+
intent: "intent";
|
|
357
|
+
concept: "concept";
|
|
358
|
+
capability: "capability";
|
|
359
|
+
element: "element";
|
|
360
|
+
realisation: "realisation";
|
|
361
|
+
invariant: "invariant";
|
|
362
|
+
principle: "principle";
|
|
363
|
+
policy: "policy";
|
|
364
|
+
protocol: "protocol";
|
|
365
|
+
stage: "stage";
|
|
366
|
+
role: "role";
|
|
367
|
+
gate: "gate";
|
|
368
|
+
mode: "mode";
|
|
369
|
+
artefact: "artefact";
|
|
370
|
+
artefact_flow: "artefact_flow";
|
|
371
|
+
decision: "decision";
|
|
372
|
+
change: "change";
|
|
373
|
+
view: "view";
|
|
374
|
+
milestone: "milestone";
|
|
375
|
+
version: "version";
|
|
376
|
+
}> & {
|
|
377
|
+
is(value: unknown): value is "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
|
|
378
|
+
};
|
|
379
|
+
name: z.ZodString;
|
|
429
380
|
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
430
381
|
is(value: unknown): value is string | string[];
|
|
431
382
|
}>;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
383
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
384
|
+
deprecated: "deprecated";
|
|
385
|
+
proposed: "proposed";
|
|
386
|
+
accepted: "accepted";
|
|
387
|
+
active: "active";
|
|
388
|
+
implemented: "implemented";
|
|
389
|
+
adopted: "adopted";
|
|
390
|
+
defined: "defined";
|
|
391
|
+
introduced: "introduced";
|
|
392
|
+
in_progress: "in_progress";
|
|
393
|
+
complete: "complete";
|
|
394
|
+
consolidated: "consolidated";
|
|
395
|
+
experimental: "experimental";
|
|
396
|
+
retired: "retired";
|
|
397
|
+
superseded: "superseded";
|
|
398
|
+
abandoned: "abandoned";
|
|
399
|
+
deferred: "deferred";
|
|
400
|
+
}> & {
|
|
401
|
+
is(value: unknown): value is "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred";
|
|
402
|
+
}>;
|
|
403
|
+
lifecycle: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
|
|
404
|
+
context: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
442
405
|
is(value: unknown): value is string | string[];
|
|
406
|
+
}>;
|
|
407
|
+
options: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
408
|
+
id: z.ZodString;
|
|
409
|
+
description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
410
|
+
is(value: unknown): value is string | string[];
|
|
411
|
+
};
|
|
412
|
+
}, z.core.$loose> & {
|
|
413
|
+
is(value: unknown): value is {
|
|
414
|
+
[x: string]: unknown;
|
|
415
|
+
id: string;
|
|
416
|
+
description: string | string[];
|
|
417
|
+
};
|
|
418
|
+
}>>;
|
|
419
|
+
selected: z.ZodOptional<z.ZodString>;
|
|
420
|
+
rationale: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
421
|
+
is(value: unknown): value is string | string[];
|
|
422
|
+
}>;
|
|
423
|
+
scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
424
|
+
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
425
|
+
type: z.ZodEnum<{
|
|
426
|
+
link: "link";
|
|
427
|
+
add: "add";
|
|
428
|
+
update: "update";
|
|
429
|
+
remove: "remove";
|
|
430
|
+
}>;
|
|
431
|
+
target: z.ZodOptional<z.ZodString>;
|
|
432
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
433
|
+
is(value: unknown): value is string | string[];
|
|
434
|
+
}>;
|
|
435
|
+
}, z.core.$loose> & {
|
|
436
|
+
is(value: unknown): value is {
|
|
437
|
+
[x: string]: unknown;
|
|
438
|
+
type: "link" | "add" | "update" | "remove";
|
|
439
|
+
target?: string | undefined;
|
|
440
|
+
description?: string | string[] | undefined;
|
|
441
|
+
};
|
|
442
|
+
}>>;
|
|
443
|
+
plan: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
444
|
+
description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
445
|
+
is(value: unknown): value is string | string[];
|
|
446
|
+
};
|
|
447
|
+
done: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
448
|
+
}, z.core.$loose> & {
|
|
449
|
+
is(value: unknown): value is {
|
|
450
|
+
[x: string]: unknown;
|
|
451
|
+
description: string | string[];
|
|
452
|
+
done?: boolean | undefined;
|
|
453
|
+
};
|
|
454
|
+
}>>;
|
|
455
|
+
propagation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
456
|
+
includes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
457
|
+
input: z.ZodOptional<z.ZodString>;
|
|
458
|
+
output: z.ZodOptional<z.ZodString>;
|
|
459
|
+
external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
460
|
+
role: z.ZodEnum<{
|
|
461
|
+
output: "output";
|
|
462
|
+
input: "input";
|
|
463
|
+
context: "context";
|
|
464
|
+
evidence: "evidence";
|
|
465
|
+
source: "source";
|
|
466
|
+
standard: "standard";
|
|
467
|
+
prior_art: "prior_art";
|
|
468
|
+
}> & {
|
|
469
|
+
is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
|
|
470
|
+
};
|
|
471
|
+
identifier: z.ZodString;
|
|
472
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
473
|
+
is(value: unknown): value is string | string[];
|
|
474
|
+
}>;
|
|
475
|
+
node_id: z.ZodOptional<z.ZodString>;
|
|
476
|
+
internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
477
|
+
is(value: unknown): value is string | string[];
|
|
478
|
+
}>;
|
|
479
|
+
}, z.core.$strip> & {
|
|
480
|
+
is(value: unknown): value is {
|
|
481
|
+
role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
|
|
482
|
+
identifier: string;
|
|
483
|
+
description?: string | string[] | undefined;
|
|
484
|
+
node_id?: string | undefined;
|
|
485
|
+
internalised?: string | string[] | undefined;
|
|
486
|
+
};
|
|
487
|
+
}>>;
|
|
488
|
+
readonly subsystem: z.ZodOptional<z.ZodObject</*elided*/ any, z.core.$strip>>;
|
|
489
|
+
}, z.core.$loose>>;
|
|
490
|
+
relationships: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
491
|
+
from: z.ZodString;
|
|
492
|
+
to: z.ZodString;
|
|
493
|
+
type: z.ZodEnum<{
|
|
494
|
+
refines: "refines";
|
|
495
|
+
realises: "realises";
|
|
496
|
+
implements: "implements";
|
|
497
|
+
depends_on: "depends_on";
|
|
498
|
+
constrained_by: "constrained_by";
|
|
499
|
+
affects: "affects";
|
|
500
|
+
supersedes: "supersedes";
|
|
501
|
+
must_preserve: "must_preserve";
|
|
502
|
+
performs: "performs";
|
|
503
|
+
part_of: "part_of";
|
|
504
|
+
precedes: "precedes";
|
|
505
|
+
must_follow: "must_follow";
|
|
506
|
+
blocks: "blocks";
|
|
507
|
+
routes_to: "routes_to";
|
|
508
|
+
governed_by: "governed_by";
|
|
509
|
+
modifies: "modifies";
|
|
510
|
+
triggered_by: "triggered_by";
|
|
511
|
+
applies_to: "applies_to";
|
|
512
|
+
produces: "produces";
|
|
513
|
+
consumes: "consumes";
|
|
514
|
+
transforms_into: "transforms_into";
|
|
515
|
+
selects: "selects";
|
|
516
|
+
requires: "requires";
|
|
517
|
+
disables: "disables";
|
|
518
|
+
}> & {
|
|
519
|
+
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";
|
|
443
520
|
};
|
|
444
|
-
|
|
521
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
522
|
+
is(value: unknown): value is string | string[];
|
|
523
|
+
}>;
|
|
445
524
|
}, z.core.$loose> & {
|
|
446
525
|
is(value: unknown): value is {
|
|
447
526
|
[x: string]: unknown;
|
|
448
|
-
|
|
449
|
-
|
|
527
|
+
from: string;
|
|
528
|
+
to: string;
|
|
529
|
+
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";
|
|
530
|
+
description?: string | string[] | undefined;
|
|
450
531
|
};
|
|
451
532
|
}>>;
|
|
452
|
-
propagation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
453
|
-
includes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
454
|
-
input: z.ZodOptional<z.ZodString>;
|
|
455
|
-
output: z.ZodOptional<z.ZodString>;
|
|
456
533
|
external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
457
534
|
role: z.ZodEnum<{
|
|
458
535
|
output: "output";
|
|
@@ -482,114 +559,65 @@ export declare const removeRelationshipOp: import("./define-operation.js").Defin
|
|
|
482
559
|
internalised?: string | string[] | undefined;
|
|
483
560
|
};
|
|
484
561
|
}>>;
|
|
485
|
-
readonly subsystem: z.ZodOptional<z.ZodObject</*elided*/ any, z.core.$strip>>;
|
|
486
|
-
}, z.core.$loose>>;
|
|
487
|
-
relationships: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
488
|
-
from: z.ZodString;
|
|
489
|
-
to: z.ZodString;
|
|
490
|
-
type: z.ZodEnum<{
|
|
491
|
-
refines: "refines";
|
|
492
|
-
realises: "realises";
|
|
493
|
-
implements: "implements";
|
|
494
|
-
depends_on: "depends_on";
|
|
495
|
-
constrained_by: "constrained_by";
|
|
496
|
-
affects: "affects";
|
|
497
|
-
supersedes: "supersedes";
|
|
498
|
-
must_preserve: "must_preserve";
|
|
499
|
-
performs: "performs";
|
|
500
|
-
part_of: "part_of";
|
|
501
|
-
precedes: "precedes";
|
|
502
|
-
must_follow: "must_follow";
|
|
503
|
-
blocks: "blocks";
|
|
504
|
-
routes_to: "routes_to";
|
|
505
|
-
governed_by: "governed_by";
|
|
506
|
-
modifies: "modifies";
|
|
507
|
-
triggered_by: "triggered_by";
|
|
508
|
-
applies_to: "applies_to";
|
|
509
|
-
produces: "produces";
|
|
510
|
-
consumes: "consumes";
|
|
511
|
-
transforms_into: "transforms_into";
|
|
512
|
-
selects: "selects";
|
|
513
|
-
requires: "requires";
|
|
514
|
-
disables: "disables";
|
|
515
|
-
}> & {
|
|
516
|
-
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";
|
|
517
|
-
};
|
|
518
|
-
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
519
|
-
is(value: unknown): value is string | string[];
|
|
520
|
-
}>;
|
|
521
|
-
}, z.core.$loose> & {
|
|
522
|
-
is(value: unknown): value is {
|
|
523
|
-
[x: string]: unknown;
|
|
524
|
-
from: string;
|
|
525
|
-
to: string;
|
|
526
|
-
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";
|
|
527
|
-
description?: string | string[] | undefined;
|
|
528
|
-
};
|
|
529
|
-
}>>;
|
|
530
|
-
external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
531
|
-
role: z.ZodEnum<{
|
|
532
|
-
output: "output";
|
|
533
|
-
input: "input";
|
|
534
|
-
context: "context";
|
|
535
|
-
evidence: "evidence";
|
|
536
|
-
source: "source";
|
|
537
|
-
standard: "standard";
|
|
538
|
-
prior_art: "prior_art";
|
|
539
|
-
}> & {
|
|
540
|
-
is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
|
|
541
|
-
};
|
|
542
|
-
identifier: z.ZodString;
|
|
543
|
-
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
544
|
-
is(value: unknown): value is string | string[];
|
|
545
|
-
}>;
|
|
546
|
-
node_id: z.ZodOptional<z.ZodString>;
|
|
547
|
-
internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
|
|
548
|
-
is(value: unknown): value is string | string[];
|
|
549
|
-
}>;
|
|
550
562
|
}, z.core.$strip> & {
|
|
551
563
|
is(value: unknown): value is {
|
|
552
|
-
|
|
553
|
-
identifier: string;
|
|
554
|
-
description?: string | string[] | undefined;
|
|
555
|
-
node_id?: string | undefined;
|
|
556
|
-
internalised?: string | string[] | undefined;
|
|
557
|
-
};
|
|
558
|
-
}>>;
|
|
559
|
-
}, z.core.$strip> & {
|
|
560
|
-
is(value: unknown): value is {
|
|
561
|
-
nodes: {
|
|
562
|
-
[x: string]: unknown;
|
|
563
|
-
id: string;
|
|
564
|
-
type: "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
|
|
565
|
-
name: string;
|
|
566
|
-
description?: string | string[] | undefined;
|
|
567
|
-
status?: "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred" | undefined;
|
|
568
|
-
lifecycle?: Record<string, string | boolean> | undefined;
|
|
569
|
-
context?: string | string[] | undefined;
|
|
570
|
-
options?: {
|
|
564
|
+
nodes: {
|
|
571
565
|
[x: string]: unknown;
|
|
572
566
|
id: string;
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
selected?: string | undefined;
|
|
576
|
-
rationale?: string | string[] | undefined;
|
|
577
|
-
scope?: string[] | undefined;
|
|
578
|
-
operations?: {
|
|
579
|
-
[x: string]: unknown;
|
|
580
|
-
type: "link" | "add" | "update" | "remove";
|
|
581
|
-
target?: string | undefined;
|
|
567
|
+
type: "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
|
|
568
|
+
name: string;
|
|
582
569
|
description?: string | string[] | undefined;
|
|
583
|
-
|
|
584
|
-
|
|
570
|
+
status?: "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred" | undefined;
|
|
571
|
+
lifecycle?: Record<string, string | boolean> | undefined;
|
|
572
|
+
context?: string | string[] | undefined;
|
|
573
|
+
options?: {
|
|
574
|
+
[x: string]: unknown;
|
|
575
|
+
id: string;
|
|
576
|
+
description: string | string[];
|
|
577
|
+
}[] | undefined;
|
|
578
|
+
selected?: string | undefined;
|
|
579
|
+
rationale?: string | string[] | undefined;
|
|
580
|
+
scope?: string[] | undefined;
|
|
581
|
+
operations?: {
|
|
582
|
+
[x: string]: unknown;
|
|
583
|
+
type: "link" | "add" | "update" | "remove";
|
|
584
|
+
target?: string | undefined;
|
|
585
|
+
description?: string | string[] | undefined;
|
|
586
|
+
}[] | undefined;
|
|
587
|
+
plan?: {
|
|
588
|
+
[x: string]: unknown;
|
|
589
|
+
description: string | string[];
|
|
590
|
+
done?: boolean | undefined;
|
|
591
|
+
}[] | undefined;
|
|
592
|
+
propagation?: Record<string, boolean> | undefined;
|
|
593
|
+
includes?: string[] | undefined;
|
|
594
|
+
input?: string | undefined;
|
|
595
|
+
output?: string | undefined;
|
|
596
|
+
external_references?: {
|
|
597
|
+
role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
|
|
598
|
+
identifier: string;
|
|
599
|
+
description?: string | string[] | undefined;
|
|
600
|
+
node_id?: string | undefined;
|
|
601
|
+
internalised?: string | string[] | undefined;
|
|
602
|
+
}[] | undefined;
|
|
603
|
+
subsystem?: /*elided*/ any | undefined;
|
|
604
|
+
}[];
|
|
605
|
+
$schema?: string | undefined;
|
|
606
|
+
metadata?: {
|
|
607
|
+
[x: string]: unknown;
|
|
608
|
+
title?: string | undefined;
|
|
609
|
+
doc_type?: string | undefined;
|
|
610
|
+
scope?: string | undefined;
|
|
611
|
+
status?: string | undefined;
|
|
612
|
+
version?: string | number | undefined;
|
|
613
|
+
} | undefined;
|
|
614
|
+
relationships?: {
|
|
585
615
|
[x: string]: unknown;
|
|
586
|
-
|
|
587
|
-
|
|
616
|
+
from: string;
|
|
617
|
+
to: string;
|
|
618
|
+
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";
|
|
619
|
+
description?: string | string[] | undefined;
|
|
588
620
|
}[] | undefined;
|
|
589
|
-
propagation?: Record<string, boolean> | undefined;
|
|
590
|
-
includes?: string[] | undefined;
|
|
591
|
-
input?: string | undefined;
|
|
592
|
-
output?: string | undefined;
|
|
593
621
|
external_references?: {
|
|
594
622
|
role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
|
|
595
623
|
identifier: string;
|
|
@@ -597,30 +625,7 @@ export declare const removeRelationshipOp: import("./define-operation.js").Defin
|
|
|
597
625
|
node_id?: string | undefined;
|
|
598
626
|
internalised?: string | string[] | undefined;
|
|
599
627
|
}[] | undefined;
|
|
600
|
-
|
|
601
|
-
}[];
|
|
602
|
-
$schema?: string | undefined;
|
|
603
|
-
metadata?: {
|
|
604
|
-
[x: string]: unknown;
|
|
605
|
-
title?: string | undefined;
|
|
606
|
-
doc_type?: string | undefined;
|
|
607
|
-
scope?: string | undefined;
|
|
608
|
-
status?: string | undefined;
|
|
609
|
-
version?: string | number | undefined;
|
|
610
|
-
} | undefined;
|
|
611
|
-
relationships?: {
|
|
612
|
-
[x: string]: unknown;
|
|
613
|
-
from: string;
|
|
614
|
-
to: string;
|
|
615
|
-
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";
|
|
616
|
-
description?: string | string[] | undefined;
|
|
617
|
-
}[] | undefined;
|
|
618
|
-
external_references?: {
|
|
619
|
-
role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
|
|
620
|
-
identifier: string;
|
|
621
|
-
description?: string | string[] | undefined;
|
|
622
|
-
node_id?: string | undefined;
|
|
623
|
-
internalised?: string | string[] | undefined;
|
|
624
|
-
}[] | undefined;
|
|
628
|
+
};
|
|
625
629
|
};
|
|
626
|
-
|
|
630
|
+
warnings: z.ZodArray<z.ZodString>;
|
|
631
|
+
}, z.core.$strip>>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { defineOperation } from "./define-operation.js";
|
|
3
3
|
import { SysProMDocument, RelationshipType } from "../schema.js";
|
|
4
|
+
import { RemoveResult } from "./remove-node.js";
|
|
4
5
|
/**
|
|
5
6
|
* Remove a relationship matching from, type, and to. Returns a new document without it.
|
|
7
|
+
* Optionally repairs must_follow chains when removal would break them.
|
|
6
8
|
* @throws {Error} If no matching relationship is found.
|
|
7
9
|
*/
|
|
8
10
|
export const removeRelationshipOp = defineOperation({
|
|
@@ -13,18 +15,67 @@ export const removeRelationshipOp = defineOperation({
|
|
|
13
15
|
from: z.string(),
|
|
14
16
|
type: RelationshipType,
|
|
15
17
|
to: z.string(),
|
|
18
|
+
repair: z.boolean().optional(),
|
|
16
19
|
}),
|
|
17
|
-
output:
|
|
18
|
-
fn({ doc, from, type, to }) {
|
|
20
|
+
output: RemoveResult,
|
|
21
|
+
fn({ doc, from, type, to, repair }) {
|
|
19
22
|
const rels = doc.relationships ?? [];
|
|
20
23
|
const idx = rels.findIndex((r) => r.from === from && r.type === type && r.to === to);
|
|
21
24
|
if (idx === -1) {
|
|
22
25
|
throw new Error(`Relationship not found: ${from} ${type} ${to}`);
|
|
23
26
|
}
|
|
24
|
-
const
|
|
27
|
+
const warnings = [];
|
|
28
|
+
// If repair flag is set and this is a must_follow relationship, prepare repairs first
|
|
29
|
+
const repairs = [];
|
|
30
|
+
if (repair && type === "must_follow") {
|
|
31
|
+
// Find all must_follow relationships from the 'to' node (outgoing)
|
|
32
|
+
const outgoing = rels.filter((r) => r.from === to && r.type === "must_follow");
|
|
33
|
+
// If there are relationships following the target, repair by connecting the source directly
|
|
34
|
+
if (outgoing.length > 0) {
|
|
35
|
+
// Also find all must_follow relationships pointing to the 'from' node (for multi-chain repair)
|
|
36
|
+
const incoming = rels.filter((r) => r.to === from && r.type === "must_follow");
|
|
37
|
+
// If there are incoming relationships, connect them all to outgoing targets
|
|
38
|
+
if (incoming.length > 0) {
|
|
39
|
+
for (const inc of incoming) {
|
|
40
|
+
for (const out of outgoing) {
|
|
41
|
+
const bridgeExists = rels.some((r) => r.from === inc.from &&
|
|
42
|
+
r.to === out.to &&
|
|
43
|
+
r.type === "must_follow");
|
|
44
|
+
if (!bridgeExists) {
|
|
45
|
+
repairs.push({
|
|
46
|
+
from: inc.from,
|
|
47
|
+
to: out.to,
|
|
48
|
+
type: "must_follow",
|
|
49
|
+
});
|
|
50
|
+
warnings.push(`Repaired chain: ${inc.from} → ${out.to}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Always connect the source node to outgoing targets
|
|
56
|
+
for (const out of outgoing) {
|
|
57
|
+
const bridgeExists = rels.some((r) => r.from === from && r.to === out.to && r.type === "must_follow");
|
|
58
|
+
if (!bridgeExists) {
|
|
59
|
+
repairs.push({
|
|
60
|
+
from: from,
|
|
61
|
+
to: out.to,
|
|
62
|
+
type: "must_follow",
|
|
63
|
+
});
|
|
64
|
+
warnings.push(`Repaired chain: ${from} → ${out.to}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Remove the specified relationship and add repairs
|
|
70
|
+
const newRelationships = rels
|
|
71
|
+
.filter((r) => !(r.from === from && r.type === type && r.to === to))
|
|
72
|
+
.concat(repairs);
|
|
25
73
|
return {
|
|
26
|
-
|
|
27
|
-
|
|
74
|
+
doc: {
|
|
75
|
+
...doc,
|
|
76
|
+
relationships: newRelationships.length > 0 ? newRelationships : undefined,
|
|
77
|
+
},
|
|
78
|
+
warnings,
|
|
28
79
|
};
|
|
29
80
|
},
|
|
30
81
|
});
|
|
@@ -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")) {
|