@principal-ai/principal-view-react 0.16.56 → 0.16.57

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 (102) hide show
  1. package/dist/graphify/consolidated.d.ts +16 -1
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/index.d.ts +1 -1
  4. package/dist/graphify/index.d.ts.map +1 -1
  5. package/dist/graphify/index.js.map +1 -1
  6. package/dist/graphify/kind.d.ts +1 -1
  7. package/dist/graphify/kind.d.ts.map +1 -1
  8. package/dist/graphify/kind.js +7 -1
  9. package/dist/graphify/kind.js.map +1 -1
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/pierre/constructColors.d.ts +24 -0
  15. package/dist/pierre/constructColors.d.ts.map +1 -0
  16. package/dist/pierre/constructColors.js +91 -0
  17. package/dist/pierre/constructColors.js.map +1 -0
  18. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
  19. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
  20. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
  21. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
  22. package/dist/subsystem/ComponentDeclaration.d.ts +2 -2
  23. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  24. package/dist/subsystem/ComponentDeclaration.js +12 -11
  25. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  26. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  27. package/dist/subsystem/SubsystemComponentGraph.js +22 -1
  28. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  29. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  30. package/dist/subsystem/formatDeclaration.js +49 -10
  31. package/dist/subsystem/formatDeclaration.js.map +1 -1
  32. package/dist/subsystem/model.d.ts +55 -12
  33. package/dist/subsystem/model.d.ts.map +1 -1
  34. package/dist/subsystem/model.js +54 -24
  35. package/dist/subsystem/model.js.map +1 -1
  36. package/dist/subsystem/nodes.d.ts +4 -1
  37. package/dist/subsystem/nodes.d.ts.map +1 -1
  38. package/dist/subsystem/nodes.js +70 -21
  39. package/dist/subsystem/nodes.js.map +1 -1
  40. package/dist/subsystem/tokenizeComponent.js +1 -1
  41. package/dist/subsystem/tokenizeComponent.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/graphify/consolidated.ts +18 -1
  44. package/src/graphify/index.ts +1 -0
  45. package/src/graphify/kind.ts +7 -1
  46. package/src/index.ts +3 -2
  47. package/src/pierre/constructColors.test.ts +52 -0
  48. package/src/pierre/constructColors.ts +112 -0
  49. package/src/stories/{BoundaryNodes.stories.tsx → Audit/BoundaryNodes.stories.tsx} +1 -1
  50. package/src/stories/{CanvasEdgeTypes.stories.tsx → Audit/CanvasEdgeTypes.stories.tsx} +1 -1
  51. package/src/stories/{CanvasNodeTypes.stories.tsx → Audit/CanvasNodeTypes.stories.tsx} +1 -1
  52. package/src/stories/{ColorPriority.stories.tsx → Audit/ColorPriority.stories.tsx} +1 -1
  53. package/src/stories/{DiamondBadges.stories.tsx → Audit/DiamondBadges.stories.tsx} +1 -1
  54. package/src/stories/{NodeDimensionsTesting.stories.tsx → Audit/NodeDimensionsTesting.stories.tsx} +2 -2
  55. package/src/stories/{NodeFieldsAudit.stories.tsx → Audit/NodeFieldsAudit.stories.tsx} +1 -1
  56. package/src/stories/{NodeFontSizeTesting.stories.tsx → Audit/NodeFontSizeTesting.stories.tsx} +2 -2
  57. package/src/stories/{NodeShapes.stories.tsx → Audit/NodeShapes.stories.tsx} +1 -1
  58. package/src/stories/{WideNodesTesting.stories.tsx → Audit/WideNodesTesting.stories.tsx} +2 -2
  59. package/src/stories/{dashboard → Components}/DashboardRenderer.stories.tsx +2 -2
  60. package/src/stories/{GraphRenderer.stories.tsx → Components/GraphRenderer.stories.tsx} +2 -2
  61. package/src/stories/{MultiCanvasRenderer.stories.tsx → Components/MultiCanvasRenderer.stories.tsx} +2 -2
  62. package/src/stories/{SequenceDiagram.stories.tsx → Components/SequenceDiagram.stories.tsx} +2 -2
  63. package/src/stories/{StateView.stories.tsx → Components/StateView.stories.tsx} +2 -2
  64. package/src/stories/{TraceViewer.stories.tsx → Components/TraceViewer.stories.tsx} +3 -3
  65. package/src/stories/{WorkflowSequenceDiagram.stories.tsx → Components/WorkflowSequenceDiagram.stories.tsx} +1 -1
  66. package/src/stories/{AnimationWorkshop.stories.tsx → Features/AnimationWorkshop.stories.tsx} +5 -5
  67. package/src/stories/{ChangeTypeVisual.stories.tsx → Features/ChangeTypeVisual.stories.tsx} +2 -2
  68. package/src/stories/{EventDrivenAnimations.stories.tsx → Features/EventDrivenAnimations.stories.tsx} +2 -2
  69. package/src/stories/{MultiConfig.stories.tsx → Features/MultiConfig.stories.tsx} +2 -2
  70. package/src/stories/{ElkEdgeRouting.stories.tsx → Layout/ElkEdgeRouting.stories.tsx} +2 -2
  71. package/src/stories/{GraphOrientation.stories.tsx → Layout/GraphOrientation.stories.tsx} +3 -3
  72. package/src/stories/{MultiDirectionalConnections.stories.tsx → Layout/MultiDirectionalConnections.stories.tsx} +2 -2
  73. package/src/stories/{NodeDefinitionComparison.stories.tsx → Nodes/NodeDefinitionComparison.stories.tsx} +2 -2
  74. package/src/stories/{NodeNameFallbacks.stories.tsx → Nodes/NodeNameFallbacks.stories.tsx} +2 -2
  75. package/src/stories/{NumericColors.stories.tsx → Nodes/NumericColors.stories.tsx} +2 -2
  76. package/src/stories/{EventNameDisplay.stories.tsx → OTEL/EventNameDisplay.stories.tsx} +2 -2
  77. package/src/stories/{EventNameFallback.stories.tsx → OTEL/EventNameFallback.stories.tsx} +2 -2
  78. package/src/stories/{OtelComponents.stories.tsx → OTEL/OtelComponents.stories.tsx} +3 -3
  79. package/src/stories/{OtelNodeTypesPrototype.stories.tsx → OTEL/OtelNodeTypesPrototype.stories.tsx} +2 -2
  80. package/src/stories/{RawEventCards.stories.tsx → OTEL/RawEventCards.stories.tsx} +3 -3
  81. package/src/stories/{SpanBadges.stories.tsx → OTEL/SpanBadges.stories.tsx} +1 -1
  82. package/src/stories/{ValidationEventsCanvas.stories.tsx → OTEL/ValidationEventsCanvas.stories.tsx} +3 -3
  83. package/src/stories/{BacklogMdCanvases.stories.tsx → RealWorld/BacklogMdCanvases.stories.tsx} +2 -2
  84. package/src/stories/{FileCitySequence.stories.tsx → RealWorld/FileCitySequence.stories.tsx} +4 -4
  85. package/src/stories/{RealCanvasFiles.stories.tsx → RealWorld/RealCanvasFiles.stories.tsx} +7 -7
  86. package/src/stories/{ComponentDeclarationAudit.stories.tsx → Subsystem/ComponentDeclarationAudit.stories.tsx} +19 -20
  87. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
  88. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
  89. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
  90. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
  91. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
  92. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +646 -0
  93. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
  94. package/src/subsystem/ComponentDeclaration.tsx +13 -13
  95. package/src/subsystem/SubsystemComponentGraph.tsx +25 -5
  96. package/src/subsystem/formatDeclaration.ts +57 -11
  97. package/src/subsystem/model.test.ts +38 -6
  98. package/src/subsystem/model.ts +108 -31
  99. package/src/subsystem/nodes.tsx +103 -27
  100. package/src/subsystem/tokenizeComponent.ts +1 -1
  101. package/src/stories/SubsystemComponentGraph.stories.tsx +0 -1225
  102. /package/src/stories/{dashboard/sample-dashboards → data}/activity-feed-analytics.dashboard.json +0 -0
