@principal-ai/subsystems-react 0.49.0 → 0.51.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.
Files changed (97) hide show
  1. package/dist/index.d.ts +8 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreFileView.d.ts +1 -1
  6. package/dist/pierre/PierreFileView.d.ts.map +1 -1
  7. package/dist/pierre/PierreFileView.js +2 -1
  8. package/dist/pierre/PierreFileView.js.map +1 -1
  9. package/dist/pierre/PierreSnippetView.d.ts +1 -1
  10. package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
  11. package/dist/pierre/PierreSnippetView.js +2 -1
  12. package/dist/pierre/PierreSnippetView.js.map +1 -1
  13. package/dist/pierre/PierreTrailCodeView.d.ts +7 -1
  14. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  15. package/dist/pierre/PierreTrailCodeView.js +71 -18
  16. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  17. package/dist/pierre/pierreBackground.d.ts +36 -2
  18. package/dist/pierre/pierreBackground.d.ts.map +1 -1
  19. package/dist/pierre/pierreBackground.js +40 -13
  20. package/dist/pierre/pierreBackground.js.map +1 -1
  21. package/dist/pierre/visibleStep.d.ts +48 -0
  22. package/dist/pierre/visibleStep.d.ts.map +1 -0
  23. package/dist/pierre/visibleStep.js +53 -0
  24. package/dist/pierre/visibleStep.js.map +1 -0
  25. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts +95 -0
  26. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts.map +1 -0
  27. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js +340 -0
  28. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js.map +1 -0
  29. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts +4 -0
  30. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts.map +1 -1
  31. package/dist/stories/Subsystem/C4Graph/c4Fixture.js +7334 -1
  32. package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +1 -1
  33. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts +2 -0
  34. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts.map +1 -0
  35. package/dist/stories/Subsystem/C4Graph/generateFixture.js +145 -0
  36. package/dist/stories/Subsystem/C4Graph/generateFixture.js.map +1 -0
  37. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  38. package/dist/subsystem/C4Graph.js +16 -60
  39. package/dist/subsystem/C4Graph.js.map +1 -1
  40. package/dist/subsystem/C4NodeCard.d.ts +84 -0
  41. package/dist/subsystem/C4NodeCard.d.ts.map +1 -0
  42. package/dist/subsystem/C4NodeCard.js +229 -0
  43. package/dist/subsystem/C4NodeCard.js.map +1 -0
  44. package/dist/subsystem/DiagnosticToggle.d.ts +8 -1
  45. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
  46. package/dist/subsystem/DiagnosticToggle.js +40 -6
  47. package/dist/subsystem/DiagnosticToggle.js.map +1 -1
  48. package/dist/subsystem/FileDrawer.d.ts +10 -1
  49. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  50. package/dist/subsystem/FileDrawer.js +61 -40
  51. package/dist/subsystem/FileDrawer.js.map +1 -1
  52. package/dist/subsystem/SubsystemComponentGraph.d.ts +6 -0
  53. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  54. package/dist/subsystem/SubsystemComponentGraph.js +21 -10
  55. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  56. package/dist/subsystem/c4Associations.d.ts +186 -0
  57. package/dist/subsystem/c4Associations.d.ts.map +1 -0
  58. package/dist/subsystem/c4Associations.js +284 -0
  59. package/dist/subsystem/c4Associations.js.map +1 -0
  60. package/dist/subsystem/model.d.ts +25 -1
  61. package/dist/subsystem/model.d.ts.map +1 -1
  62. package/dist/subsystem/model.js +15 -0
  63. package/dist/subsystem/model.js.map +1 -1
  64. package/dist/subsystem/nodes.d.ts.map +1 -1
  65. package/dist/subsystem/nodes.js +78 -7
  66. package/dist/subsystem/nodes.js.map +1 -1
  67. package/dist/subsystem/toC4.d.ts +119 -0
  68. package/dist/subsystem/toC4.d.ts.map +1 -1
  69. package/dist/subsystem/toC4.js +95 -5
  70. package/dist/subsystem/toC4.js.map +1 -1
  71. package/package.json +2 -2
  72. package/src/index.ts +39 -0
  73. package/src/pierre/PierreFileView.tsx +2 -1
  74. package/src/pierre/PierreSnippetView.tsx +2 -1
  75. package/src/pierre/PierreTrailCodeView.tsx +81 -22
  76. package/src/pierre/pierreBackground.ts +62 -12
  77. package/src/pierre/visibleStep.test.ts +108 -0
  78. package/src/pierre/visibleStep.ts +76 -0
  79. package/src/stories/Subsystem/C4Graph/C4Associations.stories.tsx +273 -0
  80. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +325 -0
  81. package/src/stories/Subsystem/C4Graph/c4Associations.fixture.ts +344 -0
  82. package/src/stories/Subsystem/C4Graph/c4Fixture.ts +7337 -4
  83. package/src/stories/Subsystem/C4Graph/generateFixture.ts +161 -0
  84. package/src/stories/Subsystem/ComponentGraph/Deprecated.stories.tsx +358 -0
  85. package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +27 -2
  86. package/src/stories/Subsystem/ComponentGraph/{Flows.stories.tsx → Trails.stories.tsx} +10 -4
  87. package/src/subsystem/C4Graph.tsx +51 -92
  88. package/src/subsystem/C4NodeCard.test.ts +196 -0
  89. package/src/subsystem/C4NodeCard.tsx +307 -0
  90. package/src/subsystem/DiagnosticToggle.tsx +58 -4
  91. package/src/subsystem/FileDrawer.tsx +112 -63
  92. package/src/subsystem/SubsystemComponentGraph.tsx +29 -5
  93. package/src/subsystem/c4Associations.test.ts +484 -0
  94. package/src/subsystem/c4Associations.ts +398 -0
  95. package/src/subsystem/model.ts +34 -1
  96. package/src/subsystem/nodes.tsx +124 -5
  97. package/src/subsystem/toC4.ts +210 -5
