@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.
- 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 +32 -12
- 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 +52 -15
- 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();
|
|
@@ -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
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
|
2231
|
-
const
|
|
2232
|
-
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;
|
|
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={
|
|
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 (
|