@@ -2,7 +2,7 @@
2
2
  * Subsystem component-graph data model + converters.
3
3
  *
4
4
  * A subsystem snapshot (see the "Subsystem artifact: facets" topic) is captured
5
- * as component nodes (kind-tagged source units), file refs, integration edges,
5
+ * as component nodes (construct-tagged source units), file refs, integration edges,
6
6
  * and entry points. This module defines the minimal document shape for the
7
7
  * *graph* facet and converts it to React Flow nodes/edges — packages render as
8
8
  * subgraphs, only cross-package edges leave the box, and shared seams
@@ -21,14 +21,30 @@ import { computeElkLayout } from '../utils/elkLayout';
21
21
  import type { GraphifyComponentDetail } from '../graphify';
22
22
  import type { SubsystemDeclarationRef } from './declarationRef';
23
23
 
24
- export type SubsystemComponentKind =
24
+ export type SubsystemComponentConstruct =
25
25
  | 'class'
26
26
  | 'function'
27
27
  | 'method'
28
- | 'type'
28
+ | 'interface'
29
+ | 'type_alias'
30
+ | 'enum'
29
31
  | 'module'
32
+ | 'store'
30
33
  | 'external';
31
34
 
35
+ /**
36
+ * Semantic role — where the node sits in the topology, orthogonal to
37
+ * `construct` (what it is). Roles have *inherited* anatomy: an entry renders
38
+ * as its real code shape (function dispatcher, type contract); a service has
39
+ * no source at all (`construct: 'external'` + purl identity). Contrast with
40
+ * `construct: 'store'`, which introduces its own anatomy (the state block) —
41
+ * that is why store is a construct and not a role. Role nodes must stay anchored to real code: an
42
+ * `entry` is a boundary element (route dispatcher, message contract) carrying
43
+ * the wire address as identity; a `service` is an external system the process
44
+ * calls out to (identity via purl, no `process` — it belongs to no region).
45
+ */
46
+ export type SubsystemComponentRole = 'entry' | 'service';
47
+
32
48
  // ---------------------------------------------------------------------------
