@principal-ai/subsystems-react 0.37.11 → 0.38.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 (39) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  6. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
  7. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  8. package/dist/subsystem/SubsystemComponentGraph.d.ts +14 -1
  9. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  10. package/dist/subsystem/SubsystemComponentGraph.js +52 -20
  11. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  12. package/dist/subsystem/model.d.ts +123 -7
  13. package/dist/subsystem/model.d.ts.map +1 -1
  14. package/dist/subsystem/model.js +91 -12
  15. package/dist/subsystem/model.js.map +1 -1
  16. package/dist/subsystem/nodes.d.ts.map +1 -1
  17. package/dist/subsystem/nodes.js +9 -5
  18. package/dist/subsystem/nodes.js.map +1 -1
  19. package/dist/utils/elkLayout.d.ts +9 -0
  20. package/dist/utils/elkLayout.d.ts.map +1 -1
  21. package/dist/utils/elkLayout.js +21 -2
  22. package/dist/utils/elkLayout.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/graphify/__fixtures__/ego-samples.json +545 -0
  25. package/src/index.ts +12 -1
  26. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +2 -2
  27. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -9
  28. package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +4 -4
  29. package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +5 -5
  30. package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +5 -5
  31. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
  32. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
  33. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +354 -0
  34. package/src/subsystem/SubsystemComponentGraph.tsx +75 -25
  35. package/src/subsystem/model.test.ts +75 -2
  36. package/src/subsystem/model.ts +201 -17
  37. package/src/subsystem/nodes.tsx +10 -6
  38. package/src/subsystem/toC4.test.ts +3 -3
  39. package/src/utils/elkLayout.ts +32 -1
@@ -103,11 +103,11 @@ const deepComponents: SubsystemComponent[] = [
103
103
  ];
104
104
 
105
105
  const deepEdges = graphSpecFromEdges([
106
- ['node-comp', 'group-comp', 'references'],
107
- ['edge-comp', 'group-comp', 'references'],
108
- ['model-view', 'layers', 'references'],
109
- ['use-drawings', 'model-view', 'references'],
110
- ['model-view', 'node-comp', 'references'],
106
+ ['node-comp', 'group-comp', 'method'],
107
+ ['edge-comp', 'group-comp', 'method'],
108
+ ['model-view', 'layers', 'method'],
109
+ ['use-drawings', 'model-view', 'method'],
110
+ ['model-view', 'node-comp', 'method'],
111
111
  ]);
112
112
 
