sysprom 1.4.0 → 1.5.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.
@@ -7,9 +7,9 @@ declare const optsSchema: z.ZodObject<{
7
7
  title: z.ZodOptional<z.ZodString>;
8
8
  scope: z.ZodOptional<z.ZodString>;
9
9
  format: z.ZodOptional<z.ZodEnum<{
10
- dir: "dir";
11
10
  json: "json";
12
11
  md: "md";
12
+ dir: "dir";
13
13
  }>>;
14
14
  }, z.core.$strict>;
15
15
  export declare const initCommand: CommandDef<typeof argsSchema, typeof optsSchema>;
@@ -32,6 +32,7 @@ export { graphOp } from "./graph.js";
32
32
  export { renameOp } from "./rename.js";
33
33
  export { jsonToMarkdownOp } from "./json-to-markdown.js";
34
34
  export { markdownToJsonOp } from "./markdown-to-json.js";
35
+ export { syncDocumentsOp, type BidirectionalSyncResult, type ConflictStrategy, } from "./sync.js";
35
36
  export { speckitImportOp } from "./speckit-import.js";
36
37
  export { speckitExportOp } from "./speckit-export.js";
37
38
  export { speckitSyncOp, type SyncResult } from "./speckit-sync.js";
@@ -38,6 +38,8 @@ export { renameOp } from "./rename.js";
38
38
  // Conversion operations
39
39
  export { jsonToMarkdownOp } from "./json-to-markdown.js";
40
40
  export { markdownToJsonOp } from "./markdown-to-json.js";
41
+ // Synchronisation operations
42
+ export { syncDocumentsOp, } from "./sync.js";
41
43
  // Spec-Kit interoperability operations
42
44
  export { speckitImportOp } from "./speckit-import.js";
43
45
  export { speckitExportOp } from "./speckit-export.js";
