@principal-ai/subsystems-react 0.37.12 → 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 +32 -12
  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 +52 -15
  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();
@@ -730,15 +744,17 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
730
744
  // The pane stays hidden until Pass 2 completes. Key off layout-affecting
731
745
  // fields only — declarationRef updates after verify must not re-run ELK.
732
746
  const layoutKey = useMemo(
733
- () => subsystemGraphLayoutKey({ components, relations, walkthroughs }),
734
- [components, relations, walkthroughs],
747
+ () => subsystemGraphLayoutKey({ components, relations, walkthroughs, graphifyRelations }),
748
+ [components, relations, walkthroughs, graphifyRelations],
735
749
  );
736
750
  const componentsRef = useRef(components);
737
751
  const relationsRef = useRef(relations);
738
752
  const walkthroughsRef = useRef(walkthroughs);
753
+ const graphifyRelationsRef = useRef(graphifyRelations);
739
754
  componentsRef.current = components;
740
755
  relationsRef.current = relations;
741
756
  walkthroughsRef.current = walkthroughs;
757
+ graphifyRelationsRef.current = graphifyRelations;
742
758
 
743
759
  // Track measured dimensions from React Flow's dimension changes.
744
760
  // These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
@@ -764,6 +780,8 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
764
780
  maxNodeWidth,
765
781
  showEdgeLabels,
766
782
  showSingletonFrames,
783
+ graphifyRelations: graphifyRelationsRef.current,
784
+ orderByLine,
767
785
  })
768
786
  .then(({ nodes, edges: e, absoluteRects }) => {
769
787
  if (!alive) return;
@@ -813,7 +831,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
813
831
  const gen = ++pass2GenRef.current;
814
832
  void buildSubsystemGraph(
815
833
  { components, relations, walkthroughs },
816
- { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames },
834
+ { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, graphifyRelations, orderByLine },
817
835
  )
818
836
  .then(({ nodes, edges: e, absoluteRects }) => {
819
837
  if (gen !== pass2GenRef.current) return;
@@ -826,7 +844,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
826
844
  // Reveal Pass 1 layout rather than leaving the cover up forever.
827
845
  setLayoutReady(true);
828
846
  });
829
- }, [built.nodes, components, relations, walkthroughs, maxNodeWidth, showEdgeLabels]);
847
+ }, [built.nodes, components, relations, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels]);
830
848
 
831
849
  // After Pass 1 commits, try Pass 2 immediately with retained measurements.
832
850
  // Same-id live updates often get no new React Flow `dimensions` events, so
@@ -1121,11 +1139,17 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1121
1139
  };
1122
1140
  };
1123
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.
1124
1144
  const edgeInView = (e: Edge): boolean => {
1125
- const mechanism = (e.data as { mechanism?: string } | undefined)?.mechanism ?? 'uses';
1126
- return resolvedEdgeView === 'relations'
1127
- ? isRelationMechanism(mechanism)
1128
- : 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);
1129
1153
  };
1130
1154
  if (openedEdgeIds || focusEdgeIds || previewEdgeIds) {
1131
1155
  return baseEdges.map((e) => {
@@ -1884,6 +1908,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1884
1908
  .map((e) => {
1885
1909
  const d = e.data as {
1886
1910
  mechanism?: string;
1911
+ provenance?: SubsystemEdgeProvenance;
1887
1912
  dimmed?: boolean;
1888
1913
  labelX?: number;
1889
1914
  labelY?: number;
@@ -1892,6 +1917,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
1892
1917
  return {
1893
1918
  id: e.id,
1894
1919
  mechanism: d?.mechanism ?? 'uses',
1920
+ provenance: d?.provenance,
1895
1921
  dimmed: d?.dimmed === true,
1896
1922
  midX: d?.labelX ?? 0,
1897
1923
  midY: d?.labelY ?? 0,
@@ -2227,9 +2253,19 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
2227
2253
  }}
2228
2254
  >
2229
2255
  {edgeLabels.map((lbl) => {
2230
- const mechanism = lbl.mechanism as SubsystemEdgeMechanism;
2231
- const color = MECHANISM_COLOR[mechanism] ?? '#888';
2232
- 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;
2233
2269
  const screenX = lbl.midX * viewport.zoom + viewport.x;
2234
2270
  const screenY = lbl.midY * viewport.zoom + viewport.y;
2235
2271
  const text = lbl.stepNos?.length
@@ -2239,7 +2275,7 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
2239
2275
  <div
2240
2276
  key={lbl.id}
2241
2277
  data-edge-label={lbl.id}
2242
- title={verifiable ? undefined : 'Not directly verifiable with graphify'}
2278
+ title={labelTitle}
2243
2279
  onClick={(e) => {
2244
2280
  e.stopPropagation();
2245
2281
  selectEdge(lbl.id);
@@ -2720,6 +2756,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
2720
2756
  components: props.components,
2721
2757
  relations: props.relations,
2722
2758
  walkthroughs: props.walkthroughs,
2759
+ graphifyRelations: props.graphifyRelations,
2723
2760
  });
2724
2761
 
2725
2762
  return (