@miragon/team-topologies-schema-model 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,7 @@ relate to (spatial overlap, the Team Topologies convention) — there is no sour
43
43
  ```jsonc
44
44
  {
45
45
  "version": 3,
46
- "title": "Online shop — team topology",
46
+ "title": "Online shop team topology",
47
47
  "nodes": [
48
48
  {
49
49
  "id": "team_checkout",
@@ -146,24 +146,34 @@ console.log(SAMPLE_DOCUMENT.title);
146
146
 
147
147
  ### API surface
148
148
 
149
- | Export | Purpose |
150
- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
151
- | `TtDocument`, `TeamNode`, `InteractionShape`, `FlowShape`, `TextAnnotation`, `Position`, `Size` | The document model types. |
152
- | `TeamType`, `InteractionMode`, `TEAM_TYPES`, `INTERACTION_MODES` | The notation's discriminator unions + their value lists. |
153
- | `TEAM_TYPE_SPECS`, `INTERACTION_MODE_SPECS`, `FLOW_SPEC`, `ANNOTATION_SPEC`, `dashArray()` | The visual notation spec (shapes, colours, strokes) as data. |
154
- | `documentSchema`, `teamNodeSchema`, `interactionSchema`, `flowSchema`, `annotationSchema` | Zod schemas for each shape. |
155
- | `parseDocument()`, `ParseResult` | Validate **and migrate** unknown input into a `TtDocument`. |
156
- | `serializeDocument()`, `canonicalize()` | Deterministic JSON serialization (sorted, rounded, fixed key order). |
157
- | `emptyDocument()`, `createTeamNode()`, `createInteractionShape()`, `createFlowShape()`, `createTextAnnotation()`, `newId()` | Factories with notation defaults + id generation. |
158
- | `SAMPLE_DOCUMENT` | A complete example topology. |
159
- | `DOCUMENT_VERSION` | The current document version (`3`). |
149
+ | Export | Purpose |
150
+ | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
151
+ | `TtDocument`, `TeamNode`, `InteractionShape`, `FlowShape`, `TextAnnotation`, `Position`, `Size` | The document model types. |
152
+ | `TeamType`, `InteractionMode`, `TEAM_TYPES`, `INTERACTION_MODES` | The notation's discriminator unions + their value lists. |
153
+ | `TEAM_TYPE_SPECS`, `INTERACTION_MODE_SPECS`, `FLOW_SPEC`, `ANNOTATION_SPEC`, `dashArray()` | The visual notation spec (shapes, colours, strokes) as data. |
154
+ | `documentSchema`, `teamNodeSchema`, `interactionSchema`, `flowSchema`, `annotationSchema` | Zod schemas for each shape. |
155
+ | `parseDocument()`, `ParseResult`, `DocumentIssue` | Validate **and migrate** unknown input into a `TtDocument`; a failure lists each issue with its path. |
156
+ | `serializeDocument()`, `canonicalize()` | Deterministic JSON serialization (sorted, rounded, fixed key order). |
157
+ | `foreignFields()`, `withForeignFields()`, `DOCUMENT_FIELDS`, `TEAM_NODE_FIELDS`, … | The fields a document carries beyond the known ones. |
158
+ | `documentIdProblems()`, `DIAGRAM_ROOT_ID`, `associationId()` | The id rules (unique, not reserved). |
159
+ | `emptyDocument()`, `createTeamNode()`, `createInteractionShape()`, `createFlowShape()`, `createTextAnnotation()`, `newId()` | Factories with notation defaults + id generation. |
160
+ | `SAMPLE_DOCUMENT` | A complete example topology. |
161
+ | `DOCUMENT_VERSION` | The current document version (`3`). |
160
162
 
161
163
  ## Determinism & migrations
162
164
 
163
165
  - **Determinism** — `serializeDocument()` runs `canonicalize()` first: arrays are sorted by `id`,
164
166
  coordinates are rounded to two decimals, optional fields with no value are dropped, and keys are
165
167
  written in a fixed order. The same logical diagram always produces byte-identical JSON, which is
166
- what makes Git diffs small and share URLs stable.
168
+ what makes Git diffs small and share URLs stable. The pretty form (the default) is the file on
169
+ disk and ends with a line break; the compact form (`serializeDocument(doc, false)`) has none.
170
+ - **Unknown fields** — fields a document carries that this package does not know (a vendor's
171
+ `"x-vendor"` on the root or on an element) pass `parseDocument()` untouched and are written after
172
+ the known fields, sorted by name (nested keys too), so another tool's data survives a save.
173
+ - **Ids** — `parseDocument()` rejects a document whose ids would collide on the canvas: an id used
174
+ twice, the diagram's own id `tt-root` (`DIAGRAM_ROOT_ID`), ids starting with `__implicitroot`, and
175
+ `assoc_<annotation id>` (`associationId()`), the id of that annotation's connector.
176
+ `documentIdProblems()` lists them with their path.
167
177
  - **Migrations** — `parseDocument()` upgrades older documents before validating (keyed by `version`).
168
178
  The `1 → 2` step drops the legacy `showFlowOfChange` flag and the old edge-based interactions,
169
179
  keeping only interactions that carry geometry (the placed-shape model). The `2 → 3` step adds an
package/dist/index.cjs CHANGED
@@ -140,6 +140,52 @@ function dashArray(style, strokeWidth = 2) {
140
140
  }
141
141
  var ALL_TEAM_SPECS = Object.values(TEAM_TYPE_SPECS);
142
142
  var ALL_INTERACTION_SPECS = Object.values(INTERACTION_MODE_SPECS);
143
+
144
+ // src/ids.ts
145
+ var DIAGRAM_ROOT_ID = "tt-root";
146
+ var ENGINE_ROOT_ID_PREFIX = "__implicitroot";
147
+ function associationId(annotationId) {
148
+ return `assoc_${annotationId}`;
149
+ }
150
+ var COLLECTIONS = ["nodes", "interactions", "flows", "annotations"];
151
+ function documentIdProblems(doc) {
152
+ const problems = [];
153
+ const connectorIds = new Map(
154
+ doc.annotations.map((annotation) => [associationId(annotation.id), annotation.id])
155
+ );
156
+ const seen = /* @__PURE__ */ new Set();
157
+ for (const collection of COLLECTIONS) {
158
+ doc[collection].forEach((element, index) => {
159
+ const path = [collection, index, "id"];
160
+ const { id } = element;
161
+ if (seen.has(id)) {
162
+ problems.push({
163
+ path,
164
+ message: `duplicate id "${id}"; ids must be unique across nodes, interactions, flows and annotations`
165
+ });
166
+ }
167
+ seen.add(id);
168
+ if (id === DIAGRAM_ROOT_ID) {
169
+ problems.push({ path, message: `"${id}" is reserved for the diagram itself` });
170
+ } else if (id.startsWith(ENGINE_ROOT_ID_PREFIX)) {
171
+ problems.push({
172
+ path,
173
+ message: `ids starting with "${ENGINE_ROOT_ID_PREFIX}" are reserved`
174
+ });
175
+ }
176
+ const annotationId = connectorIds.get(id);
177
+ if (annotationId !== void 0) {
178
+ problems.push({
179
+ path,
180
+ message: `"${id}" is reserved for the connector of annotation "${annotationId}"`
181
+ });
182
+ }
183
+ });
184
+ }
185
+ return problems;
186
+ }
187
+
188
+ // src/schema.ts
143
189
  var teamTypeSchema = zod.z.enum(TEAM_TYPES);
144
190
  var interactionModeSchema = zod.z.enum(INTERACTION_MODES);
145
191
  var sizeSchema = zod.z.object({
@@ -151,7 +197,7 @@ var positionSchema = zod.z.object({
151
197
  y: zod.z.number().finite()
152
198
  });
153
199
  var hexColor = zod.z.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "expected a hex colour");
154
- var teamNodeSchema = zod.z.object({
200
+ var teamNodeSchema = zod.z.looseObject({
155
201
  id: zod.z.string().min(1),
156
202
  type: teamTypeSchema,
157
203
  label: zod.z.string(),
@@ -161,7 +207,7 @@ var teamNodeSchema = zod.z.object({
161
207
  fill: hexColor.optional(),
162
208
  stroke: hexColor.optional()
163
209
  });
164
- var interactionSchema = zod.z.object({
210
+ var interactionSchema = zod.z.looseObject({
165
211
  id: zod.z.string().min(1),
166
212
  mode: interactionModeSchema,
167
213
  label: zod.z.string().optional(),
@@ -170,34 +216,43 @@ var interactionSchema = zod.z.object({
170
216
  fill: hexColor.optional(),
171
217
  stroke: hexColor.optional()
172
218
  });
173
- var flowSchema = zod.z.object({
219
+ var flowSchema = zod.z.looseObject({
174
220
  id: zod.z.string().min(1),
175
221
  label: zod.z.string().optional(),
176
222
  position: positionSchema,
177
223
  size: sizeSchema
178
224
  });
179
- var annotationSchema = zod.z.object({
225
+ var annotationSchema = zod.z.looseObject({
180
226
  id: zod.z.string().min(1),
181
227
  text: zod.z.string(),
182
228
  position: positionSchema,
183
229
  size: sizeSchema,
184
230
  attachedTo: zod.z.string().min(1).optional()
185
231
  });
186
- var documentSchema = zod.z.object({
232
+ var documentSchema = zod.z.looseObject({
187
233
  version: zod.z.literal(DOCUMENT_VERSION),
188
234
  title: zod.z.string(),
189
235
  nodes: zod.z.array(teamNodeSchema),
190
236
  interactions: zod.z.array(interactionSchema),
191
237
  flows: zod.z.array(flowSchema),
192
238
  annotations: zod.z.array(annotationSchema)
239
+ }).superRefine((document, context) => {
240
+ for (const { path, message } of documentIdProblems(document)) {
241
+ context.addIssue({ code: "custom", path, message });
242
+ }
193
243
  });
194
244
  function parseDocument(input) {
195
245
  const migrated = migrate(input);
196
246
  const result = documentSchema.safeParse(migrated);
197
247
  if (!result.success) {
248
+ const issues = result.error.issues.map((issue) => ({
249
+ path: issue.path.filter((key) => typeof key !== "symbol"),
250
+ message: issue.message
251
+ }));
198
252
  return {
199
253
  ok: false,
200
- error: result.error.issues.map((i2) => `${i2.path.join(".") || "(root)"}: ${i2.message}`).join("; ")
254
+ error: issues.map((i2) => `${i2.path.join(".") || "(root)"}: ${i2.message}`).join("; "),
255
+ issues
201
256
  };
202
257
  }
203
258
  return { ok: true, document: result.data };
@@ -218,6 +273,56 @@ function migrate(input) {
218
273
  return next;
219
274
  }
220
275
 
276
+ // src/foreign.ts
277
+ var DOCUMENT_FIELDS = [
278
+ "version",
279
+ "title",
280
+ "nodes",
281
+ "interactions",
282
+ "flows",
283
+ "annotations"
284
+ ];
285
+ var TEAM_NODE_FIELDS = [
286
+ "id",
287
+ "type",
288
+ "label",
289
+ "description",
290
+ "position",
291
+ "size",
292
+ "fill",
293
+ "stroke"
294
+ ];
295
+ var INTERACTION_FIELDS = [
296
+ "id",
297
+ "mode",
298
+ "label",
299
+ "position",
300
+ "size",
301
+ "fill",
302
+ "stroke"
303
+ ];
304
+ var FLOW_FIELDS = ["id", "label", "position", "size"];
305
+ var ANNOTATION_FIELDS = ["id", "text", "position", "size", "attachedTo"];
306
+ function canonicalValue(value) {
307
+ if (Array.isArray(value)) return value.map(canonicalValue);
308
+ if (value === null || typeof value !== "object") return value;
309
+ const record = value;
310
+ return Object.fromEntries(
311
+ Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => [key, canonicalValue(record[key])])
312
+ );
313
+ }
314
+ function foreignFields(value, knownFields) {
315
+ const record = value;
316
+ const names = Object.keys(record).filter((name) => !knownFields.includes(name) && record[name] !== void 0).sort();
317
+ if (names.length === 0) return void 0;
318
+ return Object.fromEntries(names.map((name) => [name, canonicalValue(record[name])]));
319
+ }
320
+ function withForeignFields(known, source, knownFields) {
321
+ const foreign = source ? foreignFields(source, knownFields) : void 0;
322
+ if (!foreign) return known;
323
+ return Object.fromEntries([...Object.entries(known), ...Object.entries(foreign)]);
324
+ }
325
+
221
326
  // src/serialize.ts
222
327
  function round(n) {
223
328
  return Math.round(n * 100) / 100;
@@ -239,7 +344,7 @@ function normNode(n) {
239
344
  if (n.description) out.description = n.description;
240
345
  if (n.fill) out.fill = n.fill;
241
346
  if (n.stroke) out.stroke = n.stroke;
242
- return out;
347
+ return withForeignFields(out, n, TEAM_NODE_FIELDS);
243
348
  }
244
349
  function normInteraction(e) {
245
350
  const out = {
@@ -251,7 +356,7 @@ function normInteraction(e) {
251
356
  if (e.label) out.label = e.label;
252
357
  if (e.fill) out.fill = e.fill;
253
358
  if (e.stroke) out.stroke = e.stroke;
254
- return out;
359
+ return withForeignFields(out, e, INTERACTION_FIELDS);
255
360
  }
256
361
  function normFlow(f) {
257
362
  const out = {
@@ -260,7 +365,7 @@ function normFlow(f) {
260
365
  size: normSize(f.size)
261
366
  };
262
367
  if (f.label) out.label = f.label;
263
- return out;
368
+ return withForeignFields(out, f, FLOW_FIELDS);
264
369
  }
265
370
  function normAnnotation(a) {
266
371
  const out = {
@@ -270,23 +375,28 @@ function normAnnotation(a) {
270
375
  size: normSize(a.size)
271
376
  };
272
377
  if (a.attachedTo) out.attachedTo = a.attachedTo;
273
- return out;
378
+ return withForeignFields(out, a, ANNOTATION_FIELDS);
274
379
  }
275
380
  var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
276
381
  function canonicalize(doc) {
277
- return {
278
- version: DOCUMENT_VERSION,
279
- title: doc.title,
280
- nodes: [...doc.nodes].map(normNode).sort(byId),
281
- interactions: [...doc.interactions].map(normInteraction).sort(byId),
282
- flows: [...doc.flows].map(normFlow).sort(byId),
283
- annotations: [...doc.annotations].map(normAnnotation).sort(byId)
284
- };
382
+ return withForeignFields(
383
+ {
384
+ version: DOCUMENT_VERSION,
385
+ title: doc.title,
386
+ nodes: [...doc.nodes].map(normNode).sort(byId),
387
+ interactions: [...doc.interactions].map(normInteraction).sort(byId),
388
+ flows: [...doc.flows].map(normFlow).sort(byId),
389
+ annotations: [...doc.annotations].map(normAnnotation).sort(byId)
390
+ },
391
+ doc,
392
+ DOCUMENT_FIELDS
393
+ );
285
394
  }
286
395
  function serializeDocument(doc, pretty = true) {
287
- const { annotations, ...canonical } = canonicalize(doc);
288
- const output = annotations.length > 0 ? { ...canonical, annotations } : canonical;
289
- return JSON.stringify(output, null, pretty ? 2 : 0);
396
+ const canonical = canonicalize(doc);
397
+ const output = canonical.annotations.length > 0 ? canonical : Object.fromEntries(Object.entries(canonical).filter(([key]) => key !== "annotations"));
398
+ return pretty ? `${JSON.stringify(output, null, 2)}
399
+ ` : JSON.stringify(output);
290
400
  }
291
401
  function newId(prefix) {
292
402
  return `${prefix}_${nanoid.nanoid(8)}`;
@@ -347,40 +457,49 @@ function emptyDocument(title = "Untitled team topology") {
347
457
  // src/sample.ts
348
458
  var s = TEAM_TYPE_SPECS;
349
459
  var i = INTERACTION_MODE_SPECS;
460
+ var LANE_WIDTH = 600;
350
461
  var SAMPLE_DOCUMENT = {
351
462
  version: DOCUMENT_VERSION,
352
- title: "Online shop \u2014 team topology",
463
+ title: "Online shop team topology",
353
464
  nodes: [
354
465
  {
355
466
  id: "team_test_automation",
356
467
  type: "enabling",
357
468
  label: "Test Automation",
358
469
  description: "Coaches stream-aligned teams in test automation, then steps back.",
359
- position: { x: 60, y: 150 },
360
- size: { ...s.enabling.defaultSize }
470
+ position: { x: 40, y: 150 },
471
+ size: { width: s.enabling.defaultSize.width, height: 222 }
361
472
  },
362
473
  {
363
474
  id: "team_discovery",
364
475
  type: "stream-aligned",
365
476
  label: "Product Discovery",
366
477
  description: "Owns search, browsing and product pages end-to-end.",
367
- position: { x: 240, y: 202 },
368
- size: { ...s["stream-aligned"].defaultSize }
478
+ position: { x: 220, y: 150 },
479
+ size: { width: LANE_WIDTH, height: s["stream-aligned"].defaultSize.height }
369
480
  },
370
481
  {
371
482
  id: "team_checkout",
372
483
  type: "stream-aligned",
373
484
  label: "Checkout & Payments",
374
485
  description: "Owns the checkout and payment journey end-to-end.",
375
- position: { x: 560, y: 202 },
376
- size: { ...s["stream-aligned"].defaultSize }
486
+ position: { x: 220, y: 276 },
487
+ size: { width: LANE_WIDTH, height: s["stream-aligned"].defaultSize.height }
488
+ },
489
+ {
490
+ id: "team_fulfilment",
491
+ type: "stream-aligned",
492
+ label: "Order Fulfilment",
493
+ description: "Owns delivery, tracking and returns end-to-end.",
494
+ position: { x: 220, y: 402 },
495
+ size: { width: LANE_WIDTH, height: s["stream-aligned"].defaultSize.height }
377
496
  },
378
497
  {
379
498
  id: "team_fraud",
380
499
  type: "complicated-subsystem",
381
500
  label: "Fraud Detection",
382
501
  description: "Specialists for the real-time fraud scoring model.",
383
- position: { x: 870, y: 175 },
502
+ position: { x: 880, y: 249 },
384
503
  size: { ...s["complicated-subsystem"].defaultSize }
385
504
  },
386
505
  {
@@ -388,34 +507,34 @@ var SAMPLE_DOCUMENT = {
388
507
  type: "platform",
389
508
  label: "Cloud Platform",
390
509
  description: "Self-service CI/CD, runtime and observability for all teams.",
391
- position: { x: 240, y: 352 },
392
- size: { width: 560, height: 110 }
510
+ position: { x: 220, y: 552 },
511
+ size: { width: LANE_WIDTH, height: 110 }
393
512
  }
394
513
  ],
395
514
  interactions: [
396
515
  {
397
516
  id: "int_testing_discovery",
398
517
  mode: "facilitating",
399
- position: { x: 168, y: 208 },
518
+ position: { x: 148, y: 156 },
519
+ size: { ...i.facilitating.defaultSize }
520
+ },
521
+ {
522
+ id: "int_testing_checkout",
523
+ mode: "facilitating",
524
+ position: { x: 148, y: 282 },
400
525
  size: { ...i.facilitating.defaultSize }
401
526
  },
402
527
  {
403
528
  id: "int_checkout_fraud",
404
529
  mode: "collaboration",
405
530
  label: "fraud rules",
406
- position: { x: 779, y: 214 },
531
+ position: { x: 794, y: 288 },
407
532
  size: { ...i.collaboration.defaultSize }
408
533
  },
409
534
  {
410
- id: "int_platform_discovery",
411
- mode: "x-as-a-service",
412
- position: { x: 316, y: 286 },
413
- size: { ...i["x-as-a-service"].defaultSize }
414
- },
415
- {
416
- id: "int_platform_checkout",
535
+ id: "int_platform_fulfilment",
417
536
  mode: "x-as-a-service",
418
- position: { x: 636, y: 286 },
537
+ position: { x: 476, y: 486 },
419
538
  size: { ...i["x-as-a-service"].defaultSize }
420
539
  }
421
540
  ],
@@ -423,15 +542,15 @@ var SAMPLE_DOCUMENT = {
423
542
  {
424
543
  id: "flow_main",
425
544
  label: "Flow of change",
426
- position: { x: 60, y: 60 },
427
- size: { width: 980, height: 56 }
545
+ position: { x: 40, y: 40 },
546
+ size: { width: 1010, height: 56 }
428
547
  }
429
548
  ],
430
549
  annotations: [
431
550
  {
432
551
  id: "ann_fraud_handover",
433
552
  text: "Collaboration until the fraud rules are stable (Q3), then X-as-a-Service",
434
- position: { x: 830, y: 350 },
553
+ position: { x: 880, y: 423 },
435
554
  size: { ...ANNOTATION_SPEC.defaultSize },
436
555
  attachedTo: "int_checkout_fraud"
437
556
  }
@@ -440,29 +559,39 @@ var SAMPLE_DOCUMENT = {
440
559
 
441
560
  exports.ALL_INTERACTION_SPECS = ALL_INTERACTION_SPECS;
442
561
  exports.ALL_TEAM_SPECS = ALL_TEAM_SPECS;
562
+ exports.ANNOTATION_FIELDS = ANNOTATION_FIELDS;
443
563
  exports.ANNOTATION_SPEC = ANNOTATION_SPEC;
564
+ exports.DIAGRAM_ROOT_ID = DIAGRAM_ROOT_ID;
444
565
  exports.DND_MIME = DND_MIME;
566
+ exports.DOCUMENT_FIELDS = DOCUMENT_FIELDS;
445
567
  exports.DOCUMENT_VERSION = DOCUMENT_VERSION;
568
+ exports.FLOW_FIELDS = FLOW_FIELDS;
446
569
  exports.FLOW_SPEC = FLOW_SPEC;
570
+ exports.INTERACTION_FIELDS = INTERACTION_FIELDS;
447
571
  exports.INTERACTION_MODES = INTERACTION_MODES;
448
572
  exports.INTERACTION_MODE_SPECS = INTERACTION_MODE_SPECS;
449
573
  exports.SAMPLE_DOCUMENT = SAMPLE_DOCUMENT;
574
+ exports.TEAM_NODE_FIELDS = TEAM_NODE_FIELDS;
450
575
  exports.TEAM_TYPES = TEAM_TYPES;
451
576
  exports.TEAM_TYPE_SPECS = TEAM_TYPE_SPECS;
452
577
  exports.annotationSchema = annotationSchema;
578
+ exports.associationId = associationId;
453
579
  exports.canonicalize = canonicalize;
454
580
  exports.createFlowShape = createFlowShape;
455
581
  exports.createInteractionShape = createInteractionShape;
456
582
  exports.createTeamNode = createTeamNode;
457
583
  exports.createTextAnnotation = createTextAnnotation;
458
584
  exports.dashArray = dashArray;
585
+ exports.documentIdProblems = documentIdProblems;
459
586
  exports.documentSchema = documentSchema;
460
587
  exports.emptyDocument = emptyDocument;
461
588
  exports.flowSchema = flowSchema;
589
+ exports.foreignFields = foreignFields;
462
590
  exports.interactionSchema = interactionSchema;
463
591
  exports.newId = newId;
464
592
  exports.parseDocument = parseDocument;
465
593
  exports.serializeDocument = serializeDocument;
466
594
  exports.teamNodeSchema = teamNodeSchema;
595
+ exports.withForeignFields = withForeignFields;
467
596
  //# sourceMappingURL=index.cjs.map
468
597
  //# sourceMappingURL=index.cjs.map