113
113
  /** Collapsed module badges show `first/…/last`. Click one to expand the full
@@ -121,8 +121,8 @@ const moduleComponents: SubsystemComponent[] = [
121
121
  ];
122
122
 
123
123
  const moduleEdges = graphSpecFromEdges([
124
- ['parse', 'record', 'references'],
125
- ['is-rollout', 'record', 'references'],
124
+ ['parse', 'record', 'method'],
125
+ ['is-rollout', 'record', 'method'],
126
126
  ['reader', 'normalize', 'method'],
127
127
  ['normalize', 'parse', 'calls'],
128
128
  ['normalize', 'tool-name', 'calls'],
@@ -261,9 +261,9 @@ const nestedComponents: SubsystemComponent[] = [
261
261
  ];
262
262
 
263
263
  const nestedEdges = graphSpecFromEdges([
264
- ['boot', 'write-session', 'references'],
265
- ['trail-view', 'bridge', 'references'],
266
- ['trail-view', 'use-trail', 'references'],
264
+ ['boot', 'write-session', 'method'],
265
+ ['trail-view', 'bridge', 'method'],
266
+ ['trail-view', 'use-trail', 'method'],
267
267
  ['boot', 'create-host', 'calls'],
268
268
  ['create-host', 'write-session', 'calls'],
269
269
  ['write-session', 'session-store', 'writes'],
@@ -138,9 +138,9 @@ const multiRepoComponents = components([
138
138
  ]);
139
139
 
140
140
  const multiRepoEdges = graphSpecFromEdges([
141
- ['detail', 'reftypes', 'references'],
142
- ['resolver', 'reftypes', 'references'],
143
- ['resolver', 'engine', 'references'],
141
+ ['detail', 'reftypes', 'method'],
142
+ ['resolver', 'reftypes', 'method'],
143
+ ['resolver', 'engine', 'method'],
144
144
  ['rewire', 'resolver', 'calls'],
145
145
  ]);
146
146
 
@@ -1116,10 +1116,10 @@ const typeSpectrumComponents: SubsystemComponent[] = [
1116
1116
 
1117
1117
  const typeSpectrumEdges = graphSpecFromEdges([
1118
1118
  ['listener', 'subscriber', 'uses'],
1119
- ['subscriber', 'messages', 'references'],
1119
+ ['subscriber', 'messages', 'method'],
1120
1120
  ['state', 'listener', 'feeds'],
1121
- ['deep', 'record', 'references'],
1122
- ['rows', 'record', 'references'],
1121
+ ['deep', 'record', 'method'],
1122
+ ['rows', 'record', 'method'],
1123
1123
  ]);
1124
1124
 
1125
1125
  function TypeFamilySpectrumDemo() {
@@ -33,7 +33,6 @@ const RELATION_TYPES = new Set<string>([
33
33
  'implements',
34
34
  'mixes_in',
35
35
  'method',
36
- 'references',
37
36
  ]);
38
37
 
39
38
  export function relations(
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Ego-graph prototype: reuse the subsystem component nodes to render one
3
+ * graphify symbol at the center plus its real connections.
4
+ *
5
+ * The center + neighbors come from `graphify/__fixtures__/ego-samples.json`
6
+ * (extracted from this repo's cached graph). Each graphify node is mapped onto
7
+ * a `SubsystemComponent`; the graphify edges are passed through the dedicated
8
+ * `graphifyRelations` input, so they render with the separate
9
+ * `GRAPHIFY_RELATION_COLOR` palette — visually distinct from authored subsystem
10
+ * mechanisms — and keep their raw verb as the label (no folding to
11
+ * `references`).
12
+ */
13
+ import React from 'react';
14
+ import '@xyflow/react/dist/style.css';
15
+ import type { Meta, StoryObj } from '@storybook/react';
16
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
17
+ import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
18
+ import type {
19
+ SubsystemComponent,
20
+ SubsystemComponentConstruct,
21
+ SubsystemGraphifyRelation,
22
+ } from '../../../subsystem/model';
23
+ import { inferConstructFromGraphify } from '../../../graphify';
24
+ import type { GraphifyEdge, GraphifyNode } from '../../../graphify';
25
+ import egoSamples from '../../../graphify/__fixtures__/ego-samples.json';
26
+
27
+ const meta = {
28
+ title: 'Subsystem/EgoGraph',
29
+ component: SubsystemComponentGraph,
30
+ parameters: { layout: 'fullscreen' },
31
+ tags: ['autodocs'],
32
+ decorators: [
33
+ (Story) => (
34
+ <ThemeProvider theme={defaultEditorTheme}>
35
+ <Story />
36
+ </ThemeProvider>
37
+ ),
38
+ ],
39
+ } satisfies Meta<typeof SubsystemComponentGraph>;
40
+
41
+ export default meta;
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Fixture shape
46
+ // ---------------------------------------------------------------------------
47
+
48
+ type FixtureNode = {
49
+ id: string;
50
+ label: string;
51
+ file_type?: string;
52
+ source_file?: string;
53
+ source_location?: string;
54
+ type?: string;
55
+ };
56
+
57
+ type FixtureEdge = {
58
+ source: string;
59
+ target: string;
60
+ relation: string;
61
+ confidence?: string;
62
+ context?: string;
63
+ source_file?: string;
64
+ source_location?: string;
65
+ };
66
+
67
+ type FixtureCategory = {
68
+ center?: FixtureNode;
69
+ degree?: number;
70
+ neighborCount?: number;
71
+ neighbors?: FixtureNode[];
72
+ edges?: FixtureEdge[];
73
+ };
74
+
75
+ type Category = 'class' | 'function' | 'method' | 'type' | 'module';
76
+
77
+ const categories = egoSamples.categories as unknown as Record<Category, FixtureCategory>;
78
+ const authoredCategories = egoSamples.authoredCategories as unknown as Record<
79
+ string,
80
+ { component: SubsystemComponent }
81
+ >;
82
+
83
+ const REPO_PURL = 'pkg:github/principal-ai/subsystem-modeling';
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // graphify -> subsystem mapping
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /** The fixture center's category, expressed as a subsystem construct. */
90
+ const CENTER_CONSTRUCT: Record<Category, SubsystemComponentConstruct> = {
91
+ class: 'class',
92
+ function: 'function',
93
+ method: 'method',
94
+ type: 'interface',
95
+ // Subsystem models draw files as module *frames*, not nodes — there is no
96
+ // `module` construct. Stand in with `external` so the file node still draws.
97
+ module: 'external',
98
+ };
99
+
100
+ /** Coarse graphify-inferred construct -> subsystem construct. */
101
+ const INFERRED_TO_CONSTRUCT: Record<string, SubsystemComponentConstruct> = {
102
+ class: 'class',
103
+ function: 'function',
104
+ method: 'method',
105
+ type: 'interface',
106
+ module: 'external',
107
+ unknown: 'external',
108
+ };
109
+
110
+ function parseLine(location?: string): number | undefined {
111
+ const m = /^L(\d+)$/.exec(location ?? '');
112
+ return m ? Number(m[1]) : undefined;
113
+ }
114
+
115
+ function purlFor(sourceFile?: string): string {
116
+ return sourceFile ? `${REPO_PURL}#${sourceFile}` : REPO_PURL;
117
+ }
118
+
119
+ /**
120
+ * Graphify labels carry their own decoration: functions end `name()`, methods
121
+ * start `.name()`. A subsystem `symbol` is the bare code identity and the node
122
+ * renderer re-derives the `()` / `.` decoration itself, so passing a decorated
123
+ * label through would double it (`readFoo()()` → the name printed twice).
124
+ * Strip the graphify decoration and hand back a clean symbol.
125
+ */
126
+ function stripGraphifyDecoration(label: string): string {
127
+ let s = label.trim();
128
+ if (s.endsWith('()')) s = s.slice(0, -2);
129
+ if (s.startsWith('.')) s = s.slice(1);
130
+ return s;
131
+ }
132
+
133
+ function toComponent(
134
+ alias: string,
135
+ node: FixtureNode,
136
+ construct: SubsystemComponentConstruct,
137
+ purpose: string,
138
+ lineOverride?: number,
139
+ ): SubsystemComponent {
140
+ const line = lineOverride ?? parseLine(node.source_location);
141
+ const symbol = stripGraphifyDecoration(node.label);
142
+ return {
143
+ alias,
144
+ name: symbol,
145
+ construct,
146
+ symbol,
147
+ file: node.source_file ?? '',
148
+ purl: purlFor(node.source_file),
149
+ purpose,
150
+ // `line` also seeds ELK's same-layer ordering (orderByLine).
151
+ ...(line != null ? { line } : {}),
152
+ ...(line != null
153
+ ? {
154
+ declarationRef: {
155
+ file: node.source_file ?? '',
156
+ startLine: line,
157
+ lineHash: 'ego-fixture',
158
+ capturedAt: new Date(0).toISOString(),
159
+ },
160
+ }
161
+ : {}),
162
+ };
163
+ }
164
+
165
+ interface EgoGraph {
166
+ components: SubsystemComponent[];
167
+ graphifyRelations: SubsystemGraphifyRelation[];
168
+ }
169
+
170
+ /** Turn one fixture category into a subsystem graph centered on its symbol. */
171
+ function toEgoGraph(cat: FixtureCategory, category: Category): EgoGraph {
172
+ const center = cat.center;
173
+ if (!center) return { components: [], graphifyRelations: [] };
174
+
175
+ const edges = cat.edges ?? [];
176
+ const neighbors = cat.neighbors ?? [];
177
+
178
+ // graphify file nodes (`inferConstructFromGraphify` → 'module') are not
179
+ // symbols — they are the CONTAINER of symbols. The center's own file becomes
180
+ // a `module` boundary (a frame around its members) and its `contains` edges
181
+ // are dropped: membership is expressed by the frame, exactly as an authored
182
+ // model does it. Files reached as neighbors stay as nodes (they are external
183
+ // to the center's file), so only the center's file is framed.
184
+ const isFileNode = (n: FixtureNode): boolean =>
185
+ inferConstructFromGraphify(
186
+ n as unknown as GraphifyNode,
187
+ edges as unknown as GraphifyEdge[],
188
+ ).construct === 'module';
189
+
190
+ const centerFile = center.source_file;
191
+ const fileNodeIds = new Set(
192
+ neighbors.filter((n) => isFileNode(n)).map((n) => n.id),
193
+ );
194
+ // Neighbors that live in the center's file stay; the file node for that file
195
+ // is dropped (its containment is now the frame).
196
+ const knotless = neighbors.filter(
197
+ (n) => !(fileNodeIds.has(n.id) && n.source_file === centerFile),
198
+ );
199
+
200
+ const aliasById = new Map<string, string>([[center.id, 'center']]);
201
+ knotless.forEach((n, i) => aliasById.set(n.id, `n${i}`));
202
+ const aliasFor = (id: string): string | undefined => aliasById.get(id);
203
+
204
+ const centerConstruct = CENTER_CONSTRUCT[category];
205
+ const framedCenter: SubsystemComponent = {
206
+ ...toComponent(
207
+ 'center',
208
+ center,
209
+ centerConstruct,
210
+ `graphify center — ${center.label}`,
211
+ ),
212
+ ...(centerConstruct !== 'external' && centerFile ? { module: centerFile } : {}),
213
+ };
214
+ const components: SubsystemComponent[] = [framedCenter];
215
+
216
+ // `component.line` is the CALL-SITE line, not the declaration line: the
217
+ // edge's `source_location` on an edge FROM the center (L115, L119, …), which
218
+ // `orderByLine` uses to stack callees in the order the center calls them.
219
+ // (The neighbor's own node `source_location` — where it is declared — is
220
+ // deliberately ignored here; declaration order would read arbitrarily.)
221
+ const callLineByAlias = new Map<string, number>();
222
+ for (const e of edges) {
223
+ const from = aliasFor(e.source);
224
+ const to = aliasFor(e.target);
225
+ const line = parseLine(e.source_location);
226
+ if (from === 'center' && to && line != null) callLineByAlias.set(to, line);
227
+ }
228
+
229
+ knotless.forEach((n, i) => {
230
+ const inferred = inferConstructFromGraphify(
231
+ n as unknown as GraphifyNode,
232
+ edges as unknown as GraphifyEdge[],
233
+ ).construct;
234
+ const construct = INFERRED_TO_CONSTRUCT[inferred] ?? 'external';
235
+ const purpose =
236
+ inferred === 'module'
237
+ ? 'graphify file node in ANOTHER file — kept as a node (outside the center frame)'
238
+ : `graphify ${inferred} neighbor (${n.file_type ?? 'code'})`;
239
+ // Members of the center's file join the frame; others stand alone.
240
+ const module = n.source_file === centerFile ? centerFile : undefined;
241
+ const c = toComponent(`n${i}`, n, construct, purpose, callLineByAlias.get(`n${i}`));
242
+ components.push(module ? { ...c, module } : c);
243
+ });
244
+
245
+ // Raw graphify edges — passed straight through (no verb folding). The
246
+ // canvas colors them from GRAPHIFY_RELATION_COLOR and draws them dashed.
247
+ // `contains` is dropped: the frame carries containment, not an edge.
248
+ const graphifyRelations: SubsystemGraphifyRelation[] = edges
249
+ .map((e, i): SubsystemGraphifyRelation | null => {
250
+ if (e.relation === 'contains') return null;
251
+ const from = aliasFor(e.source);
252
+ const to = aliasFor(e.target);
253
+ if (!from || !to) return null;
254
+ return {
255
+ id: `g${i}`,
256
+ from,
257
+ to,
258
+ relation: e.relation,
259
+ refs: e.source_file ? [e.source_file] : undefined,
260
+ line: parseLine(e.source_location),
261
+ confidence: e.confidence,
262
+ context: e.context,
263
+ };
264
+ })
265
+ .filter((x): x is SubsystemGraphifyRelation => x != null);
266
+
267
+ return { components, graphifyRelations };
268
+ }
269
+
270
+ function EgoDemo({ category, orderByLine }: { category: Category; orderByLine: boolean }) {
271
+ const graph = React.useMemo(() => toEgoGraph(categories[category], category), [category]);
272
+ const cat = categories[category];
273
+ const verbs = [...new Set(graph.graphifyRelations.map((r) => r.relation))];
274
+ return (
275
+ <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
276
+ <SubsystemComponentGraph
277
+ components={graph.components}
278
+ relations={[]}
279
+ graphifyRelations={graph.graphifyRelations}
280
+ orderByLine={orderByLine}
281
+ showSingletonFrames
282
+ title={`Ego graph — ${category}`}
283
+ description={`graphify center ${cat.center?.label ?? '?'} · ${cat.degree ?? 0} edges · verbs: ${verbs.join(', ')}${orderByLine ? ' · ordered by call-site line' : ''}`}
284
+ showEdgeLabels
285
+ hideSidebar={false}
286
+ />
287
+ </div>
288
+ );
289
+ }
290
+
291
+ /** Everything on disk for the center: node + neighbor nodes + raw edges. */
292
+ function RawDump({ category }: { category: Category }) {
293
+ const cat = categories[category];
294
+ return (
295
+ <pre
296
+ style={{
297
+ margin: 0,
298
+ padding: 12,
299
+ fontSize: 12,
300
+ lineHeight: 1.5,
301
+ fontFamily: 'monospace',
302
+ color: '#ddd',
303
+ background: '#111',
304
+ height: '100vh',
305
+ overflow: 'auto',
306
+ boxSizing: 'border-box',
307
+ }}
308
+ >
309
+ {JSON.stringify({ category, ...cat }, null, 2)}
310
+ </pre>
311
+ );
312
+ }
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // Stories — one per graphify node category
316
+ // ---------------------------------------------------------------------------
317
+
318
+ export const Class: Story = { render: () => <EgoDemo category="class" orderByLine /> };
319
+ export const Function: Story = { render: () => <EgoDemo category="function" orderByLine /> };
320
+ export const Method: Story = { render: () => <EgoDemo category="method" orderByLine /> };
321
+ export const Type: Story = { render: () => <EgoDemo category="type" orderByLine /> };
322
+ export const Module: Story = { render: () => <EgoDemo category="module" orderByLine /> };
323
+
324
+ /** Same centers, ELK's default ordering — compare against the line-ordered set. */
325
+ export const MethodReorderOff: Story = {
326
+ render: () => <EgoDemo category="method" orderByLine={false} />,
327
+ };
328
+
329
+ /** The raw extracted data behind each center — no rendering involved. */
330
+ export const Raw: Story = { render: () => <RawDump category="class" /> };
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // Authored-only constructs (no graphify node) pulled from the model store
334
+ // ---------------------------------------------------------------------------
335
+
336
+ const authoredComponents: SubsystemComponent[] = [
337
+ authoredCategories.store?.component,
338
+ authoredCategories.external?.component,
339
+ authoredCategories.custom_entity?.component,
340
+ ].filter(Boolean) as SubsystemComponent[];
341
+
342
+ /** store / external / custom_entity — authored nodes, no graphify anchor. */
343
+ export const AuthoredConstructs: Story = {
344
+ render: () => (
345
+ <div style={{ width: '100%', height: '100vh' }}>
346
+ <SubsystemComponentGraph
347
+ components={authoredComponents}
348
+ relations={[]}
349
+ title="Authored-only constructs"
350
+ description="store / external / custom_entity — reused cast from stored subsystem models (no graphify node, no edges)."
351
+ />
352
+ </div>
353
+ ),
354
+ };
@@ -40,14 +40,15 @@ import {
40
40
  isConstructsOnlyModel,
41
41
  isRelationMechanism,
42
42
  isWalkthroughMechanism,
43
- MECHANISM_COLOR,
43
+ edgeColor,
44
44
  MECHANISM_DESCRIPTIONS,
45
45
  subsystemGraphLayoutKey,
46
46
  walkthroughStepGraphEdgeId,
47
47
  type SubsystemComponentEdge,
48
48
  type SubsystemComponent,
49
+ type SubsystemEdgeProvenance,
49
50
  type SubsystemEdgeView,
50
- type SubsystemEdgeMechanism,
51
+ type SubsystemGraphifyRelation,
51
52
  type SubsystemRelation,
52
53
  type SubsystemWalkthrough,
53
54
  } from './model';