@@ -37,6 +37,12 @@ export interface C4Node {
37
37
  parentId?: string;
38
38
  /** Grouping key the node was derived from (process key, purl, alias). */
39
39
  key?: string;
40
+ /**
41
+ * Every derived key folded into this node, when one association claims more
42
+ * than one. Present only on a merged box — a node built from a single key
43
+ * leaves it undefined and `key` tells the whole story.
44
+ */
45
+ sourceKeys?: string[];
40
46
  /** Source component aliases rolled into this node. */
41
47
  members: string[];
42
48
  /** Distinct constructs among the members (for a quick breakdown). */
@@ -47,8 +53,58 @@ export interface C4Node {
47
53
  component?: SubsystemComponent;
48
54
  /** Source model ids that contributed members, when attribution is supplied. */
49
55
  models?: string[];
56
+ /**
57
+ * Confirmed C4 attributes, supplied by a decoration. Absent for the raw
58
+ * derivation — a bare `toC4(doc)` proposes nothing and confirms nothing.
59
+ */
60
+ decoration?: C4Decoration;
50
61
  }
51
62
 
63
+ /**
64
+ * The C4 attributes a reviewer (or an agent they approve) attaches to one
65
+ * derived box. This is the seam between "the models said X" and "a person
66
+ * agreed X is a container with technology Y".
67
+ */
68
+ export interface C4Decoration {
69
+ /** Identity of the confirmed element. Replaces the derived node id, which is
70
+ * what makes two derived keys able to collapse into one box. */
71
+ id: string;
72
+ /** Display name. Defaults to the grouping key. */
73
+ label?: string;
74
+ /** C4 element type — required by the notation, absent from the document. */
75
+ type?: C4ElementType;
76
+ /** C4 "technology" — required on every container by the notation. */
77
+ technology?: string;
78
+ /** One-line responsibility. Derivable from member `purpose`. */
79
+ description?: string;
80
+ /** Has a person looked at this yet? */
81
+ state?: C4AssociationState;
82
+ /** Override the nesting parent (container id) for a component-level element. */
83
+ parentId?: string;
84
+ }
85
+
86
+ /**
87
+ * C4 element type — the shape of the box.
88
+ *
89
+ * The document has no field for this: `construct` says what a *declaration* is
90
+ * (function / store / external), not what an *element* is. c4model.com requires
91
+ * a type on every element, so this is something the projection has to supply
92
+ * or the reviewer has to confirm.
93
+ */
94
+ export type C4ElementType =
95
+ /** a deployable unit — web app, server, CLI, desktop app */
96
+ | 'application'
97
+ /** a database, file store, or blob store */
98
+ | 'data-store'
99
+ /** a queue or topic */
100
+ | 'queue'
101
+ /** a JAR / assembly / npm package: organised *within* a container, not one */
102
+ | 'library'
103
+ /** another software system, outside our boundary */
104
+ | 'software-system'
105
+ /** a human */
106
+ | 'person';
107
+
52
108
  /** A compound frame (the system, or a container holding components). */
53
109
  export interface C4Group {
54
110
  id: string;
@@ -78,6 +134,52 @@ export interface C4Model {
78
134
  edges: C4Edge[];
79
135
  }
80
136
 
137
+ /**
138
+ * Has anyone signed off on this element yet?
139
+ *
140
+ * `proposed` is what an agent writes and a human has not looked at. `accepted`
141
+ * is a human decision. `rejected` means the derivation was wrong and the box
142
+ * should not be drawn at all — the one state that removes a node.
143
+ */
144
+ export type C4AssociationState = 'proposed' | 'accepted' | 'rejected';
145
+
146
+ /**
147
+ * One confirmed C4 element: a container or component a person (or an agent a
148
+ * person approved) has said is a real element of the architecture.
149
+ *
150
+ * This lives OUTSIDE the subsystem model document on purpose. The document
151
+ * describes code — what a symbol is and where it runs. Whether two of those
152
+ * runs are one deployable unit is a different, revisable claim about the
153
+ * architecture, and writing it back would mean the next audit had to defend it.
154
+ */
155
+ export interface C4Association {
156
+ /** Stable identity for this element: `container:<slug>`. Survives renames. */
157
+ id: string;
158
+ /** What C4 level this is. Determines where it is drawn. */
159
+ level: 'container' | 'component';
160
+ /**
161
+ * The derived keys this element absorbs. One key in the common case; more
162
+ * than one when several `process` values are really the same deployable.
163
+ * This is the whole dedup mechanism: merging is claiming several keys under
164
+ * one id, so the rollup below collapses them into a single box for free.
165
+ */
166
+ sourceKeys: string[];
167
+ label: string;
168
+ /** C4 requires a type on every element. */
169
+ type: C4ElementType;
170
+ /** C4 requires technology on every container. */
171
+ technology?: string;
172
+ /** C4 requires a description on every element. */
173
+ description?: string;
174
+ state: C4AssociationState;
175
+ /** Why the association says what it says — shown in the confirm UI. */
176
+ rationale?: string;
177
+ /** Who or what authored this association. */
178
+ author?: string;
179
+ /** When the state last changed (ISO). */
180
+ decidedAt?: string;
181
+ }
182
+
81
183
  export interface ToC4Options {
82
184
  view?: C4View;
83
185
  /** Override the system title; defaults to the repo key's owner/name. */
@@ -86,6 +188,12 @@ export interface ToC4Options {
86
188
  repoKey?: string;
87
189
  /** Model-id attribution per component alias (e.g. from a merge sidecar). */
88
190
  modelsByAlias?: Record<string, string[]>;
191
+ /**
192
+ * Confirmed elements. When supplied, a derived key claimed by an
193
+ * `accepted` association is drawn as that association's box instead of as a
194
+ * raw key, and a `rejected` one is not drawn at all.
195
+ */
196
+ associations?: readonly C4Association[];
89
197
  }
90
198
 
91
199
  const EXTERNAL_PURL = 'external';
@@ -96,6 +204,48 @@ export function isGroundedComponent(c: SubsystemComponent): boolean {
96
204
  return c.construct !== 'external' && c.construct !== 'custom_entity';
97
205
  }
98
206
 
207
+ /**
208
+ * Index confirmed associations by the derived keys they claim.
209
+ *
210
+ * First writer wins on a contested key so the result does not depend on array
211
+ * order, and the loser is reported so the conflict is visible rather than
212
+ * silently dropped.
213
+ */
214
+ export function indexAssociations(
215
+ associations: readonly C4Association[] | undefined,
216
+ ): {
217
+ byKey: Map<string, C4Association>;
218
+ contested: Array<{ key: string; kept: string; dropped: string }>;
219
+ } {
220
+ const byKey = new Map<string, C4Association>();
221
+ const contested: Array<{ key: string; kept: string; dropped: string }> = [];
222
+ if (!associations) return { byKey, contested };
223
+
224
+ // Stable order: accepted before proposed, so a confirmed decision outranks a
225
+ // speculative one for the same key.
226
+ const ordered = [...associations].sort((a, b) => {
227
+ const rank = (s: C4AssociationState) => (s === 'accepted' ? 0 : s === 'proposed' ? 1 : 2);
228
+ return rank(a.state) - rank(b.state) || a.id.localeCompare(b.id);
229
+ });
230
+
231
+ for (const a of ordered) {
232
+ for (const key of a.sourceKeys) {
233
+ const existing = byKey.get(key);
234
+ if (existing && existing.id !== a.id) {
235
+ contested.push({ key, kept: existing.id, dropped: a.id });
236
+ continue;
237
+ }
238
+ byKey.set(key, a);
239
+ }
240
+ }
241
+ return { byKey, contested };
242
+ }
243
+
244
+ /** True when a resolved association means this box should not be drawn. */
245
+ function isSuppressed(a: C4Association | undefined): boolean {
246
+ return a?.state === 'rejected';
247
+ }
248
+
99
249
  /** `owner/name` from a purl, trimmed of scheme and fragment. */
100
250
  export function labelFromPurl(purl: string): string {
101
251
  const base = purl.replace(/^external:/, '').split('#')[0]?.trim() ?? '';
@@ -133,6 +283,11 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
133
283
  const view: C4View = options.view ?? 'container';
134
284
  const repoKey = options.repoKey ?? deriveRepoKey(doc) ?? 'system';
135
285
  const systemId = `system:${repoKey}`;
286
+ const { byKey: assocByKey } = indexAssociations(options.associations);
287
+ // Id -> association, so a container frame can be labelled from its
288
+ // association even when no node exists for it (component view).
289
+ const assocById = new Map<string, C4Association>();
290
+ for (const a of options.associations ?? []) assocById.set(a.id, a);
136
291
 
137
292
  const system: C4Node = {
138
293
  id: systemId,
@@ -150,12 +305,33 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
150
305
  const containerNodeId = (key: string) => `container:${key}`;
151
306
  const componentNodeId = (alias: string) => `component:${alias}`;
152
307
 
308
+ /**
309
+ * Resolve one derived container key into the box that should carry it.
310
+ *
311
+ * An accepted association wins: its id becomes the node id, so every key it
312
+ * claims rolls into one node (that is the dedup), and its label / type /
313
+ * technology / description become the node's presentation. A rejected one
314
+ * means the box is not drawn at all. Unclaimed keys keep the raw derived
315
+ * behaviour, so the projection is still useful with zero associations.
316
+ */
317
+ const resolveContainer = (key: string): { id: string; label: string; assoc: C4Association | undefined } => {
318
+ const assoc = assocByKey.get(key);
319
+ if (assoc && assoc.level === 'container') {
320
+ return { id: assoc.id, label: assoc.label, assoc };
321
+ }
322
+ if (view === 'component') {
323
+ return { id: containerNodeId(key), label: key === UNASSIGNED ? 'Unassigned' : key, assoc: undefined };
324
+ }
325
+ return { id: containerNodeId(key), label: key === UNASSIGNED ? 'Unassigned' : key, assoc: undefined };
326
+ };
327
+
153
328
  for (const c of doc.components) {
154
329
  let id: string;
155
330
  let kind: C4Kind;
156
331
  let label: string;
157
332
  let parentId: string | undefined;
158
333
  let key: string | undefined;
334
+ let assoc: C4Association | undefined;
159
335
 
160
336
  if (c.construct === 'external') {
161
337
  key = externalKey(c);
@@ -169,16 +345,22 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
169
345
  label = c.name;
170
346
  } else if (view === 'component') {
171
347
  key = containerKey(c);
348
+ const resolved = resolveContainer(key);
349
+ if (isSuppressed(resolved.assoc)) continue;
172
350
  id = componentNodeId(c.alias);
173
351
  kind = 'component';
174
352
  label = c.name;
175
- parentId = containerNodeId(key);
353
+ parentId = resolved.id;
354
+ assoc = undefined; // the component itself has no association
176
355
  } else {
177
356
  key = containerKey(c);
178
- id = containerNodeId(key);
357
+ const resolved = resolveContainer(key);
358
+ if (isSuppressed(resolved.assoc)) continue;
359
+ id = resolved.id;
179
360
  kind = 'container';
180
- label = key === UNASSIGNED ? 'Unassigned' : key;
361
+ label = resolved.label;
181
362
  parentId = systemId;
363
+ assoc = resolved.assoc;
182
364
  }
183
365
 
184
366
  ownerOf.set(c.alias, id);
@@ -194,7 +376,12 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
194
376
  members: [],
195
377
  constructs: [],
196
378
  isStore: false,
379
+ // A merged box is built from several derived keys. Track them as a
380
+ // set so the confirm UI can show what was folded together — without
381
+ // this, the accumulation would run once per member, not per key.
382
+ ...(assoc && assoc.sourceKeys.length > 1 ? { sourceKeys: [...assoc.sourceKeys] } : {}),
197
383
  ...(kind === 'component' ? { component: c } : {}),
384
+ ...(assoc ? { decoration: toDecoration(assoc) } : {}),
198
385
  };
199
386
  nodes.set(id, node);
200
387
  }
@@ -209,6 +396,10 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
209
396
 
210
397
  // --- Groups (compound frames) ------------------------------------------
211
398
  const groups: C4Group[] = [];
399
+ const labelForContainerId = (id: string): string =>
400
+ nodes.get(id)?.label ??
401
+ assocById.get(id)?.label ??
402
+ (id.startsWith('container:') ? id.slice('container:'.length) : id);
212
403
  if (view === 'container') {
213
404
  const containerIds = [...nodes.values()].filter((n) => n.kind === 'container').map((n) => n.id);
214
405
  if (containerIds.length > 0) {
@@ -224,8 +415,7 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
224
415
  }
225
416
  const containerIds: string[] = [];
226
417
  for (const [id, memberIds] of byContainer) {
227
- const label = id.slice('container:'.length);
228
- groups.push({ id, kind: 'container', label: label === UNASSIGNED ? 'Unassigned' : label, parentId: systemId, memberIds });
418
+ groups.push({ id, kind: 'container', label: labelForContainerId(id), parentId: systemId, memberIds });
229
419
  containerIds.push(id);
230
420
  }
231
421
  if (containerIds.length > 0) {
@@ -244,6 +434,9 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
244
434
  mechanism: string,
245
435
  ) => {
246
436
  if (!source || !target || source === target) return;
437
+ // An endpoint whose box was rejected has no owner; the edge cannot be
438
+ // drawn, so drop it rather than inventing a dangling arrow.
439
+ if (!nodes.has(source) || !nodes.has(target)) return;
247
440
  const key = `${source}\0${target}`;
248
441
  let edge = bucket.get(key);
249
442
  if (!edge) {
@@ -271,3 +464,15 @@ export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4
271
464
  edges,
272
465
  };
273
466
  }
467
+
468
+ /** Project an association down to the attributes a node carries. */
469
+ function toDecoration(a: C4Association): C4Decoration {
470
+ return {
471
+ id: a.id,
472
+ label: a.label,
473
+ type: a.type,
474
+ technology: a.technology,
475
+ description: a.description,
476
+ state: a.state,
477
+ };
478
+ }