33
49
  // Declaration tokens — structured source representation
34
50
  // ---------------------------------------------------------------------------
@@ -65,19 +81,48 @@ export type SubsystemEdgeMechanism =
65
81
  | 'contains'
66
82
  | 'feeds'
67
83
  | 'produces'
84
+ | 'writes'
85
+ | 'reads'
86
+ | 'watches'
68
87
  | 'registers-into';
69
88
 
70
- /** A component node — the named unit, kind-tagged; `file` is its location. */
89
+ /** A component node — the named unit, construct-tagged; `file` is its location. */
71
90
  export interface SubsystemComponent {
72
91
  id: string;
73
92
  name: string;
74
- kind: SubsystemComponentKind;
93
+ /**
94
+ * The node's construct — what it IS as a declaration (class, function,
95
+ * method, interface, type alias, enum, store, external), driving node
96
+ * anatomy, color, badge, and the verification strategy. Every construct
97
+ * anchors to a definition; runtime occurrences (variables, activations,
98
+ * instances) are NOT constructs — they belong to a future execution-mode
99
+ * graph whose occurrence nodes reference these definitions. Ontology:
100
+ * construct = what it is, role = where it sits, process = where it runs.
101
+ */
102
+ construct: SubsystemComponentConstruct;
75
103
  /** Source location the component lives in (repo-root-relative path). */
76
104
  file: string;
77
105
  /** PURL identifying the repo or package this component lives in (for subgraph grouping). */
78
106
  purl: string;
79
107
  /** One-line purpose shown on the node. */
80
108
  purpose?: string;
109
+ /**
110
+ * Semantic role — where the node sits in the topology (boundary element,
111
+ * external system), orthogonal to `construct` (what it is). Drives the
112
+ * glyph and the edge-pairing rules: boundary-crossing edges must terminate
113
+ * at an entry or a service. Retained state is NOT a role — it is
114
+ * `construct: 'store'` (state-block anatomy, `writes`/`reads`/`watches`
115
+ * inbound, `produces` outbound).
116
+ */
117
+ role?: SubsystemComponentRole;
118
+ /**
119
+ * Runtime process membership — which deployment unit this node is a
120
+ * member of (e.g. `trail-viewer/host`, `trail-viewer/renderer`). Nodes
121
+ * sharing a `process` are drawn inside one boundary region (grouping is
122
+ * `process ?? purl`); nodes without one sit outside every boundary
123
+ * (external actors, services, libraries).
124
+ */
125
+ process?: string;
81
126
  /** A symbol this component exposes / is (the node's identity). */
82
127
  symbol?: string;
83
128
  /**
@@ -96,7 +141,7 @@ export interface SubsystemComponent {
96
141
  capture?: 'edited' | 'analyzed' | 'referenced';
97
142
  /**
98
143
  * Graphify drill-down detail for this component, when the facet is anchored
99
- * to a graphify node. Discriminated by kind (class/function/type/module/
144
+ * to a graphify node. Discriminated by kind (class/function/type/module/store/
100
145
  * external); rendered in the detail panel on click.
101
146
  */
102
147
  detail?: GraphifyComponentDetail;
@@ -133,10 +178,10 @@ export interface SubsystemComponentEdge {
133
178
  }
134
179
 
135
180
  /**
136
- * Derive a consistent display `name` from a code `symbol` + kind.
181
+ * Derive a consistent display `name` from a code `symbol` + construct.
137
182
  *
138
- * `symbol` is the source of truth (fully-qualified code identity). For most
139
- * kinds the name is the symbol itself or its last dotted segment:
183
+ * `symbol` is the source of truth (fully-qualified code identity). The name
184
+ * is the symbol itself:
140
185
  * - class/type/module/function/script/... symbol → symbol (e.g. `SessionReader`)
141
186
  * - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
142
187
  * - module, no symbol → basename of `file` (e.g. `transcript.ts` → `transcript`) —
@@ -145,20 +190,37 @@ export interface SubsystemComponentEdge {
145
190
  */
146
191
  export function deriveNameFromSymbol(
147
192
  symbol: string | undefined,
148
- kind: SubsystemComponentKind,
193
+ construct: SubsystemComponentConstruct,
149
194
  existingName?: string,
150
195
  file?: string,
151
196
  ): string {
197
+ let name: string | undefined;
152
198
  if (symbol && symbol.trim()) {
153
- return symbol;
154
- }
155
- // Module nodes without a symbol are whole files → use the file basename.
156
- if (kind === 'module' && file) {
199
+ name = symbol;
200
+ } else if (construct === 'module' && file) {
157
201
  const base = file.split('/').pop() ?? '';
158
202
  const clean = base.replace(/\.[^.]+$/, ''); // strip extension
159
- if (clean) return clean;
203
+ if (clean) name = clean;
204
+ }
205
+ if (!name) name = existingName ?? 'untitled';
206
+
207
+ // Decoration = what the drill-down shows. Executable constructs wear `()`
208
+ // (a signature you can call); brace-bodied constructs wear ` {}` (a member
209
+ // body — fields for types/interfaces/enums, fields+methods for classes).
210
+ // Everything else (store, variable, module, external) renders bare.
211
+ if ((construct === 'function' || construct === 'method') && !name.endsWith('()')) {
212
+ name = `${name}()`;
160
213
  }
161
- return existingName ?? 'untitled';
214
+ if (
215
+ (construct === 'class' ||
216
+ construct === 'interface' ||
217
+ construct === 'type_alias' ||
218
+ construct === 'enum') &&
219
+ !name.endsWith('{}')
220
+ ) {
221
+ name = `${name} {}`;
222
+ }
223
+ return name;
162
224
  }
163
225
 
164
226
  /**
@@ -237,6 +299,9 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
237
299
  contains: '#6c5ce7', // indigo
238
300
  feeds: '#22c55e', // green — data-flow into a processor
239
301
  produces: '#e07a5f', // terracotta — emits an output type
302
+ writes: '#2f9e44', // deep green — mutates retained state
303
+ reads: '#0ea5e9', // sky — pulls from retained state
304
+ watches: '#9ca3af', // gray — observes, owns nothing
240
305
  'registers-into': '#ff6b35', // orange
241
306
  };
242
307
 
@@ -256,6 +321,9 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
256
321
  contains: 'solid',
257
322
  feeds: 'solid',
258
323
  produces: 'solid',
324
+ writes: 'solid',
325
+ reads: 'solid',
326
+ watches: 'dashed',
259
327
  'registers-into': 'dashed',
260
328
  };
261
329
 
@@ -274,14 +342,19 @@ export function packageColor(name: string): string {
274
342
  return palette[h % palette.length];
275
343
  }
276
344
 
277
- /** Color per component kind — the informative signal (rather than package). */
278
- export const KIND_COLOR: Record<SubsystemComponentKind, string> = {
279
- class: '#0893d2', // blue
280
- function: '#6c5ce7', // indigo
281
- method: '#d4a03c', // gold
282
- type: '#e07a5f', // terracotta
283
- module: '#4ec9b0', // teal
284
- external: '#b48ead', // purple
345
+ /** Color per semantic roleapplied ONLY to the role badge (top-right tab on
346
+ * nodes that carry a role). The node border stays construct-colored. Note:
347
+ * `service` currently equals the old class hex; harmless while role badges
348
+ * are the only surface using it, but pick a distinct value if roles ever
349
+ * take over node borders. */
350
+ export const ROLE_COLOR: Record<SubsystemComponentRole, string> = {
351
+ entry: '#ff6b35', // orange — boundary element
352
+ service: '#0893d2', // blue — external system
353
+ };
354
+
355
+ export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
356
+ entry: 'entry',
357
+ service: 'service',
285
358
  };
286
359
 
287
360
  /**
@@ -297,12 +370,16 @@ export function convertSubsystemToNodes(
297
370
  opts: { maxNodeWidth?: number } = {},
298
371
  ): SubsystemGraphNode[] {
299
372
  const { maxNodeWidth } = opts;
300
- // Group components by package.
373
+ // Group components into boundary regions by process when authored, else by
374
+ // package. Process is runtime membership (drawn as one boundary region);
375
+ // purl is code identity — nodes without a process (external actors,
376
+ // services, libraries) fall back to purl and sit outside every boundary.
301
377
  const byPkg = new Map<string, SubsystemComponent[]>();
302
378
  for (const c of doc.components) {
303
- const list = byPkg.get(c.purl) ?? [];
379
+ const regionKey = c.process ?? c.purl;
380
+ const list = byPkg.get(regionKey) ?? [];
304
381
  list.push(c);
305
- byPkg.set(c.purl, list);
382
+ byPkg.set(regionKey, list);
306
383
  }
307
384
 
308
385
  const nodes: SubsystemGraphNode[] = [];
@@ -322,7 +399,7 @@ export function convertSubsystemToNodes(
322
399
  const row = Math.floor(i / COLS);
323
400
  // Estimate rendered node width from the longest text line so ELK reserves
324
401
  // the right space (names can wrap, so we cap at the configurable max).
325
- const text = [c.symbol, c.name, c.file.split('/').pop() ?? '']
402
+ const text = [c.symbol, c.name, c.file?.split('/').pop() ?? '']
326
403
  .filter((t): t is string => !!t)
327
404
  .sort((a, b) => b.length - a.length)[0];
328
405
  const cap = maxNodeWidth ?? 300;
@@ -387,8 +464,8 @@ export function subsystemGraphLayoutKey(
387
464
  doc: Pick<SubsystemGraphDocument, 'components' | 'edges'>,
388
465
  ): string {
389
466
  const components = doc.components
390
- .map(({ id, purl, name, symbol, kind, file, purpose }) =>
391
- [id, purl, name, symbol ?? '', kind, file, purpose ?? ''].join('\0'))
467
+ .map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
468
+ [id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
392
469
  .sort()
393
470
  .join('\n');
394
471
  const edgeKey = doc.edges
@@ -437,7 +514,7 @@ export async function buildSubsystemGraph(
437
514
  component: {
438
515
  id: extId,
439
516
  name: label,
440
- kind: 'external',
517
+ construct: 'external',
441
518
  purl: 'external',
442
519
  file: '',
443
520
  purpose: 'cross-package integration target (not a member node)',
@@ -16,18 +16,26 @@ import {
16
16
  } from '@xyflow/react';
17
17
  import { useTheme } from '@principal-ade/industry-theme';
18
18
  import {
19
- KIND_COLOR,
20
19
  MECHANISM_COLOR,
20
+ MECHANISM_STYLE,
21
+ ROLE_COLOR,
22
+ ROLE_LABEL,
21
23
  deriveNameFromSymbol,
22
24
  type SubsystemGraphNode,
23
25
  type SubsystemGraphEdge,
24
26
  } from './model';
27
+ import { constructColorsFromPierreTheme } from '../pierre/constructColors';
28
+ import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
25
29
 
26
- export const KIND_LABEL: Record<string, string> = {
30
+ export const CONSTRUCT_LABEL: Record<string, string> = {
27
31
  class: 'class',
28
32
  function: 'function',
29
- type: 'type',
33
+ method: 'method',
34
+ interface: 'interface',
35
+ type_alias: 'type alias',
36
+ enum: 'enum',
30
37
  module: 'module',
38
+ store: 'store',
31
39
  external: 'external',
32
40
  };
33
41
 
@@ -48,6 +56,9 @@ function breakWords(s: string): string {
48
56
  export interface SubsystemGraphCallbacks {
49
57
  /** Click a component — open its file/entry point. */
50
58
  onSelect?: (componentId: string) => void;
59
+ /** Click the filename badge — open that component's file in the drawer
60
+ * (same path as clicking the file link in the declaration panel). */
61
+ onOpenFile?: (componentId: string) => void;
51
62
  /** Click an edge (or its label) — select the relationship. */
52
63
  onEdgeSelect?: (edgeId: string) => void;
53
64
  /** Hover a component (null on leave) — associates it with the file tree. */
@@ -60,15 +71,19 @@ export interface SubsystemGraphCallbacks {
60
71
  export const SUBSYSTEM_CALLBACKS: SubsystemGraphCallbacks = {};
61
72
 
62
73
  export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
63
- const { theme } = useTheme();
74
+ const { theme, mode } = useTheme();
64
75
  const { data, selected, width: nodeWidth, height: nodeHeight } = props;
65
76
  const c = data.component;
66
- const color = KIND_COLOR[c.kind] ?? '#888';
77
+ // Construct owns node color, derived from the active Pierre syntax theme —
78
+ // the same palette the declaration panel and file drawer render with. Role
79
+ // shows as the hover badge, not as color — for now; a role glyph/accent may
80
+ // come later.
81
+ const color = constructColorsFromPierreTheme(resolvePierreSyntaxThemeName(mode))[c.construct];
67
82
  const [hover, setHover] = useState(false);
68
83
  const configuredMax = SUBSYSTEM_CALLBACKS.maxNodeWidth;
69
84
  const maxWidth = configuredMax ?? 300;
70
85
  // `symbol` is the source of truth; `name` is derived from it consistently.
71
- const displayName = deriveNameFromSymbol(c.symbol, c.kind, c.name, c.file);
86
+ const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file);
72
87
  // Set while a file is open in the drawer: true → spotlight, false → dim,
73
88
  // absent (no file open) → neutral.
74
89
  const fileMatch = data.fileMatch as boolean | undefined;
@@ -117,36 +132,91 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
117
132
  fontFamily: theme.fonts.body,
118
133
  }}
