@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.
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +14 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +52 -20
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +123 -7
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +91 -12
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +9 -5
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +9 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +21 -2
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/graphify/__fixtures__/ego-samples.json +545 -0
- package/src/index.ts +12 -1
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -9
- package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +5 -5
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +5 -5
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +354 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +75 -25
- package/src/subsystem/model.test.ts +75 -2
- package/src/subsystem/model.ts +201 -17
- package/src/subsystem/nodes.tsx +10 -6
- package/src/subsystem/toC4.test.ts +3 -3
- 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', '
|
|
107
|
-
['edge-comp', 'group-comp', '
|
|
108
|
-
['model-view', 'layers', '
|
|
109
|
-
['use-drawings', 'model-view', '
|
|
110
|
-
['model-view', 'node-comp', '
|
|
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', '
|
|
125
|
-
['is-rollout', 'record', '
|
|
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', '
|
|
265
|
-
['trail-view', 'bridge', '
|
|
266
|
-
['trail-view', 'use-trail', '
|
|
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', '
|
|
142
|
-
['resolver', 'reftypes', '
|
|
143
|
-
['resolver', 'engine', '
|
|
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', '
|
|
1119
|
+
['subscriber', 'messages', 'method'],
|
|
1120
1120
|
['state', 'listener', 'feeds'],
|
|
1121
|
-
['deep', 'record', '
|
|
1122
|
-
['rows', 'record', '
|
|
1121
|
+
['deep', 'record', 'method'],
|
|
1122
|
+
['rows', 'record', 'method'],
|
|
1123
1123
|
]);
|
|
1124
1124
|
|
|
1125
1125
|
function TypeFamilySpectrumDemo() {
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
|
2218
|
-
const
|
|
2219
|
-
const
|
|
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={
|
|
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 (
|