@@ -0,0 +1,945 @@
1
+ import * as z from "zod";
2
+ import { SysProMDocument } from "../schema.js";
3
+ /**
4
+ * Conflict resolution strategy when both documents have diverged.
5
+ * - 'json': prefer the JSON document as source of truth
6
+ * - 'md': prefer the Markdown-parsed document as source of truth
7
+ * - 'interactive': (not yet supported) prompt user per-conflict
8
+ * - 'report': return summary without resolving
9
+ */
10
+ export declare const ConflictStrategy: z.ZodEnum<{
11
+ json: "json";
12
+ md: "md";
13
+ interactive: "interactive";
14
+ report: "report";
15
+ }>;
16
+ /** Type for conflict resolution strategies. */
17
+ export type ConflictStrategy = z.infer<typeof ConflictStrategy>;
18
+ /**
19
+ * Result of synchronisation between two documents.
20
+ * @property synced The resolved document after applying sync strategy
21
+ * @property jsonChanged True if JSON side was newer/modified
22
+ * @property mdChanged True if MD side was newer/modified
23
+ * @property conflict True if both sides diverged
24
+ * @property strategy The strategy applied to resolve any conflicts
25
+ * @property changedNodes IDs of nodes that differ between JSON and MD
26
+ */
27
+ export interface BidirectionalSyncResult {
28
+ synced: SysProMDocument;
29
+ jsonChanged: boolean;
30
+ mdChanged: boolean;
31
+ conflict: boolean;
32
+ strategy: ConflictStrategy;
33
+ changedNodes: string[];
34
+ }
35
+ /**
36
+ * Synchronise two SysProM document representations.
37
+ * Implements bidirectional sync with configurable conflict resolution.
38
+ * @throws {Error} If conflict resolution is impossible (both changed with 'report' strategy)
39
+ */
40
+ export declare const syncDocumentsOp: import("./define-operation.js").DefinedOperation<z.ZodObject<{
41
+ jsonDoc: z.ZodObject<{
42
+ $schema: z.ZodOptional<z.ZodString>;
43
+ metadata: z.ZodOptional<z.ZodObject<{
44
+ title: z.ZodOptional<z.ZodString>;
45
+ doc_type: z.ZodOptional<z.ZodString>;
46
+ scope: z.ZodOptional<z.ZodString>;
47
+ status: z.ZodOptional<z.ZodString>;
48
+ version: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodInt]>>;
49
+ }, z.core.$loose> & {
50
+ is(value: unknown): value is {
51
+ [x: string]: unknown;
52
+ title?: string | undefined;
53
+ doc_type?: string | undefined;
54
+ scope?: string | undefined;
55
+ status?: string | undefined;
56
+ version?: string | number | undefined;
57
+ };
58
+ }>;
59
+ nodes: z.ZodArray<z.ZodObject<{
60
+ id: z.ZodString;
61
+ type: z.ZodEnum<{
62
+ intent: "intent";
63
+ concept: "concept";
64
+ capability: "capability";
65
+ element: "element";
66
+ realisation: "realisation";
67
+ invariant: "invariant";
68
+ principle: "principle";
69
+ policy: "policy";
70
+ protocol: "protocol";
71
+ stage: "stage";
72
+ role: "role";
73
+ gate: "gate";
74
+ mode: "mode";
75
+ artefact: "artefact";
76
+ artefact_flow: "artefact_flow";
77
+ decision: "decision";
78
+ change: "change";
79
+ view: "view";
80
+ milestone: "milestone";
81
+ version: "version";
82
+ }> & {
83
+ 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";
84
+ };
85
+ name: z.ZodString;
86
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
87
+ is(value: unknown): value is string | string[];
88
+ }>;
89
+ status: z.ZodOptional<z.ZodEnum<{
90
+ deprecated: "deprecated";
91
+ proposed: "proposed";
92
+ accepted: "accepted";
93
+ active: "active";
94
+ implemented: "implemented";
95
+ adopted: "adopted";
96
+ defined: "defined";
97
+ introduced: "introduced";
98
+ in_progress: "in_progress";
99
+ complete: "complete";
100
+ consolidated: "consolidated";
101
+ experimental: "experimental";
102
+ retired: "retired";
103
+ superseded: "superseded";
104
+ abandoned: "abandoned";
105
+ deferred: "deferred";
106
+ }> & {
107
+ is(value: unknown): value is "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred";
108
+ }>;
109
+ lifecycle: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
110
+ context: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
111
+ is(value: unknown): value is string | string[];
112
+ }>;
113
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
114
+ id: z.ZodString;
115
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
116
+ is(value: unknown): value is string | string[];
117
+ };
118
+ }, z.core.$loose> & {
119
+ is(value: unknown): value is {
120
+ [x: string]: unknown;
121
+ id: string;
122
+ description: string | string[];
123
+ };
124
+ }>>;
125
+ selected: z.ZodOptional<z.ZodString>;
126
+ rationale: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
127
+ is(value: unknown): value is string | string[];
128
+ }>;
129
+ scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
130
+ operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
131
+ type: z.ZodEnum<{
132
+ link: "link";
133
+ add: "add";
134
+ update: "update";
135
+ remove: "remove";
136
+ }>;
137
+ target: z.ZodOptional<z.ZodString>;
138
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
139
+ is(value: unknown): value is string | string[];
140
+ }>;
141
+ }, z.core.$loose> & {
142
+ is(value: unknown): value is {
143
+ [x: string]: unknown;
144
+ type: "link" | "add" | "update" | "remove";
145
+ target?: string | undefined;
146
+ description?: string | string[] | undefined;
147
+ };
148
+ }>>;
149
+ plan: z.ZodOptional<z.ZodArray<z.ZodObject<{
150
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
151
+ is(value: unknown): value is string | string[];
152
+ };
153
+ done: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
154
+ }, z.core.$loose> & {
155
+ is(value: unknown): value is {
156
+ [x: string]: unknown;
157
+ description: string | string[];
158
+ done?: boolean | undefined;
159
+ };
160
+ }>>;
161
+ propagation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
162
+ includes: z.ZodOptional<z.ZodArray<z.ZodString>>;
163
+ input: z.ZodOptional<z.ZodString>;
164
+ output: z.ZodOptional<z.ZodString>;
165
+ external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
166
+ role: z.ZodEnum<{
167
+ output: "output";
168
+ input: "input";
169
+ context: "context";
170
+ evidence: "evidence";
171
+ source: "source";
172
+ standard: "standard";
173
+ prior_art: "prior_art";
174
+ }> & {
175
+ is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
176
+ };
177
+ identifier: z.ZodString;
178
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
179
+ is(value: unknown): value is string | string[];
180
+ }>;
181
+ node_id: z.ZodOptional<z.ZodString>;
182
+ internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
183
+ is(value: unknown): value is string | string[];
184
+ }>;
185
+ }, z.core.$strip> & {
186
+ is(value: unknown): value is {
187
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
188
+ identifier: string;
189
+ description?: string | string[] | undefined;
190
+ node_id?: string | undefined;
191
+ internalised?: string | string[] | undefined;
192
+ };
193
+ }>>;
194
+ readonly subsystem: z.ZodOptional<z.ZodObject</*elided*/ any, z.core.$strip>>;
195
+ }, z.core.$loose>>;
196
+ relationships: z.ZodOptional<z.ZodArray<z.ZodObject<{
197
+ from: z.ZodString;
198
+ to: z.ZodString;
199
+ type: z.ZodEnum<{
200
+ refines: "refines";
201
+ realises: "realises";
202
+ implements: "implements";
203
+ depends_on: "depends_on";
204
+ constrained_by: "constrained_by";
205
+ affects: "affects";
206
+ supersedes: "supersedes";
207
+ must_preserve: "must_preserve";
208
+ performs: "performs";
209
+ part_of: "part_of";
210
+ precedes: "precedes";
211
+ must_follow: "must_follow";
212
+ blocks: "blocks";
213
+ routes_to: "routes_to";
214
+ governed_by: "governed_by";
215
+ modifies: "modifies";
216
+ triggered_by: "triggered_by";
217
+ applies_to: "applies_to";
218
+ produces: "produces";
219
+ consumes: "consumes";
220
+ transforms_into: "transforms_into";
221
+ selects: "selects";
222
+ requires: "requires";
223
+ disables: "disables";
224
+ }> & {
225
+ 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";
226
+ };
227
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
228
+ is(value: unknown): value is string | string[];
229
+ }>;
230
+ }, z.core.$loose> & {
231
+ is(value: unknown): value is {
232
+ [x: string]: unknown;
233
+ from: string;
234
+ to: string;
235
+ 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";
236
+ description?: string | string[] | undefined;
237
+ };
238
+ }>>;
239
+ external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
240
+ role: z.ZodEnum<{
241
+ output: "output";
242
+ input: "input";
243
+ context: "context";
244
+ evidence: "evidence";
245
+ source: "source";
246
+ standard: "standard";
247
+ prior_art: "prior_art";
248
+ }> & {
249
+ is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
250
+ };
251
+ identifier: z.ZodString;
252
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
253
+ is(value: unknown): value is string | string[];
254
+ }>;
255
+ node_id: z.ZodOptional<z.ZodString>;
256
+ internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
257
+ is(value: unknown): value is string | string[];
258
+ }>;
259
+ }, z.core.$strip> & {
260
+ is(value: unknown): value is {
261
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
262
+ identifier: string;
263
+ description?: string | string[] | undefined;
264
+ node_id?: string | undefined;
265
+ internalised?: string | string[] | undefined;
266
+ };
267
+ }>>;
268
+ }, z.core.$strip> & {
269
+ is(value: unknown): value is {
270
+ nodes: {
271
+ [x: string]: unknown;
272
+ id: string;
273
+ type: "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
274
+ name: string;
275
+ description?: string | string[] | undefined;
276
+ status?: "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred" | undefined;
277
+ lifecycle?: Record<string, string | boolean> | undefined;
278
+ context?: string | string[] | undefined;
279
+ options?: {
280
+ [x: string]: unknown;
281
+ id: string;
282
+ description: string | string[];
283
+ }[] | undefined;
284
+ selected?: string | undefined;
285
+ rationale?: string | string[] | undefined;
286
+ scope?: string[] | undefined;
287
+ operations?: {
288
+ [x: string]: unknown;
289
+ type: "link" | "add" | "update" | "remove";
290
+ target?: string | undefined;
291
+ description?: string | string[] | undefined;
292
+ }[] | undefined;
293
+ plan?: {
294
+ [x: string]: unknown;
295
+ description: string | string[];
296
+ done?: boolean | undefined;
297
+ }[] | undefined;
298
+ propagation?: Record<string, boolean> | undefined;
299
+ includes?: string[] | undefined;
300
+ input?: string | undefined;
301
+ output?: string | undefined;
302
+ external_references?: {
303
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
304
+ identifier: string;
305
+ description?: string | string[] | undefined;
306
+ node_id?: string | undefined;
307
+ internalised?: string | string[] | undefined;
308
+ }[] | undefined;
309
+ subsystem?: /*elided*/ any | undefined;
310
+ }[];
311
+ $schema?: string | undefined;
312
+ metadata?: {
313
+ [x: string]: unknown;
314
+ title?: string | undefined;
315
+ doc_type?: string | undefined;
316
+ scope?: string | undefined;
317
+ status?: string | undefined;
318
+ version?: string | number | undefined;
319
+ } | undefined;
320
+ relationships?: {
321
+ [x: string]: unknown;
322
+ from: string;
323
+ to: string;
324
+ 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";
325
+ description?: string | string[] | undefined;
326
+ }[] | undefined;
327
+ external_references?: {
328
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
329
+ identifier: string;
330
+ description?: string | string[] | undefined;
331
+ node_id?: string | undefined;
332
+ internalised?: string | string[] | undefined;
333
+ }[] | undefined;
334
+ };
335
+ };
336
+ mdDoc: z.ZodObject<{
337
+ $schema: z.ZodOptional<z.ZodString>;
338
+ metadata: z.ZodOptional<z.ZodObject<{
339
+ title: z.ZodOptional<z.ZodString>;
340
+ doc_type: z.ZodOptional<z.ZodString>;
341
+ scope: z.ZodOptional<z.ZodString>;
342
+ status: z.ZodOptional<z.ZodString>;
343
+ version: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodInt]>>;
344
+ }, z.core.$loose> & {
345
+ is(value: unknown): value is {
346
+ [x: string]: unknown;
347
+ title?: string | undefined;
348
+ doc_type?: string | undefined;
349
+ scope?: string | undefined;
350
+ status?: string | undefined;
351
+ version?: string | number | undefined;
352
+ };
353
+ }>;
354
+ nodes: z.ZodArray<z.ZodObject<{
355
+ id: z.ZodString;
356
+ type: z.ZodEnum<{
357
+ intent: "intent";
358
+ concept: "concept";
359
+ capability: "capability";
360
+ element: "element";
361
+ realisation: "realisation";
362
+ invariant: "invariant";
363
+ principle: "principle";
364
+ policy: "policy";
365
+ protocol: "protocol";
366
+ stage: "stage";
367
+ role: "role";
368
+ gate: "gate";
369
+ mode: "mode";
370
+ artefact: "artefact";
371
+ artefact_flow: "artefact_flow";
372
+ decision: "decision";
373
+ change: "change";
374
+ view: "view";
375
+ milestone: "milestone";
376
+ version: "version";
377
+ }> & {
378
+ 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";
379
+ };
380
+ name: z.ZodString;
381
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
382
+ is(value: unknown): value is string | string[];
383
+ }>;
384
+ status: z.ZodOptional<z.ZodEnum<{
385
+ deprecated: "deprecated";
386
+ proposed: "proposed";
387
+ accepted: "accepted";
388
+ active: "active";
389
+ implemented: "implemented";
390
+ adopted: "adopted";
391
+ defined: "defined";
392
+ introduced: "introduced";
393
+ in_progress: "in_progress";
394
+ complete: "complete";
395
+ consolidated: "consolidated";
396
+ experimental: "experimental";
397
+ retired: "retired";
398
+ superseded: "superseded";
399
+ abandoned: "abandoned";
400
+ deferred: "deferred";
401
+ }> & {
402
+ is(value: unknown): value is "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred";
403
+ }>;
404
+ lifecycle: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
405
+ context: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
406
+ is(value: unknown): value is string | string[];
407
+ }>;
408
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
409
+ id: z.ZodString;
410
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
411
+ is(value: unknown): value is string | string[];
412
+ };
413
+ }, z.core.$loose> & {
414
+ is(value: unknown): value is {
415
+ [x: string]: unknown;
416
+ id: string;
417
+ description: string | string[];
418
+ };
419
+ }>>;
420
+ selected: z.ZodOptional<z.ZodString>;
421
+ rationale: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
422
+ is(value: unknown): value is string | string[];
423
+ }>;
424
+ scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
425
+ operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
426
+ type: z.ZodEnum<{
427
+ link: "link";
428
+ add: "add";
429
+ update: "update";
430
+ remove: "remove";
431
+ }>;
432
+ target: z.ZodOptional<z.ZodString>;
433
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
434
+ is(value: unknown): value is string | string[];
435
+ }>;
436
+ }, z.core.$loose> & {
437
+ is(value: unknown): value is {
438
+ [x: string]: unknown;
439
+ type: "link" | "add" | "update" | "remove";
440
+ target?: string | undefined;
441
+ description?: string | string[] | undefined;
442
+ };
443
+ }>>;
444
+ plan: z.ZodOptional<z.ZodArray<z.ZodObject<{
445
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
446
+ is(value: unknown): value is string | string[];
447
+ };
448
+ done: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
449
+ }, z.core.$loose> & {
450
+ is(value: unknown): value is {
451
+ [x: string]: unknown;
452
+ description: string | string[];
453
+ done?: boolean | undefined;
454
+ };
455
+ }>>;
456
+ propagation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
457
+ includes: z.ZodOptional<z.ZodArray<z.ZodString>>;
458
+ input: z.ZodOptional<z.ZodString>;
459
+ output: z.ZodOptional<z.ZodString>;
460
+ external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
461
+ role: z.ZodEnum<{
462
+ output: "output";
463
+ input: "input";
464
+ context: "context";
465
+ evidence: "evidence";
466
+ source: "source";
467
+ standard: "standard";
468
+ prior_art: "prior_art";
469
+ }> & {
470
+ is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
471
+ };
472
+ identifier: z.ZodString;
473
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
474
+ is(value: unknown): value is string | string[];
475
+ }>;
476
+ node_id: z.ZodOptional<z.ZodString>;
477
+ internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
478
+ is(value: unknown): value is string | string[];
479
+ }>;
480
+ }, z.core.$strip> & {
481
+ is(value: unknown): value is {
482
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
483
+ identifier: string;
484
+ description?: string | string[] | undefined;
485
+ node_id?: string | undefined;
486
+ internalised?: string | string[] | undefined;
487
+ };
488
+ }>>;
489
+ readonly subsystem: z.ZodOptional<z.ZodObject</*elided*/ any, z.core.$strip>>;
490
+ }, z.core.$loose>>;
491
+ relationships: z.ZodOptional<z.ZodArray<z.ZodObject<{
492
+ from: z.ZodString;
493
+ to: z.ZodString;
494
+ type: z.ZodEnum<{
495
+ refines: "refines";
496
+ realises: "realises";
497
+ implements: "implements";
498
+ depends_on: "depends_on";
499
+ constrained_by: "constrained_by";
500
+ affects: "affects";
501
+ supersedes: "supersedes";
502
+ must_preserve: "must_preserve";
503
+ performs: "performs";
504
+ part_of: "part_of";
505
+ precedes: "precedes";
506
+ must_follow: "must_follow";
507
+ blocks: "blocks";
508
+ routes_to: "routes_to";
509
+ governed_by: "governed_by";
510
+ modifies: "modifies";
511
+ triggered_by: "triggered_by";
512
+ applies_to: "applies_to";
513
+ produces: "produces";
514
+ consumes: "consumes";
515
+ transforms_into: "transforms_into";
516
+ selects: "selects";
517
+ requires: "requires";
518
+ disables: "disables";
519
+ }> & {
520
+ 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";
521
+ };
522
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
523
+ is(value: unknown): value is string | string[];
524
+ }>;
525
+ }, z.core.$loose> & {
526
+ is(value: unknown): value is {
527
+ [x: string]: unknown;
528
+ from: string;
529
+ to: string;
530
+ 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";
531
+ description?: string | string[] | undefined;
532
+ };
533
+ }>>;
534
+ external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
535
+ role: z.ZodEnum<{
536
+ output: "output";
537
+ input: "input";
538
+ context: "context";
539
+ evidence: "evidence";
540
+ source: "source";
541
+ standard: "standard";
542
+ prior_art: "prior_art";
543
+ }> & {
544
+ is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
545
+ };
546
+ identifier: z.ZodString;
547
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
548
+ is(value: unknown): value is string | string[];
549
+ }>;
550
+ node_id: z.ZodOptional<z.ZodString>;
551
+ internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
552
+ is(value: unknown): value is string | string[];
553
+ }>;
554
+ }, z.core.$strip> & {
555
+ is(value: unknown): value is {
556
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
557
+ identifier: string;
558
+ description?: string | string[] | undefined;
559
+ node_id?: string | undefined;
560
+ internalised?: string | string[] | undefined;
561
+ };
562
+ }>>;
563
+ }, z.core.$strip> & {
564
+ is(value: unknown): value is {
565
+ nodes: {
566
+ [x: string]: unknown;
567
+ id: string;
568
+ type: "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
569
+ name: string;
570
+ description?: string | string[] | undefined;
571
+ status?: "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred" | undefined;
572
+ lifecycle?: Record<string, string | boolean> | undefined;
573
+ context?: string | string[] | undefined;
574
+ options?: {
575
+ [x: string]: unknown;
576
+ id: string;
577
+ description: string | string[];
578
+ }[] | undefined;
579
+ selected?: string | undefined;
580
+ rationale?: string | string[] | undefined;
581
+ scope?: string[] | undefined;
582
+ operations?: {
583
+ [x: string]: unknown;
584
+ type: "link" | "add" | "update" | "remove";
585
+ target?: string | undefined;
586
+ description?: string | string[] | undefined;
587
+ }[] | undefined;
588
+ plan?: {
589
+ [x: string]: unknown;
590
+ description: string | string[];
591
+ done?: boolean | undefined;
592
+ }[] | undefined;
593
+ propagation?: Record<string, boolean> | undefined;
594
+ includes?: string[] | undefined;
595
+ input?: string | undefined;
596
+ output?: string | undefined;
597
+ external_references?: {
598
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
599
+ identifier: string;
600
+ description?: string | string[] | undefined;
601
+ node_id?: string | undefined;
602
+ internalised?: string | string[] | undefined;
603
+ }[] | undefined;
604
+ subsystem?: /*elided*/ any | undefined;
605
+ }[];
606
+ $schema?: string | undefined;
607
+ metadata?: {
608
+ [x: string]: unknown;
609
+ title?: string | undefined;
610
+ doc_type?: string | undefined;
611
+ scope?: string | undefined;
612
+ status?: string | undefined;
613
+ version?: string | number | undefined;
614
+ } | undefined;
615
+ relationships?: {
616
+ [x: string]: unknown;
617
+ from: string;
618
+ to: string;
619
+ 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";
620
+ description?: string | string[] | undefined;
621
+ }[] | undefined;
622
+ external_references?: {
623
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
624
+ identifier: string;
625
+ description?: string | string[] | undefined;
626
+ node_id?: string | undefined;
627
+ internalised?: string | string[] | undefined;
628
+ }[] | undefined;
629
+ };
630
+ };
631
+ jsonChanged: z.ZodBoolean;
632
+ mdChanged: z.ZodBoolean;
633
+ strategy: z.ZodOptional<z.ZodEnum<{
634
+ json: "json";
635
+ md: "md";
636
+ interactive: "interactive";
637
+ report: "report";
638
+ }>>;
639
+ }, z.core.$strip>, z.ZodObject<{
640
+ synced: z.ZodObject<{
641
+ $schema: z.ZodOptional<z.ZodString>;
642
+ metadata: z.ZodOptional<z.ZodObject<{
643
+ title: z.ZodOptional<z.ZodString>;
644
+ doc_type: z.ZodOptional<z.ZodString>;
645
+ scope: z.ZodOptional<z.ZodString>;
646
+ status: z.ZodOptional<z.ZodString>;
647
+ version: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodInt]>>;
648
+ }, z.core.$loose> & {
649
+ is(value: unknown): value is {
650
+ [x: string]: unknown;
651
+ title?: string | undefined;
652
+ doc_type?: string | undefined;
653
+ scope?: string | undefined;
654
+ status?: string | undefined;
655
+ version?: string | number | undefined;
656
+ };
657
+ }>;
658
+ nodes: z.ZodArray<z.ZodObject<{
659
+ id: z.ZodString;
660
+ type: z.ZodEnum<{
661
+ intent: "intent";
662
+ concept: "concept";
663
+ capability: "capability";
664
+ element: "element";
665
+ realisation: "realisation";
666
+ invariant: "invariant";
667
+ principle: "principle";
668
+ policy: "policy";
669
+ protocol: "protocol";
670
+ stage: "stage";
671
+ role: "role";
672
+ gate: "gate";
673
+ mode: "mode";
674
+ artefact: "artefact";
675
+ artefact_flow: "artefact_flow";
676
+ decision: "decision";
677
+ change: "change";
678
+ view: "view";
679
+ milestone: "milestone";
680
+ version: "version";
681
+ }> & {
682
+ 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";
683
+ };
684
+ name: z.ZodString;
685
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
686
+ is(value: unknown): value is string | string[];
687
+ }>;
688
+ status: z.ZodOptional<z.ZodEnum<{
689
+ deprecated: "deprecated";
690
+ proposed: "proposed";
691
+ accepted: "accepted";
692
+ active: "active";
693
+ implemented: "implemented";
694
+ adopted: "adopted";
695
+ defined: "defined";
696
+ introduced: "introduced";
697
+ in_progress: "in_progress";
698
+ complete: "complete";
699
+ consolidated: "consolidated";
700
+ experimental: "experimental";
701
+ retired: "retired";
702
+ superseded: "superseded";
703
+ abandoned: "abandoned";
704
+ deferred: "deferred";
705
+ }> & {
706
+ is(value: unknown): value is "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred";
707
+ }>;
708
+ lifecycle: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
709
+ context: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
710
+ is(value: unknown): value is string | string[];
711
+ }>;
712
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
713
+ id: z.ZodString;
714
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
715
+ is(value: unknown): value is string | string[];
716
+ };
717
+ }, z.core.$loose> & {
718
+ is(value: unknown): value is {
719
+ [x: string]: unknown;
720
+ id: string;
721
+ description: string | string[];
722
+ };
723
+ }>>;
724
+ selected: z.ZodOptional<z.ZodString>;
725
+ rationale: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
726
+ is(value: unknown): value is string | string[];
727
+ }>;
728
+ scope: z.ZodOptional<z.ZodArray<z.ZodString>>;
729
+ operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
730
+ type: z.ZodEnum<{
731
+ link: "link";
732
+ add: "add";
733
+ update: "update";
734
+ remove: "remove";
735
+ }>;
736
+ target: z.ZodOptional<z.ZodString>;
737
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
738
+ is(value: unknown): value is string | string[];
739
+ }>;
740
+ }, z.core.$loose> & {
741
+ is(value: unknown): value is {
742
+ [x: string]: unknown;
743
+ type: "link" | "add" | "update" | "remove";
744
+ target?: string | undefined;
745
+ description?: string | string[] | undefined;
746
+ };
747
+ }>>;
748
+ plan: z.ZodOptional<z.ZodArray<z.ZodObject<{
749
+ description: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
750
+ is(value: unknown): value is string | string[];
751
+ };
752
+ done: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
753
+ }, z.core.$loose> & {
754
+ is(value: unknown): value is {
755
+ [x: string]: unknown;
756
+ description: string | string[];
757
+ done?: boolean | undefined;
758
+ };
759
+ }>>;
760
+ propagation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
761
+ includes: z.ZodOptional<z.ZodArray<z.ZodString>>;
762
+ input: z.ZodOptional<z.ZodString>;
763
+ output: z.ZodOptional<z.ZodString>;
764
+ external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
765
+ role: z.ZodEnum<{
766
+ output: "output";
767
+ input: "input";
768
+ context: "context";
769
+ evidence: "evidence";
770
+ source: "source";
771
+ standard: "standard";
772
+ prior_art: "prior_art";
773
+ }> & {
774
+ is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
775
+ };
776
+ identifier: z.ZodString;
777
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
778
+ is(value: unknown): value is string | string[];
779
+ }>;
780
+ node_id: z.ZodOptional<z.ZodString>;
781
+ internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
782
+ is(value: unknown): value is string | string[];
783
+ }>;
784
+ }, z.core.$strip> & {
785
+ is(value: unknown): value is {
786
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
787
+ identifier: string;
788
+ description?: string | string[] | undefined;
789
+ node_id?: string | undefined;
790
+ internalised?: string | string[] | undefined;
791
+ };
792
+ }>>;
793
+ readonly subsystem: z.ZodOptional<z.ZodObject</*elided*/ any, z.core.$strip>>;
794
+ }, z.core.$loose>>;
795
+ relationships: z.ZodOptional<z.ZodArray<z.ZodObject<{
796
+ from: z.ZodString;
797
+ to: z.ZodString;
798
+ type: z.ZodEnum<{
799
+ refines: "refines";
800
+ realises: "realises";
801
+ implements: "implements";
802
+ depends_on: "depends_on";
803
+ constrained_by: "constrained_by";
804
+ affects: "affects";
805
+ supersedes: "supersedes";
806
+ must_preserve: "must_preserve";
807
+ performs: "performs";
808
+ part_of: "part_of";
809
+ precedes: "precedes";
810
+ must_follow: "must_follow";
811
+ blocks: "blocks";
812
+ routes_to: "routes_to";
813
+ governed_by: "governed_by";
814
+ modifies: "modifies";
815
+ triggered_by: "triggered_by";
816
+ applies_to: "applies_to";
817
+ produces: "produces";
818
+ consumes: "consumes";
819
+ transforms_into: "transforms_into";
820
+ selects: "selects";
821
+ requires: "requires";
822
+ disables: "disables";
823
+ }> & {
824
+ 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";
825
+ };
826
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
827
+ is(value: unknown): value is string | string[];
828
+ }>;
829
+ }, z.core.$loose> & {
830
+ is(value: unknown): value is {
831
+ [x: string]: unknown;
832
+ from: string;
833
+ to: string;
834
+ 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";
835
+ description?: string | string[] | undefined;
836
+ };
837
+ }>>;
838
+ external_references: z.ZodOptional<z.ZodArray<z.ZodObject<{
839
+ role: z.ZodEnum<{
840
+ output: "output";
841
+ input: "input";
842
+ context: "context";
843
+ evidence: "evidence";
844
+ source: "source";
845
+ standard: "standard";
846
+ prior_art: "prior_art";
847
+ }> & {
848
+ is(value: unknown): value is "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
849
+ };
850
+ identifier: z.ZodString;
851
+ description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
852
+ is(value: unknown): value is string | string[];
853
+ }>;
854
+ node_id: z.ZodOptional<z.ZodString>;
855
+ internalised: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]> & {
856
+ is(value: unknown): value is string | string[];
857
+ }>;
858
+ }, z.core.$strip> & {
859
+ is(value: unknown): value is {
860
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
861
+ identifier: string;
862
+ description?: string | string[] | undefined;
863
+ node_id?: string | undefined;
864
+ internalised?: string | string[] | undefined;
865
+ };
866
+ }>>;
867
+ }, z.core.$strip> & {
868
+ is(value: unknown): value is {
869
+ nodes: {
870
+ [x: string]: unknown;
871
+ id: string;
872
+ type: "intent" | "concept" | "capability" | "element" | "realisation" | "invariant" | "principle" | "policy" | "protocol" | "stage" | "role" | "gate" | "mode" | "artefact" | "artefact_flow" | "decision" | "change" | "view" | "milestone" | "version";
873
+ name: string;
874
+ description?: string | string[] | undefined;
875
+ status?: "deprecated" | "proposed" | "accepted" | "active" | "implemented" | "adopted" | "defined" | "introduced" | "in_progress" | "complete" | "consolidated" | "experimental" | "retired" | "superseded" | "abandoned" | "deferred" | undefined;
876
+ lifecycle?: Record<string, string | boolean> | undefined;
877
+ context?: string | string[] | undefined;
878
+ options?: {
879
+ [x: string]: unknown;
880
+ id: string;
881
+ description: string | string[];
882
+ }[] | undefined;
883
+ selected?: string | undefined;
884
+ rationale?: string | string[] | undefined;
885
+ scope?: string[] | undefined;
886
+ operations?: {
887
+ [x: string]: unknown;
888
+ type: "link" | "add" | "update" | "remove";
889
+ target?: string | undefined;
890
+ description?: string | string[] | undefined;
891
+ }[] | undefined;
892
+ plan?: {
893
+ [x: string]: unknown;
894
+ description: string | string[];
895
+ done?: boolean | undefined;
896
+ }[] | undefined;
897
+ propagation?: Record<string, boolean> | undefined;
898
+ includes?: string[] | undefined;
899
+ input?: string | undefined;
900
+ output?: string | undefined;
901
+ external_references?: {
902
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
903
+ identifier: string;
904
+ description?: string | string[] | undefined;
905
+ node_id?: string | undefined;
906
+ internalised?: string | string[] | undefined;
907
+ }[] | undefined;
908
+ subsystem?: /*elided*/ any | undefined;
909
+ }[];
910
+ $schema?: string | undefined;
911
+ metadata?: {
912
+ [x: string]: unknown;
913
+ title?: string | undefined;
914
+ doc_type?: string | undefined;
915
+ scope?: string | undefined;
916
+ status?: string | undefined;
917
+ version?: string | number | undefined;
918
+ } | undefined;
919
+ relationships?: {
920
+ [x: string]: unknown;
921
+ from: string;
922
+ to: string;
923
+ 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";
924
+ description?: string | string[] | undefined;
925
+ }[] | undefined;
926
+ external_references?: {
927
+ role: "output" | "input" | "context" | "evidence" | "source" | "standard" | "prior_art";
928
+ identifier: string;
929
+ description?: string | string[] | undefined;
930
+ node_id?: string | undefined;
931
+ internalised?: string | string[] | undefined;
932
+ }[] | undefined;
933
+ };
934
+ };
935
+ jsonChanged: z.ZodBoolean;
936
+ mdChanged: z.ZodBoolean;
937
+ conflict: z.ZodBoolean;
938
+ strategy: z.ZodEnum<{
939
+ json: "json";
940
+ md: "md";
941
+ interactive: "interactive";
942
+ report: "report";
943
+ }>;
944
+ changedNodes: z.ZodArray<z.ZodString>;
945
+ }, z.core.$strip>>;
@@ -0,0 +1,123 @@
1
+ import * as z from "zod";
2
+ import { defineOperation } from "./define-operation.js";
3
+ import { SysProMDocument } from "../schema.js";
4
+ /**
5
+ * Conflict resolution strategy when both documents have diverged.
6
+ * - 'json': prefer the JSON document as source of truth
7
+ * - 'md': prefer the Markdown-parsed document as source of truth
8
+ * - 'interactive': (not yet supported) prompt user per-conflict
9
+ * - 'report': return summary without resolving
10
+ */
11
+ export const ConflictStrategy = z.enum(["json", "md", "interactive", "report"]);
12
+ const BidirectionalSyncResultSchema = z.object({
13
+ synced: SysProMDocument,
14
+ jsonChanged: z.boolean(),
15
+ mdChanged: z.boolean(),
16
+ conflict: z.boolean(),
17
+ strategy: ConflictStrategy,
18
+ changedNodes: z.array(z.string()),
19
+ });
20
+ /**
21
+ * Synchronise two SysProM document representations.
22
+ * Implements bidirectional sync with configurable conflict resolution.
23
+ * @throws {Error} If conflict resolution is impossible (both changed with 'report' strategy)
24
+ */
25
+ export const syncDocumentsOp = defineOperation({
26
+ name: "syncDocuments",
27
+ description: "Synchronise two SysProM document representations (JSON and Markdown-parsed) with conflict resolution strategies.",
28
+ input: z.object({
29
+ jsonDoc: SysProMDocument,
30
+ mdDoc: SysProMDocument,
31
+ jsonChanged: z.boolean(),
32
+ mdChanged: z.boolean(),
33
+ strategy: ConflictStrategy.optional(),
34
+ }),
35
+ output: BidirectionalSyncResultSchema,
36
+ fn(params) {
37
+ const { jsonDoc, mdDoc, jsonChanged, mdChanged, strategy = "json", } = params;
38
+ // Identify which nodes differ
39
+ const jsonNodeMap = new Map(jsonDoc.nodes.map((n) => [n.id, n]));
40
+ const mdNodeMap = new Map(mdDoc.nodes.map((n) => [n.id, n]));
41
+ const changedNodes = [];
42
+ for (const node of jsonDoc.nodes) {
43
+ const mdNode = mdNodeMap.get(node.id);
44
+ if (!mdNode || JSON.stringify(node) !== JSON.stringify(mdNode)) {
45
+ changedNodes.push(node.id);
46
+ }
47
+ }
48
+ for (const node of mdDoc.nodes) {
49
+ if (!jsonNodeMap.has(node.id)) {
50
+ changedNodes.push(node.id);
51
+ }
52
+ }
53
+ // Determine sync strategy and result
54
+ if (!jsonChanged && !mdChanged) {
55
+ // No changes, return either document
56
+ return {
57
+ synced: jsonDoc,
58
+ jsonChanged: false,
59
+ mdChanged: false,
60
+ conflict: false,
61
+ strategy: strategy,
62
+ changedNodes: [],
63
+ };
64
+ }
65
+ if (jsonChanged && !mdChanged) {
66
+ // JSON is the modified source, return it as-is
67
+ return {
68
+ synced: jsonDoc,
69
+ jsonChanged: true,
70
+ mdChanged: false,
71
+ conflict: false,
72
+ strategy: strategy,
73
+ changedNodes,
74
+ };
75
+ }
76
+ if (!jsonChanged && mdChanged) {
77
+ // MD is the modified source, return it
78
+ return {
79
+ synced: mdDoc,
80
+ jsonChanged: false,
81
+ mdChanged: true,
82
+ conflict: false,
83
+ strategy: strategy,
84
+ changedNodes,
85
+ };
86
+ }
87
+ // Both have changed — apply conflict strategy
88
+ switch (strategy) {
89
+ case "json": {
90
+ return {
91
+ synced: jsonDoc,
92
+ jsonChanged: true,
93
+ mdChanged: true,
94
+ conflict: true,
95
+ strategy: "json",
96
+ changedNodes,
97
+ };
98
+ }
99
+ case "md": {
100
+ return {
101
+ synced: mdDoc,
102
+ jsonChanged: true,
103
+ mdChanged: true,
104
+ conflict: true,
105
+ strategy: "md",
106
+ changedNodes,
107
+ };
108
+ }
109
+ case "report": {
110
+ const nodeCountStr = String(changedNodes.length);
111
+ throw new Error(`Conflict detected: both JSON and Markdown have diverged (${nodeCountStr} nodes differ). Use --prefer-json or --prefer-md to resolve.`);
112
+ }
113
+ case "interactive": {
114
+ throw new Error("Interactive conflict resolution not yet implemented");
115
+ }
116
+ default: {
117
+ // TypeScript exhaustiveness check
118
+ const _exhaustive = strategy;
119
+ return _exhaustive;
120
+ }
121
+ }
122
+ },
123
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Result of detecting changes between JSON and Markdown representations.
3
+ * @property jsonChanged True if JSON has changed since MD was created
4
+ * @property mdChanged True if Markdown has changed since JSON was created
5
+ * @property conflict True if both sides have diverged (both changed)
6
+ */
7
+ export interface DetectionResult {
8
+ jsonChanged: boolean;
9
+ mdChanged: boolean;
10
+ conflict: boolean;
11
+ }
12
+ /**
13
+ * Detect whether JSON and/or Markdown have changed.
14
+ * Strategy:
15
+ * 1. Parse both JSON and Markdown to document objects
16
+ * 2. If documents are identical → no change
17
+ * 3. If documents differ:
18
+ * - Use file modification times to determine which was edited more recently
19
+ * - The newer file is considered the "changed" one
20
+ * - If modification times are very close (< 100ms), treat as conflict
21
+ *
22
+ * @param jsonPath Path to JSON file
23
+ * @param mdPath Path to Markdown file (single or multi-doc)
24
+ * @returns Detection result with jsonChanged, mdChanged, and conflict flags
25
+ */
26
+ export declare function detectChanges(jsonPath: string, mdPath: string): DetectionResult;
@@ -0,0 +1,81 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { markdownToJson } from "./md-to-json.js";
4
+ import { SysProMDocument } from "./schema.js";
5
+ /**
6
+ * Compute a normalised hash of a document for comparison.
7
+ * Uses canonical JSON representation.
8
+ * @param doc The SysProM document
9
+ * @returns SHA256 hash of the canonicalised document
10
+ */
11
+ function normaliseHash(doc) {
12
+ const sorted = JSON.stringify(doc, Object.keys(doc).sort());
13
+ return createHash("sha256").update(sorted).digest("hex");
14
+ }
15
+ /**
16
+ * Detect whether JSON and/or Markdown have changed.
17
+ * Strategy:
18
+ * 1. Parse both JSON and Markdown to document objects
19
+ * 2. If documents are identical → no change
20
+ * 3. If documents differ:
21
+ * - Use file modification times to determine which was edited more recently
22
+ * - The newer file is considered the "changed" one
23
+ * - If modification times are very close (< 100ms), treat as conflict
24
+ *
25
+ * @param jsonPath Path to JSON file
26
+ * @param mdPath Path to Markdown file (single or multi-doc)
27
+ * @returns Detection result with jsonChanged, mdChanged, and conflict flags
28
+ */
29
+ export function detectChanges(jsonPath, mdPath) {
30
+ // Read files
31
+ const jsonContent = readFileSync(jsonPath, "utf8");
32
+ const mdContent = readFileSync(mdPath, "utf8");
33
+ // Parse JSON
34
+ const jsonDoc = JSON.parse(jsonContent);
35
+ if (!SysProMDocument.is(jsonDoc)) {
36
+ throw new Error("JSON file is not a valid SysProM document");
37
+ }
38
+ // Parse Markdown to document
39
+ const mdDoc = markdownToJson(mdPath);
40
+ // Compare parsed documents
41
+ const jsonHash = normaliseHash(jsonDoc);
42
+ const mdHash = normaliseHash(mdDoc);
43
+ // If both parse to identical documents, no changes on either side
44
+ if (jsonHash === mdHash) {
45
+ return {
46
+ jsonChanged: false,
47
+ mdChanged: false,
48
+ conflict: false,
49
+ };
50
+ }
51
+ // Parsed documents differ. Use file modification times to determine which changed.
52
+ // The file that was modified more recently is the one that diverged.
53
+ const jsonStats = statSync(jsonPath);
54
+ const mdStats = statSync(mdPath);
55
+ const timeDiff = Math.abs(jsonStats.mtimeMs - mdStats.mtimeMs);
56
+ // If modification times are very close (within 100ms), treat as simultaneous edit (conflict)
57
+ if (timeDiff < 100) {
58
+ return {
59
+ jsonChanged: true,
60
+ mdChanged: true,
61
+ conflict: true,
62
+ };
63
+ }
64
+ // Otherwise, determine which file is newer
65
+ if (jsonStats.mtimeMs > mdStats.mtimeMs) {
66
+ // JSON is newer → JSON was modified after MD
67
+ return {
68
+ jsonChanged: true,
69
+ mdChanged: false,
70
+ conflict: false,
71
+ };
72
+ }
73
+ else {
74
+ // MD is newer → MD was modified after JSON
75
+ return {
76
+ jsonChanged: false,
77
+ mdChanged: true,
78
+ conflict: false,
79
+ };
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sysprom",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "SysProM — System Provenance Model CLI and library",
5
5
  "author": "ExaDev",
6
6
  "homepage": "https://exadev.github.io/SysProM",