@@ -174,6 +175,19 @@ export interface SubsystemComponentGraphProps {
174
175
  * (unselected ones dimmed); everything else is hidden.
175
176
  */
176
177
  walkthroughs?: SubsystemWalkthrough[];
178
+ /**
179
+ * graphify-native relations (raw static-graph edges: `imports`, `contains`,
180
+ * `re_exports`, …). Display-only, drawn with the separate
181
+ * `GRAPHIFY_RELATION_COLOR` palette so they read as derived facts, distinct
182
+ * from authored subsystem mechanisms. Visible in the `relations` edge view.
183
+ */
184
+ graphifyRelations?: readonly SubsystemGraphifyRelation[];
185
+ /**
186
+ * Order same-layer nodes by `component.line` (ascending) instead of ELK's
187
+ * crossing-minimizer — reads a source region top-to-bottom. Components need a
188
+ * `line` for this to have any effect. @default false
189
+ */
190
+ orderByLine?: boolean;
177
191
  /**
178
192
  * Deep-link target: when set, the matching walkthrough is selected on mount
179
193
  * — its steps expanded and its flow focused on the canvas. Hosts use this
@@ -470,7 +484,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
470
484
  measured: { w: number; h: number } | null;
471
485
  }
472
486
 
473
- function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, walkthroughFocusDurationMs = 300, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey }: InnerProps) {
487
+ function Inner({ components, relations, walkthroughs, graphifyRelations, orderByLine, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, walkthroughFocusDurationMs = 300, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey }: InnerProps) {
474
488
  const { theme } = useTheme();
475
489
  const { fitView, fitBounds } = useReactFlow();
476
490
  const viewport = useViewport();
@@ -641,31 +655,50 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
641
655
  if (drawerTarget?.kind !== 'walkthrough' || !walkthroughs) return null;
642
656
  return walkthroughs.find((t) => t.id === drawerTarget.walkthroughId) ?? null;
643
657
  }, [drawerTarget, walkthroughs]);
658
+ // Autoplay focus: a sidebar-less embed has no expanded flow to "open", so
659
+ // branding every step as a selection would hide all other nodes. Instead the
660
+ // autoplay sets this (flow id + step) to frame the hop and dim the rest —
661
+ // same treatment as hover — without hiding anything. Declared before the
662
+ // overlay memos, which read it to title the canvas chip + step bar.
663
+ const [autoPlayFocus, setAutoPlayFocus] = useState<
664
+ { walkthroughId: string; stepIndex: number } | null
665
+ >(null);
644
666
  // Ref mirror of the drawer's walkthrough id so the symbol resolver stays
645
667
  // stable across graph re-renders (the drawer is memoized on callback identity).
646
668
  const drawerWalkthroughIdRef = useRef<string | null>(null);
647
669
  drawerWalkthroughIdRef.current = focusedWalkthrough?.id ?? null;
648
670
 
649
- // Walkthrough shown on the canvas title chip (focus or hover/autoplay highlight).
671
+ // Walkthrough shown on the canvas title chip (focus, hover highlight, or
672
+ // autoplay focus).
650
673
  const overlayWalkthroughTitle = useMemo(() => {
651
674
  if (!showWalkthroughTitle || !walkthroughs?.length) return null;
652
- const id = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId;
675
+ const id =
676
+ focusedWalkthroughId ??
677
+ autoPlayFocus?.walkthroughId ??
678
+ hoveredWalkthroughStep?.walkthroughId;
653
679
  if (!id) return null;
654
680
  return walkthroughs.find((t) => t.id === id)?.title ?? null;
655
681
  }, [
656
682
  showWalkthroughTitle,
657
683
  walkthroughs,
658
684
  focusedWalkthroughId,
685
+ autoPlayFocus,
659
686
  hoveredWalkthroughStep,
660
687
  ]);
661
688
 
662
689
  // Active step for the bottom-of-title progress + annotation chip.
663
690
  const overlayWalkthroughStep = useMemo(() => {
664
691
  if (!showWalkthroughTitle || !walkthroughs?.length) return null;
665
- const tlId = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId ?? null;
692
+ const tlId =
693
+ focusedWalkthroughId ??
694
+ autoPlayFocus?.walkthroughId ??
695
+ hoveredWalkthroughStep?.walkthroughId ??
696
+ null;
666
697
  let stepIndex: number | null = null;
667
698
  if (focusedWalkthroughId != null) {
668
699
  stepIndex = focusedStepIndex;
700
+ } else if (autoPlayFocus != null) {
701
+ stepIndex = autoPlayFocus.stepIndex;
669
702
  } else if (hoveredWalkthroughStep != null) {
670
703
  stepIndex = hoveredWalkthroughStep.stepIndex;
671
704
  }
@@ -683,6 +716,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
683
716
  walkthroughs,
684
717
  focusedWalkthroughId,
685
718
  focusedStepIndex,
719
+ autoPlayFocus,
686
720
  hoveredWalkthroughStep,
687
721
  ]);
688
722
 
@@ -710,15 +744,17 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
710
744
  // The pane stays hidden until Pass 2 completes. Key off layout-affecting
711
745
  // fields only — declarationRef updates after verify must not re-run ELK.
712
746
  const layoutKey = useMemo(
713
- () => subsystemGraphLayoutKey({ components, relations, walkthroughs }),
714
- [components, relations, walkthroughs],
747
+ () => subsystemGraphLayoutKey({ components, relations, walkthroughs, graphifyRelations }),
748
+ [components, relations, walkthroughs, graphifyRelations],
715
749
  );
716
750
  const componentsRef = useRef(components);
717
751
  const relationsRef = useRef(relations);
718
752
  const walkthroughsRef = useRef(walkthroughs);
753
+ const graphifyRelationsRef = useRef(graphifyRelations);
719
754
  componentsRef.current = components;
720
755
  relationsRef.current = relations;
721
756
  walkthroughsRef.current = walkthroughs;
757
+ graphifyRelationsRef.current = graphifyRelations;
722
758
 
723
759
  // Track measured dimensions from React Flow's dimension changes.
724
760
  // These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
@@ -744,6 +780,8 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
744
780
  maxNodeWidth,
745
781
  showEdgeLabels,
746
782
  showSingletonFrames,
783
+ graphifyRelations: graphifyRelationsRef.current,
784
+ orderByLine,
747
785
  })
748
786
  .then(({ nodes, edges: e, absoluteRects }) => {
749
787
  if (!alive) return;
@@ -793,7 +831,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
793
831
  const gen = ++pass2GenRef.current;
794
832
  void buildSubsystemGraph(
795
833
  { components, relations, walkthroughs },
796
- { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames },
834
+ { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, graphifyRelations, orderByLine },
797
835
  )
798
836
  .then(({ nodes, edges: e, absoluteRects }) => {
799
837
  if (gen !== pass2GenRef.current) return;
@@ -806,7 +844,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
806
844
  // Reveal Pass 1 layout rather than leaving the cover up forever.
807
845
  setLayoutReady(true);
808
846
  });
809
- }, [built.nodes, components, relations, walkthroughs, maxNodeWidth, showEdgeLabels]);
847
+ }, [built.nodes, components, relations, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels]);
810
848
 
811
849
  // After Pass 1 commits, try Pass 2 immediately with retained measurements.
812
850
  // Same-id live updates often get no new React Flow `dimensions` events, so
@@ -894,13 +932,6 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
894
932
  // without a stale closure value.
895
933
  const selectedEdgeIdRef = useRef<string | null>(null);
896
934
  selectedEdgeIdRef.current = selectedEdgeId;
897
- // Autoplay focus: a sidebar-less embed has no expanded flow to "open", so
898
- // branding every step as a selection would hide all other nodes. Instead the
899
- // autoplay sets this (flow id + step) to frame the hop and dim the rest —
900
- // same treatment as hover — without hiding anything.
901
- const [autoPlayFocus, setAutoPlayFocus] = useState<
902
- { walkthroughId: string; stepIndex: number } | null
903
- >(null);
904
935
  // Edge ids in walkthrough focus (an active flow's edge set, or a single
905
936
  // step's edge). Used to frame the camera. `null` = no walkthrough focus.
906
937
  const focusEdgeIds = useMemo(() => {
@@ -1108,11 +1139,17 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1108
1139
  };
1109
1140
  };
1110
1141
  // Edges outside the selected view are hidden entirely (labels included).
1142
+ // graphify-native edges belong to the relations view (they are static
1143
+ // topology, not runtime hops) regardless of their verb.
1111
1144
  const edgeInView = (e: Edge): boolean => {
1112
- const mechanism = (e.data as { mechanism?: string } | undefined)?.mechanism ?? 'uses';
1113
- return resolvedEdgeView === 'relations'
1114
- ? isRelationMechanism(mechanism)
1115
- : isWalkthroughMechanism(mechanism);
1145
+ const d = e.data as
1146
+ | { mechanism?: string; provenance?: SubsystemEdgeProvenance }
1147
+ | undefined;
1148
+ const mechanism = d?.mechanism ?? 'uses';
1149
+ if (resolvedEdgeView === 'relations') {
1150
+ return d?.provenance === 'graphify' || isRelationMechanism(mechanism);
1151
+ }
1152
+ return isWalkthroughMechanism(mechanism);
1116
1153
  };
1117
1154
  if (openedEdgeIds || focusEdgeIds || previewEdgeIds) {
1118
1155
  return baseEdges.map((e) => {
@@ -1871,6 +1908,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1871
1908
  .map((e) => {
1872
1909
  const d = e.data as {
1873
1910
  mechanism?: string;
1911
+ provenance?: SubsystemEdgeProvenance;
1874
1912
  dimmed?: boolean;
1875
1913
  labelX?: number;
1876
1914
  labelY?: number;
@@ -1879,6 +1917,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1879
1917
  return {
1880
1918
  id: e.id,
1881
1919
  mechanism: d?.mechanism ?? 'uses',
1920
+ provenance: d?.provenance,
1882
1921
  dimmed: d?.dimmed === true,
1883
1922
  midX: d?.labelX ?? 0,
1884
1923
  midY: d?.labelY ?? 0,
@@ -2214,9 +2253,19 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
2214
2253
  }}
2215
2254
  >
2216
2255
  {edgeLabels.map((lbl) => {
2217
- const mechanism = lbl.mechanism as SubsystemEdgeMechanism;
2218
- const color = MECHANISM_COLOR[mechanism] ?? '#888';
2219
- const verifiable = MECHANISM_DESCRIPTIONS.find(([m]) => m === mechanism)?.[2] ?? true;
2256
+ const mechanism = lbl.mechanism;
2257
+ const isGraphify = lbl.provenance === 'graphify';
2258
+ const color = edgeColor({ mechanism, provenance: lbl.provenance });
2259
+ // Label chrome stays the crisp rounded box for every edge — graphify
2260
+ // provenance is carried by hue (GRAPHIFY_RELATION_COLOR), the dashed
2261
+ // stroke, and the tooltip, NOT by the "ambiguous" cloud silhouette
2262
+ // (that idiom is reserved for soft authored mechanisms).
2263
+ const verifiable = true;
2264
+ const labelTitle = isGraphify
2265
+ ? 'Derived from graphify (static symbol graph)'
2266
+ : MECHANISM_DESCRIPTIONS.find(([m]) => m === mechanism)?.[2] === false
2267
+ ? 'Not directly verifiable with graphify'
2268
+ : undefined;
2220
2269
  const screenX = lbl.midX * viewport.zoom + viewport.x;
2221
2270
  const screenY = lbl.midY * viewport.zoom + viewport.y;
2222
2271
  const text = lbl.stepNos?.length
@@ -2226,7 +2275,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
2226
2275
  <div
2227
2276
  key={lbl.id}
2228
2277
  data-edge-label={lbl.id}
2229
- title={verifiable ? undefined : 'Not directly verifiable with graphify'}
2278
+ title={labelTitle}
2230
2279
  onClick={(e) => {
2231
2280
  e.stopPropagation();
2232
2281
  selectEdge(lbl.id);
@@ -2707,6 +2756,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
2707
2756
  components: props.components,
2708
2757
  relations: props.relations,
2709
2758
  walkthroughs: props.walkthroughs,
2759
+ graphifyRelations: props.graphifyRelations,
2710
2760
  });
2711
2761
 
2712
2762
  return (