sysprom 1.3.0 → 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.
@@ -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 newDoc = removeRelationshipOp({
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(newDoc, loaded, opts);
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
  }
@@ -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>;
@@ -17,17 +17,71 @@ export const removeNodeOp = defineOperation({
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
- // Remove the node
29
- const newNodes = doc.nodes.filter((n) => n.id !== id);
30
- // Clean up all references to the removed node
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)
31
85
  const cleanedNodes = newNodes.map((n) => {
32
86
  let updated = n;
33
87
  // Remove from view includes
@@ -59,12 +113,6 @@ export const removeNodeOp = defineOperation({
59
113
  }
60
114
  return updated;
61
115
  });
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}`);
67
- }
68
116
  // Remove from external references
69
117
  const newExternalRefs = (doc.external_references ?? []).filter((ref) => ref.node_id !== id);
70
118
  return {
@@ -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
- $schema: z.ZodOptional<z.ZodString>;
334
- metadata: z.ZodOptional<z.ZodObject<{
335
- title: z.ZodOptional<z.ZodString>;
336
- doc_type: z.ZodOptional<z.ZodString>;
337
- scope: z.ZodOptional<z.ZodString>;
338
- status: z.ZodOptional<z.ZodString>;
339
- version: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodInt]>>;
340
- }, z.core.$loose> & {
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
- id: string;
413
- description: string | string[];
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
- scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
421
- operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
353
+ nodes: z.ZodArray<z.ZodObject<{
354
+ id: z.ZodString;
422
355
  type: z.ZodEnum<{
423
- link: "link";
424
- add: "add";
425
- update: "update";
426
- remove: "remove";
427
- }>;
428
- target: z.ZodOptional<z.ZodString>;
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
- }, z.core.$loose> & {
433
- is(value: unknown): value is {
434
- [x: string]: unknown;
435
- type: "link" | "add" | "update" | "remove";
436
- target?: string | undefined;
437
- description?: string | string[] | undefined;
438
- };
439
- }>>;
440
- plan: z.ZodOptional<z.ZodArray<z.ZodObject<{
441
- description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
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
- done: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
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
- description: string | string[];
449
- done?: boolean | undefined;
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
- role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
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
- description: string | string[];
574
- }[] | undefined;
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
- }[] | undefined;
584
- plan?: {
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
- description: string | string[];
587
- done?: boolean | undefined;
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
- subsystem?: /*elided*/ any | undefined;
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: SysProMDocument,
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 newRelationships = rels.filter((r) => !(r.from === from && r.type === type && r.to === to));
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
- ...doc,
27
- relationships: newRelationships.length > 0 ? newRelationships : undefined,
74
+ doc: {
75
+ ...doc,
76
+ relationships: newRelationships.length > 0 ? newRelationships : undefined,
77
+ },
78
+ warnings,
28
79
  };
29
80
  },
30
81
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sysprom",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "SysProM — System Provenance Model CLI and library",
5
5
  "author": "ExaDev",
6
6
  "homepage": "https://exadev.github.io/SysProM",