@principal-ai/subsystems-react 0.20.5 → 0.21.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 (87) hide show
  1. package/README.md +1 -1
  2. package/dist/graphify/anchor.d.ts +17 -0
  3. package/dist/graphify/anchor.d.ts.map +1 -1
  4. package/dist/graphify/anchor.js +41 -0
  5. package/dist/graphify/anchor.js.map +1 -1
  6. package/dist/graphify/consolidated.d.ts +40 -1
  7. package/dist/graphify/consolidated.d.ts.map +1 -1
  8. package/dist/graphify/construct.d.ts +23 -0
  9. package/dist/graphify/construct.d.ts.map +1 -0
  10. package/dist/graphify/construct.js +77 -0
  11. package/dist/graphify/construct.js.map +1 -0
  12. package/dist/graphify/index.d.ts +2 -2
  13. package/dist/graphify/index.d.ts.map +1 -1
  14. package/dist/graphify/index.js +1 -1
  15. package/dist/graphify/index.js.map +1 -1
  16. package/dist/index.d.ts +7 -7
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/pierre/PierreWalkthroughCodeView.d.ts +22 -0
  21. package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -0
  22. package/dist/pierre/PierreWalkthroughCodeView.js +198 -0
  23. package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -0
  24. package/dist/pierre/index.d.ts +2 -2
  25. package/dist/pierre/index.js +1 -1
  26. package/dist/pierre/pierreFileLang.d.ts +1 -1
  27. package/dist/pierre/pierreFileLang.js +1 -1
  28. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +14 -2
  29. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  30. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +86 -20
  31. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  32. package/dist/subsystem/ComponentDeclaration.js +1 -1
  33. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  34. package/dist/subsystem/FileDrawer.d.ts +2 -2
  35. package/dist/subsystem/FileDrawer.js +2 -2
  36. package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -24
  37. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  38. package/dist/subsystem/SubsystemComponentGraph.js +220 -194
  39. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  40. package/dist/subsystem/formatDeclaration.d.ts +2 -1
  41. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  42. package/dist/subsystem/formatDeclaration.js +57 -33
  43. package/dist/subsystem/formatDeclaration.js.map +1 -1
  44. package/dist/subsystem/model.d.ts +109 -42
  45. package/dist/subsystem/model.d.ts.map +1 -1
  46. package/dist/subsystem/model.js +107 -5
  47. package/dist/subsystem/model.js.map +1 -1
  48. package/dist/subsystem/nodes.d.ts.map +1 -1
  49. package/dist/subsystem/nodes.js +43 -18
  50. package/dist/subsystem/nodes.js.map +1 -1
  51. package/dist/subsystem/tokenizeComponent.js +2 -2
  52. package/dist/subsystem/tokenizeComponent.js.map +1 -1
  53. package/package.json +3 -3
  54. package/src/graphify/anchor.test.ts +31 -2
  55. package/src/graphify/anchor.ts +46 -0
  56. package/src/graphify/consolidated.ts +43 -1
  57. package/src/graphify/{kind.test.ts → construct.test.ts} +20 -20
  58. package/src/graphify/{kind.ts → construct.ts} +17 -17
  59. package/src/graphify/index.ts +5 -2
  60. package/src/index.ts +20 -9
  61. package/src/pierre/{PierreThroughlineCodeView.tsx → PierreWalkthroughCodeView.tsx} +39 -47
  62. package/src/pierre/index.ts +2 -2
  63. package/src/pierre/pierreFileLang.ts +1 -1
  64. package/src/stories/Pierre/CodeView.stories.tsx +3 -3
  65. package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +14 -14
  66. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +13 -15
  67. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
  68. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +16 -20
  69. package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +11 -10
  70. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +13 -14
  71. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +31 -66
  72. package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +2 -2
  73. package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
  74. package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
  75. package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +254 -0
  76. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +518 -33
  77. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +13 -13
  78. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +105 -21
  79. package/src/subsystem/ComponentDeclaration.tsx +1 -1
  80. package/src/subsystem/FileDrawer.tsx +2 -2
  81. package/src/subsystem/SubsystemComponentGraph.tsx +284 -254
  82. package/src/subsystem/formatDeclaration.test.ts +76 -1
  83. package/src/subsystem/formatDeclaration.ts +57 -33
  84. package/src/subsystem/model.test.ts +49 -15
  85. package/src/subsystem/model.ts +210 -51
  86. package/src/subsystem/nodes.tsx +63 -22
  87. package/src/subsystem/tokenizeComponent.ts +2 -2