119
134
  >
120
- {/* Kind + package tooltip, shown at the top on hover */}
121
- {hover && (
135
+ {/* Construct badge a small tab riding the top-right border, in the
136
+ construct color. Persistent (no hover needed); pointer-events none so
137
+ clicks pass through to the node. */}
138
+ <div
139
+ style={{
140
+ position: 'absolute',
141
+ top: -9,
142
+ left: 10,
143
+ zIndex: 1,
144
+ fontFamily: theme.fonts.monospace,
145
+ fontSize: theme.fontSizes[0] * 1.1,
146
+ letterSpacing: 0.5,
147
+ textTransform: 'uppercase',
148
+ lineHeight: '17px',
149
+ color,
150
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
151
+ border: `1px solid ${color}`,
152
+ borderRadius: 4,
153
+ padding: '0 5px',
154
+ }}
155
+ >
156
+ {CONSTRUCT_LABEL[c.construct] ?? c.construct}
157
+ </div>
158
+
159
+ {/* Role badge — top-right, only when the node carries a topology role.
160
+ Role color (entry orange / service blue) on the badge; the node
161
+ border stays construct-colored. */}
162
+ {c.role != null && (
122
163
  <div
123
164
  style={{
124
165
  position: 'absolute',
125
- top: -26,
126
- left: 0,
127
- zIndex: 1000,
128
- display: 'flex',
129
- alignItems: 'center',
130
- gap: 6,
131
- whiteSpace: 'nowrap',
132
- fontSize: theme.fontSizes[0] * 0.8,
166
+ top: -9,
167
+ right: 10,
168
+ zIndex: 1,
133
169
  fontFamily: theme.fonts.monospace,
170
+ fontSize: theme.fontSizes[0] * 1.1,
134
171
  letterSpacing: 0.5,
135
- color,
136
- background: theme.colors.background,
137
- border: `1px solid ${theme.colors.border}`,
172
+ textTransform: 'uppercase',
173
+ lineHeight: '17px',
174
+ color: ROLE_COLOR[c.role],
175
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
176
+ border: `1px solid ${ROLE_COLOR[c.role]}`,
138
177
  borderRadius: 4,
139
- padding: '1px 6px',
178
+ padding: '0 5px',
140
179
  }}
141
180
  >
142
- <span style={{ textTransform: 'uppercase' }}>
143
- {KIND_LABEL[c.kind] ?? c.kind}
144
- </span>
145
- {c.file ? ` · ${c.file.split('/').pop()}` : ''}
146
- {c.capture && c.capture !== 'edited' ? ` · ${c.capture}` : ''}
181
+ {ROLE_LABEL[c.role]}
147
182
  </div>
148
183
  )}
149
184
 
185
+ {/* Filename badge — a strip across the bottom border, shown on hover
186
+ and pinned while this node's file is open in the drawer (fileMatch).
187
+ Mirrors the construct/role tab badges; hidden for fileless nodes. */}
188
+ {(hover || fileMatch === true) && c.file ? (
189
+ <div
190
+ style={{
191
+ position: 'absolute',
192
+ bottom: -13,
193
+ left: '50%',
194
+ transform: 'translateX(-50%)',
195
+ zIndex: 1,
196
+ fontFamily: theme.fonts.monospace,
197
+ fontSize: theme.fontSizes[0] * 1.1,
198
+ letterSpacing: 0.5,
199
+ lineHeight: '17px',
200
+ whiteSpace: 'nowrap',
201
+ // filename stays neutral — the construct color belongs to the
202
+ // borders and badges, not the file name
203
+ color: theme.colors.text ?? theme.colors.textSecondary,
204
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
205
+ border: `1px solid ${color}`,
206
+ borderRadius: 4,
207
+ padding: '0 5px',
208
+ cursor: 'pointer',
209
+ }}
210
+ onClick={(e) => {
211
+ // Open the file directly; don't also toggle node selection.
212
+ e.stopPropagation();
213
+ SUBSYSTEM_CALLBACKS.onOpenFile?.(c.id);
214
+ }}
215
+ >
216
+ {c.file.split('/').pop()}
217
+ </div>
218
+ ) : null}
219
+
150
220
  {/* Purpose is no longer shown as a hover tooltip below the node — it
151
221
  lives in the declaration panel and the graphify detail payload. */}
152
222
 
@@ -168,7 +238,11 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
168
238
  {breakWords(displayName)}
169
239
  </div>
170
240
 
171
- {c.symbol && c.symbol !== displayName && (
241
+ {/* Hide the identity line when the symbol is just the title without its
242
+ decoration (`()` or ` {}`) — only show it when it adds information
243
+ (e.g. the dotted host on methods, or a different code identity). */}
244
+ {c.symbol &&
245
+ c.symbol !== displayName.replace(/ ?\{\}$/, '').replace(/\(\)$/, '') && (
172
246
  <div
173
247
  style={{
174
248
  fontSize: theme.fontSizes[0] * 0.82,
@@ -204,7 +278,9 @@ export function SubsystemEdge({
204
278
  const path = data?.elkPath ?? '';
205
279
  const mechanism = data?.mechanism ?? 'imports';
206
280
  const color = MECHANISM_COLOR[mechanism] ?? '#888';
207
- const isDashed = mechanism === 'registers-into';
281
+ // Dash style comes from the mechanism table (dashed = inverted-control or
282
+ // observational relationships: hierarchy, registration, watches).
283
+ const isDashed = MECHANISM_STYLE[mechanism] === 'dashed';
208
284
  const dimmed = data?.dimmed === true;
209
285
  const opacity = dimmed ? 0.15 : 1;
210
286
 
@@ -50,7 +50,7 @@ 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.kind;
53
+ const kind = component.detail?.kind ?? component.construct;
54
54
  if (kind === 'external') {
55
55
  const label = component.detail?.kind === 'external' ? component.detail.label : component.name;
56
56
  const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");