@@ -21,6 +21,10 @@ import { computeElkLayout, calculatePathLength } from '../utils/elkLayout';
21
21
  import type { GraphifyComponentDetail } from '../graphify';
22
22
  import type { SubsystemDeclarationRef } from './declarationRef';
23
23
 
24
+ /** Structured declaration shape — same union as graphify drill-down payloads. */
25
+ export type SubsystemConstructDeclaration = GraphifyComponentDetail;
26
+ export type SubsystemDeclarationProvenance = 'verified' | 'authored';
27
+
24
28
  export type SubsystemComponentConstruct =
25
29
  | 'class'
26
30
  | 'function'
@@ -82,20 +86,29 @@ export interface SubsystemDeclToken {
82
86
  color?: string;
83
87
  }
84
88
 
85
- export type SubsystemEdgeMechanism =
89
+ /**
90
+ * Topology relation type — structural / module / type claims.
91
+ * Belongs on `relations[]`, not on walkthrough hops.
92
+ */
93
+ export type SubsystemRelationType =
86
94
  | 'imports'
87
95
  | 'imports_from'
88
96
  | 're_exports'
89
97
  | 'defines'
90
- | 'calls'
91
98
  | 'extends'
92
99
  | 'inherits'
93
100
  | 'implements'
94
101
  | 'mixes_in'
95
- | 'uses'
96
102
  | 'method'
97
103
  | 'references'
98
- | 'contains'
104
+ | 'contains';
105
+
106
+ /**
107
+ * Walkthrough hop mechanism — runtime seams with a `file:line` site.
108
+ */
109
+ export type SubsystemWalkthroughMechanism =
110
+ | 'calls'
111
+ | 'uses'
99
112
  | 'feeds'
100
113
  | 'produces'
101
114
  | 'writes'
@@ -103,6 +116,11 @@ export type SubsystemEdgeMechanism =
103
116
  | 'watches'
104
117
  | 'registers-into';
105
118
 
119
+ /** Union for derived graph-edge styling (relationType or hop mechanism). */
120
+ export type SubsystemEdgeMechanism =
121
+ | SubsystemRelationType
122
+ | SubsystemWalkthroughMechanism;
123
+
106
124
  /** A component node — the named unit, construct-tagged; `file` is its location. */
107
125
  export interface SubsystemComponent {
108
126
  id: string;
@@ -126,6 +144,13 @@ export interface SubsystemComponent {
126
144
  purl: string;
127
145
  /** One-line purpose shown on the node. */
128
146
  purpose?: string;
147
+ /**
148
+ * Design / migration placeholder — participates in edges and flows but is
149
+ * not a live source declaration yet. Verification skips source checks until
150
+ * promoted (`proposed` cleared, `file` + `symbol` filled). Orthogonal to
151
+ * `construct` (intended shape) and `role` (topology).
152
+ */
153
+ proposed?: boolean;
129
154
  /**
130
155
  * Semantic role — where the node sits in the topology (boundary element,
131
156
  * external system), orthogonal to `construct` (what it is). Drives the
@@ -180,27 +205,20 @@ export interface SubsystemComponent {
180
205
  */
181
206
  layer?: number;
182
207
  /**
183
- * How this session *occupied* the component — `edited` (modified it) vs
184
- * `analyzed` (read deeply / built an understanding) vs `referenced`
185
- * (touched in passing). Drives clustering: edits strengthen a subsystem,
186
- * but a read-only investigation can still form one around an analyzed seam.
187
- */
188
- capture?: 'edited' | 'analyzed' | 'referenced';
189
- /**
190
- * Graphify drill-down detail for this component, when the facet is anchored
191
- * to a graphify node. Discriminated by kind (class/function/type/module/store/
192
- * external); rendered in the detail panel on click.
208
+ * Structured declaration shape of the construct (params, members, type
209
+ * buckets, …). Single source of truth for the click panel — authored by
210
+ * agents/CLI or filled by verified graphify capture. Discriminated by kind.
193
211
  */
194
- detail?: GraphifyComponentDetail;
212
+ declaration?: SubsystemConstructDeclaration;
195
213
  /**
196
- * Where the drill-down `detail` came from. `verified` = extracted from
214
+ * Where the structured `declaration` came from. `verified` = extracted from
197
215
  * source by tooling (graphify AST, signature extraction) — may be trusted
198
216
  * as matching the code. `authored` = written by the authoring agent/human
199
217
  * to highlight specific inputs/outputs — informative, not checked against
200
- * source. Detail without provenance is treated as `authored`; only tooling
201
- * may claim `verified`.
218
+ * source. Declaration without provenance is treated as `authored`; only
219
+ * tooling may claim `verified`.
202
220
  */
203
- detailProvenance?: 'verified' | 'authored';
221
+ declarationProvenance?: SubsystemDeclarationProvenance;
204
222
  /**
205
223
  * Pre-tokenized declaration for the detail panel. When present, the
206
224
  * renderer skips client-side tokenization. Tokens are language-agnostic;
@@ -214,7 +232,20 @@ export interface SubsystemComponent {
214
232
  declarationRef?: SubsystemDeclarationRef;
215
233
  }
216
234
 
217
- /** A cross-component edge in the subsystem graph. */
235
+ /** A topology relation between components (structural / module / type). */
236
+ export interface SubsystemRelation {
237
+ id: string;
238
+ from: string;
239
+ to: string;
240
+ relationType: SubsystemRelationType;
241
+ /** Concrete file/symbol refs backing the relation. */
242
+ refs?: string[];
243
+ }
244
+
245
+ /**
246
+ * Derived / display graph edge used by renderers. Built from `relations`
247
+ * and/or walkthrough hops — not authored as its own document field.
248
+ */
218
249
  export interface SubsystemComponentEdge {
219
250
  id: string;
220
251
  from: string; // component id
@@ -225,41 +256,97 @@ export interface SubsystemComponentEdge {
225
256
  }
226
257
 
227
258
  /**
228
- * A single site on an existing edge — the exact `file:line` where that edge's
229
- * seam manifests for a given flow. The edge stays the abstract contract
230
- * (`from`, `to`, `mechanism`); a throughline step picks the concrete
231
- * manifestation. One edge can appear in many steps.
259
+ * A single runtime hop — `from`/`to`/`mechanism` plus the exact `file:line`
260
+ * where that seam fires for a walkthrough.
232
261
  */
233
- export interface SubsystemThroughlineStep {
234
- /** Id of the existing edge this hop traverses. */
235
- edgeId: string;
236
- /** Repo-root-relative path of the file where the edge fires. */
262
+ export interface SubsystemWalkthroughStep {
263
+ from: string;
264
+ to: string;
265
+ mechanism: SubsystemWalkthroughMechanism;
266
+ /** Repo-root-relative path of the file where the seam fires. */
237
267
  file: string;
238
268
  /** 1-based line of the site within `file`. */
239
269
  line: number;
240
270
  /**
241
271
  * Frame name for this hop — the function/method/symbol on the stack at
242
- * this site. Optional so existing throughlines keep working; when set the
243
- * flows list shows it instead of mechanism + filename.
272
+ * this site. Optional; when set the walkthroughs list shows it instead of
273
+ * mechanism + filename.
244
274
  */
245
275
  symbol?: string;
246
276
  /**
247
277
  * Free-text note anchored to this hop's site line. Optional — informative
248
- * only, never verified against source; the Pierre throughline code view
278
+ * only, never verified against source; the Pierre walkthrough code view
249
279
  * surfaces it in the annotation column next to the highlighted line.
250
280
  */
251
281
  annotation?: string;
252
282
  }
253
283
 
254
- /**
255
- * An ordered execution story over a graph's edges — each step references an
256
- * existing edge and the exact site where that relationship fires for a flow;
257
- * ordering is the array. One throughline per flow (save flow, load flow, …).
258
- */
259
- export interface SubsystemThroughline {
284
+ /** An ordered runtime walkthrough — one named behavior story. */
285
+ export interface SubsystemWalkthrough {
260
286
  id: string;
261
287
  title: string;
262
- steps: SubsystemThroughlineStep[];
288
+ steps: SubsystemWalkthroughStep[];
289
+ }
290
+
291
+ export interface SubsystemModelDocument {
292
+ components: SubsystemComponent[];
293
+ /** Topology relations (structural / module / type). May be empty. */
294
+ relations: SubsystemRelation[];
295
+ /** Ordered runtime walkthroughs (one per named behavior). */
296
+ walkthroughs?: SubsystemWalkthrough[];
297
+ }
298
+
299
+ /** Stable id for a derived graph edge from a relation or walkthrough hop. */
300
+ export function derivedGraphEdgeId(
301
+ from: string,
302
+ to: string,
303
+ mechanism: SubsystemEdgeMechanism,
304
+ ): string {
305
+ return `${from}--${mechanism}-->${to}`;
306
+ }
307
+
308
+ /**
309
+ * Build display edges for the graph canvas from topology relations and
310
+ * walkthrough hops (deduped by from/to/mechanism).
311
+ */
312
+ /** React Flow / canvas edge id for a walkthrough hop. */
313
+ export function walkthroughStepGraphEdgeId(
314
+ step: Pick<SubsystemWalkthroughStep, 'from' | 'to' | 'mechanism'>,
315
+ ): string {
316
+ return derivedGraphEdgeId(step.from, step.to, step.mechanism);
317
+ }
318
+
319
+ export function deriveGraphEdges(doc: {
320
+ relations?: SubsystemRelation[];
321
+ walkthroughs?: SubsystemWalkthrough[];
322
+ }): SubsystemComponentEdge[] {
323
+ const byId = new Map<string, SubsystemComponentEdge>();
324
+ for (const r of doc.relations ?? []) {
325
+ const id = r.id || derivedGraphEdgeId(r.from, r.to, r.relationType);
326
+ if (!byId.has(id)) {
327
+ byId.set(id, {
328
+ id,
329
+ from: r.from,
330
+ to: r.to,
331
+ mechanism: r.relationType,
332
+ refs: r.refs,
333
+ });
334
+ }
335
+ }
336
+ for (const w of doc.walkthroughs ?? []) {
337
+ for (const step of w.steps) {
338
+ const id = derivedGraphEdgeId(step.from, step.to, step.mechanism);
339
+ if (!byId.has(id)) {
340
+ byId.set(id, {
341
+ id,
342
+ from: step.from,
343
+ to: step.to,
344
+ mechanism: step.mechanism,
345
+ });
346
+ }
347
+ }
348
+ }
349
+ return [...byId.values()];
263
350
  }
264
351
 
265
352
  /**
@@ -337,13 +424,6 @@ export function formatPurl(purl: string): string {
337
424
  return identity;
338
425
  }
339
426
 
340
- export interface SubsystemModelDocument {
341
- components: SubsystemComponent[];
342
- edges: SubsystemComponentEdge[];
343
- /** Ordered execution stories over the graph's edges (one per flow). */
344
- throughlines?: SubsystemThroughline[];
345
- }
346
-
347
427
  // ---------------------------------------------------------------------------
348
428
  // React Flow conversion
349
429
  // ---------------------------------------------------------------------------
@@ -525,6 +605,77 @@ export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
525
605
  service: 'service',
526
606
  };
527
607
 
608
+ /** Right-badge accent for `proposed: true` (no role, or combined with role). */
609
+ export const PROPOSED_COLOR = '#b8860b'; // darkgoldenrod — not-yet-in-source
610
+
611
+ /**
612
+ * Top-right badge label: proposed wins over role when both are set
613
+ * (proposed is the temporary exception to scan for; role stays on the model).
614
+ * Returns null when neither applies.
615
+ */
616
+ export function rightBadgeLabel(component: {
617
+ role?: SubsystemComponentRole;
618
+ proposed?: boolean;
619
+ }): string | null {
620
+ if (component.proposed) return 'proposed';
621
+ if (component.role != null) return ROLE_LABEL[component.role];
622
+ return null;
623
+ }
624
+
625
+ /** Color for the top-right badge (role and/or proposed). */
626
+ export function rightBadgeColor(component: {
627
+ role?: SubsystemComponentRole;
628
+ proposed?: boolean;
629
+ }): string | null {
630
+ if (component.proposed) return PROPOSED_COLOR;
631
+ if (component.role != null) return ROLE_COLOR[component.role];
632
+ return null;
633
+ }
634
+
635
+ /** Display label per store `storage` backing. */
636
+ export const STORAGE_LABEL: Record<string, string> = {
637
+ memory: 'memory',
638
+ disk: 'disk',
639
+ external: 'db',
640
+ };
641
+
642
+ /** Right-badge accent per store `storage` backing — retention reads as its
643
+ * own hue so memory / disk / db are scannable from far on a canvas. */
644
+ export const STORAGE_COLOR: Record<string, string> = {
645
+ memory: '#5aa9e6', // sky — process-lifetime RAM
646
+ disk: '#e3b341', // amber — this process on the filesystem
647
+ external: '#c46fd8', // violet — mediated by another system (db/service)
648
+ };
649
+
650
+ function storeStorage(component: {
651
+ declaration?: GraphifyComponentDetail;
652
+ }): string | undefined {
653
+ return component.declaration?.kind === 'store'
654
+ ? component.declaration.storage
655
+ : undefined;
656
+ }
657
+
658
+ /** Top-right storage badge for `construct: 'store'` nodes with an authored
659
+ * `storage` declaration — how the retained state is backed. Sits beside the
660
+ * role/proposed badge (storage is orthogonal to topology). Null otherwise. */
661
+ export function storageBadgeLabel(component: {
662
+ construct: SubsystemComponentConstruct;
663
+ declaration?: GraphifyComponentDetail;
664
+ }): string | null {
665
+ if (component.construct !== 'store') return null;
666
+ const storage = storeStorage(component);
667
+ return storage ? (STORAGE_LABEL[storage] ?? storage) : null;
668
+ }
669
+
670
+ export function storageBadgeColor(component: {
671
+ construct: SubsystemComponentConstruct;
672
+ declaration?: GraphifyComponentDetail;
673
+ }): string | null {
674
+ if (component.construct !== 'store') return null;
675
+ const storage = storeStorage(component);
676
+ return storage ? (STORAGE_COLOR[storage] ?? null) : null;
677
+ }
678
+
528
679
  /**
529
680
  * Primary badge text for a node: prefer framework stereotype over the
530
681
  * language construct so a React UI unit reads as "component" / "hook"
@@ -576,13 +727,21 @@ export function nodeMinWidthForBadges(component: {
576
727
  framework?: string;
577
728
  stereotype?: string;
578
729
  role?: SubsystemComponentRole;
730
+ proposed?: boolean;
579
731
  entityKind?: string;
732
+ declaration?: GraphifyComponentDetail;
580
733
  }): number {
581
734
  const left = estimateBadgeLabelWidth(constructBadgeLabel(component));
582
- if (component.role == null) {
735
+ const rightBadges = [
736
+ storageBadgeLabel(component),
737
+ rightBadgeLabel(component),
738
+ ].filter((l): l is string => l != null);
739
+ if (rightBadges.length === 0) {
583
740
  return Math.max(NODE_CSS_MIN_WIDTH, BADGE_EDGE_INSET + left + BADGE_EDGE_INSET);
584
741
  }
585
- const right = estimateBadgeLabelWidth(ROLE_LABEL[component.role]);
742
+ const right = rightBadges
743
+ .map(estimateBadgeLabelWidth)
744
+ .reduce((acc, w) => acc + BADGE_PAIR_GAP + w);
586
745
  return Math.max(
587
746
  NODE_CSS_MIN_WIDTH,
588
747
  BADGE_EDGE_INSET + left + BADGE_PAIR_GAP + right + BADGE_EDGE_INSET,
@@ -685,7 +844,7 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
685
844
  const compIds = new Set(doc.components.map((c) => c.id));
686
845
  const edges: SubsystemGraphEdge[] = [];
687
846
 
688
- for (const e of doc.edges) {
847
+ for (const e of deriveGraphEdges(doc)) {
689
848
  const color = MECHANISM_COLOR[e.mechanism];
690
849
  const style = MECHANISM_STYLE[e.mechanism];
691
850
  // If `to` is a real component, connect directly; otherwise point at a stub node.
@@ -712,14 +871,14 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
712
871
 
713
872
  /** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
714
873
  export function subsystemGraphLayoutKey(
715
- doc: Pick<SubsystemModelDocument, 'components' | 'edges'>,
874
+ doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'>,
716
875
  ): string {
717
876
  const components = doc.components
718
877
  .map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
719
878
  [id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
720
879
  .sort()
721
880
  .join('\n');
722
- const edgeKey = doc.edges
881
+ const edgeKey = deriveGraphEdges(doc)
723
882
  .map(({ id, from, to, mechanism }) => [id, from, to, mechanism].join('\0'))
724
883
  .sort()
725
884
  .join('\n');
@@ -759,7 +918,7 @@ export async function buildSubsystemGraph(
759
918
  // cross-package edges have something to land on.
760
919
  const realIds = new Set(doc.components.map((c) => c.id));
761
920
  const externalIds: string[] = [];
762
- for (const e of doc.edges) {
921
+ for (const e of deriveGraphEdges(doc)) {
763
922
  if (!realIds.has(e.to)) {
764
923
  const extId = `external:${e.to}`;
765
924
  if (!externalIds.includes(extId)) externalIds.push(extId);
@@ -19,9 +19,12 @@ import { useTheme } from '@principal-ade/industry-theme';
19
19
  import {
20
20
  MECHANISM_COLOR,
21
21
  MECHANISM_STYLE,
22
- ROLE_COLOR,
23
- ROLE_LABEL,
22
+ PROPOSED_COLOR,
24
23
  constructBadgeLabel,
24
+ rightBadgeLabel,
25
+ rightBadgeColor,
26
+ storageBadgeLabel,
27
+ storageBadgeColor,
25
28
  deriveNameFromSymbol,
26
29
  BADGE_EDGE_INSET,
27
30
  nodeMinWidthForBadges,
@@ -92,11 +95,23 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
92
95
  // `symbol` is the source of truth; `name` is derived from it consistently.
93
96
  const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file, c.stereotype);
94
97
  // Top badges are absolutely positioned — widen the node so they nowrap
95
- // instead of wrapping, including when construct + role badges share the top.
98
+ // instead of wrapping, including when construct + role/proposed share the top.
96
99
  const badgeMinWidth = nodeMinWidthForBadges(c);
100
+ const topRightLabel = rightBadgeLabel(c);
101
+ const topRightColor = rightBadgeColor(c);
102
+ // Store retention backing badge — memory/disk/db, beside role (orthogonal).
103
+ const storageLabel = storageBadgeLabel(c);
104
+ const storageColor = storageBadgeColor(c);
97
105
  // Set while a file is open in the drawer: true → spotlight, false → dim,
98
106
  // absent (no file open) → neutral.
99
107
  const fileMatch = data.fileMatch as boolean | undefined;
108
+ // Border: proposed uses the goldenrod accent (dashed); construct color stays
109
+ // on the left badge. File-open spotlight still wins with primary.
110
+ const borderColor = fileMatch
111
+ ? theme.colors.primary
112
+ : c.proposed
113
+ ? PROPOSED_COLOR
114
+ : color;
100
115
  // Selection is stamped into data by the graph component — React Flow's own
101
116
  // `selected` never updates because the node stops click propagation.
102
117
  const isSelected = selected || (data.isSelected as boolean | undefined) === true;
@@ -129,10 +144,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
129
144
  padding: '6px 10px',
130
145
  borderRadius: 8,
131
146
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
132
- // Selected / file-matched nodes get a thicker border; kind colors are
133
- // kept for selection (thickness only). The file-open spotlight keeps
134
- // its primary tint.
135
- border: `${isSelected || fileMatch ? 4 : 2}px solid ${fileMatch ? theme.colors.primary : color}`,
147
+ // Selected / file-matched nodes get a thicker border. Proposed nodes
148
+ // use a dashed goldenrod border; left construct badge keeps construct color.
149
+ border: `${isSelected || fileMatch ? 4 : 2}px ${c.proposed ? 'dashed' : 'solid'} ${borderColor}`,
136
150
  boxShadow: fileMatch
137
151
  ? `0 1px 4px rgba(0,0,0,0.25), 0 0 12px ${theme.colors.primary}55`
138
152
  : '0 1px 4px rgba(0,0,0,0.25)',
@@ -167,30 +181,57 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
167
181
  {constructBadgeLabel(c)}
168
182
  </div>
169
183
 
170
- {/* Role badge — top-right, only when the node carries a topology role.
171
- Role color (entry orange / service blue) on the badge; the node
172
- border stays construct-colored. */}
173
- {c.role != null && (
184
+ {/* Right badge cluster — proposed wins over role when both are set;
185
+ store storage (retention backing) sits beside, since it is
186
+ orthogonal to topology. Persistent; pointer-events pass through. */}
187
+ {(topRightLabel != null || storageLabel != null) && (
174
188
  <div
175
189
  style={{
176
190
  position: 'absolute',
177
191
  top: -9,
178
192
  right: BADGE_EDGE_INSET,
179
193
  zIndex: 1,
180
- fontFamily: theme.fonts.monospace,
181
- fontSize: theme.fontSizes[0] * 1.1,
182
- letterSpacing: 0.5,
183
- textTransform: 'uppercase',
184
- lineHeight: '17px',
194
+ display: 'flex',
195
+ gap: 6,
185
196
  whiteSpace: 'nowrap',
186
- color: ROLE_COLOR[c.role],
187
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
188
- border: `1px solid ${ROLE_COLOR[c.role]}`,
189
- borderRadius: 4,
190
- padding: '0 5px',
191
197
  }}
192
198
  >
193
- {ROLE_LABEL[c.role]}
199
+ {storageLabel != null && storageColor != null && (
200
+ <span
201
+ style={{
202
+ fontFamily: theme.fonts.monospace,
203
+ fontSize: theme.fontSizes[0] * 1.1,
204
+ letterSpacing: 0.5,
205
+ textTransform: 'uppercase',
206
+ lineHeight: '17px',
207
+ color: storageColor,
208
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
209
+ border: `1px solid ${storageColor}`,
210
+ borderRadius: 4,
211
+ padding: '0 5px',
212
+ }}
213
+ >
214
+ {storageLabel}
215
+ </span>
216
+ )}
217
+ {topRightLabel != null && topRightColor != null && (
218
+ <span
219
+ style={{
220
+ fontFamily: theme.fonts.monospace,
221
+ fontSize: theme.fontSizes[0] * 1.1,
222
+ letterSpacing: 0.5,
223
+ textTransform: 'uppercase',
224
+ lineHeight: '17px',
225
+ color: topRightColor,
226
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
227
+ border: `1px solid ${topRightColor}`,
228
+ borderRadius: 4,
229
+ padding: '0 5px',
230
+ }}
231
+ >
232
+ {topRightLabel}
233
+ </span>
234
+ )}
194
235
  </div>
195
236
  )}
196
237
 
@@ -50,9 +50,9 @@ export async function tokenizeComponent(
50
50
  if (component.tokens) return component.tokens;
51
51
 
52
52
  // External kind — not valid TypeScript, bypass Prettier.
53
- const kind = component.detail?.kind ?? component.construct;
53
+ const kind = component.declaration?.kind ?? component.construct;
54
54
  if (kind === 'external') {
55
- const label = component.detail?.kind === 'external' ? component.detail.label : component.name;
55
+ const label = component.declaration?.kind === 'external' ? component.declaration.label : component.name;
56
56
  const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
57
57
  return tokenizeFormatted(`external '${escaped}'`, themeName);
58
58
  }