@principal-ai/subsystems-react 0.23.4 → 0.24.1
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 +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +3 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +2 -2
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +30 -0
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -0
- package/dist/subsystem/ConstructsCatalog.js +349 -0
- package/dist/subsystem/ConstructsCatalog.js.map +1 -0
- package/dist/subsystem/DiagnosticToggle.d.ts +46 -0
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -0
- package/dist/subsystem/DiagnosticToggle.js +95 -0
- package/dist/subsystem/DiagnosticToggle.js.map +1 -0
- package/dist/subsystem/IssueList.d.ts +102 -0
- package/dist/subsystem/IssueList.d.ts.map +1 -0
- package/dist/subsystem/IssueList.js +282 -0
- package/dist/subsystem/IssueList.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +40 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +103 -65
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +32 -2
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +47 -0
- package/dist/subsystem/model.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +42 -0
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +287 -0
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +138 -0
- package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +159 -0
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +130 -0
- package/src/subsystem/ComponentDeclaration.tsx +4 -1
- package/src/subsystem/ConstructsCatalog.tsx +644 -0
- package/src/subsystem/DiagnosticToggle.tsx +160 -0
- package/src/subsystem/IssueList.tsx +488 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +156 -42
- package/src/subsystem/model.test.ts +34 -0
- package/src/subsystem/model.ts +72 -2
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* inside one labeled boundary frame; nodes without one sit outside every
|
|
8
8
|
* boundary. Clicking a component invokes `onSelect`.
|
|
9
9
|
*
|
|
10
|
+
* When the model has components but no topology or walkthrough edges, the
|
|
11
|
+
* canvas is a constructs catalog (list + signature) instead of a graph.
|
|
12
|
+
*
|
|
10
13
|
* This is a focused fork of the package's `GraphRenderer` pipeline (same ELK
|
|
11
14
|
* edge routing, delayed fitView, Background/Controls/MiniMap, node/edge type
|
|
12
15
|
* injection, onNodeClick) adapted to the subsystem model — read-only.
|
|
@@ -37,18 +40,25 @@ import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
|
37
40
|
import {
|
|
38
41
|
buildSubsystemGraph,
|
|
39
42
|
deriveGraphEdges,
|
|
43
|
+
isConstructsOnlyModel,
|
|
44
|
+
isRelationMechanism,
|
|
45
|
+
isWalkthroughMechanism,
|
|
40
46
|
MECHANISM_COLOR,
|
|
41
47
|
MECHANISM_DESCRIPTIONS,
|
|
42
48
|
subsystemGraphLayoutKey,
|
|
43
49
|
walkthroughStepGraphEdgeId,
|
|
44
50
|
type SubsystemComponentEdge,
|
|
45
51
|
type SubsystemComponent,
|
|
52
|
+
type SubsystemEdgeView,
|
|
46
53
|
type SubsystemEdgeMechanism,
|
|
47
54
|
type SubsystemRelation,
|
|
48
55
|
type SubsystemWalkthrough,
|
|
49
56
|
} from './model';
|
|
57
|
+
import { ConstructsCatalog } from './ConstructsCatalog';
|
|
50
58
|
import type { SubsystemOpenFileOptions } from './declarationRef';
|
|
51
59
|
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
60
|
+
import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './DiagnosticToggle';
|
|
61
|
+
import { SubsystemIssueList, type SubsystemIssue } from './IssueList';
|
|
52
62
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
53
63
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
54
64
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
@@ -99,6 +109,17 @@ export interface SubsystemComponentGraphProps {
|
|
|
99
109
|
maxNodeWidth?: number;
|
|
100
110
|
/** Show edge labels (mechanism names) on the graph. @default true */
|
|
101
111
|
showEdgeLabels?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Which edge vocabulary the canvas draws. The relation and walkthrough
|
|
114
|
+
* vocabularies are disjoint, so a graph carrying both shows one or the
|
|
115
|
+
* other — never both. Edges outside the view are hidden (labels go too).
|
|
116
|
+
* - `relations`: topology relation edges (`imports`, `extends`, …)
|
|
117
|
+
* - `walkthroughs`: walkthrough hop edges (`calls`, `feeds`, …), including
|
|
118
|
+
* step numbers when a flow is focused/hovered
|
|
119
|
+
* Defaults to `walkthroughs` when the model has walkthroughs but no
|
|
120
|
+
* relations (e.g. flow-only models), otherwise `relations`.
|
|
121
|
+
*/
|
|
122
|
+
edgeView?: SubsystemEdgeView;
|
|
102
123
|
/** Subsystem title displayed in the sidebar. */
|
|
103
124
|
title?: string;
|
|
104
125
|
/**
|
|
@@ -146,6 +167,29 @@ export interface SubsystemComponentGraphProps {
|
|
|
146
167
|
sidebarExtra?: ReactNode;
|
|
147
168
|
/** Rendered in the sidebar under the description (e.g. selection inspector). */
|
|
148
169
|
sidebarAfterDescription?: ReactNode;
|
|
170
|
+
/**
|
|
171
|
+
* Diagnostics status chip in the sidebar title row (beside the description
|
|
172
|
+
* toggle). Shows the last verification pass's state as an icon color + count;
|
|
173
|
+
* `onOpen` opens the full report. Omit to hide the chip.
|
|
174
|
+
*/
|
|
175
|
+
diagnostic?: SubsystemDiagnostic;
|
|
176
|
+
/**
|
|
177
|
+
* Verification issues for this graph (audit findings). When diagnostics are
|
|
178
|
+
* active (see `showIssues`) the sidebar's bottom panel becomes the issue
|
|
179
|
+
* list instead of the file tree / walkthroughs.
|
|
180
|
+
*/
|
|
181
|
+
issues?: SubsystemIssue[];
|
|
182
|
+
/**
|
|
183
|
+
* Force the sidebar into diagnostics mode. Defaults to `true` whenever
|
|
184
|
+
* `issues` is non-empty (and nothing else has been forced).
|
|
185
|
+
*/
|
|
186
|
+
showIssues?: boolean;
|
|
187
|
+
/** Click an issue — focus its target on the graph / open detail. */
|
|
188
|
+
onSelectIssue?: (issue: SubsystemIssue) => void;
|
|
189
|
+
/** Apply an issue's deterministic fix. */
|
|
190
|
+
onApplyIssueFix?: (issue: SubsystemIssue) => void;
|
|
191
|
+
/** Hover an issue — transiently highlight its target (null on leave). */
|
|
192
|
+
onHoverIssue?: (issue: SubsystemIssue | null) => void;
|
|
149
193
|
/**
|
|
150
194
|
* Host-injected reader/renderer for the bottom file drawer, keyed by
|
|
151
195
|
* repo-root-relative path. Opening happens on declaration/file-tree clicks.
|
|
@@ -260,7 +304,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
260
304
|
measured: { w: number; h: number } | null;
|
|
261
305
|
}
|
|
262
306
|
|
|
263
|
-
function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
307
|
+
function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
264
308
|
const { theme } = useTheme();
|
|
265
309
|
const { fitView } = useReactFlow();
|
|
266
310
|
const viewport = useViewport();
|
|
@@ -268,6 +312,13 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
268
312
|
() => deriveGraphEdges({ relations, walkthroughs }),
|
|
269
313
|
[relations, walkthroughs],
|
|
270
314
|
);
|
|
315
|
+
// One edge vocabulary at a time. When the caller doesn't pick, follow the
|
|
316
|
+
// model: a flow-only graph defaults to walkthrough edges rather than empty.
|
|
317
|
+
const resolvedEdgeView: SubsystemEdgeView =
|
|
318
|
+
edgeView ??
|
|
319
|
+
(relations.length === 0 && (walkthroughs?.length ?? 0) > 0
|
|
320
|
+
? 'walkthroughs'
|
|
321
|
+
: 'relations');
|
|
271
322
|
const [built, setBuilt] = useState<{ nodes: Node[]; edges: Edge[] }>({
|
|
272
323
|
nodes: [],
|
|
273
324
|
edges: [],
|
|
@@ -808,6 +859,13 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
808
859
|
markerEnd: nextMarker,
|
|
809
860
|
};
|
|
810
861
|
};
|
|
862
|
+
// Edges outside the selected view are hidden entirely (labels included).
|
|
863
|
+
const edgeInView = (e: Edge): boolean => {
|
|
864
|
+
const mechanism = (e.data as { mechanism?: string } | undefined)?.mechanism ?? 'imports';
|
|
865
|
+
return resolvedEdgeView === 'relations'
|
|
866
|
+
? isRelationMechanism(mechanism)
|
|
867
|
+
: isWalkthroughMechanism(mechanism);
|
|
868
|
+
};
|
|
811
869
|
if (openedEdgeIds || focusEdgeIds || previewEdgeIds) {
|
|
812
870
|
return baseEdges.map((e) => {
|
|
813
871
|
const vis = flowElementVisibility({
|
|
@@ -817,13 +875,18 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
817
875
|
anySelected: focusEdgeIds != null,
|
|
818
876
|
});
|
|
819
877
|
const dimmed = previewEdgeIds ? vis.hidden || !previewEdgeIds.has(e.id) : vis.dimmed;
|
|
820
|
-
const hidden = vis.hidden;
|
|
878
|
+
const hidden = vis.hidden || !edgeInView(e);
|
|
821
879
|
return { ...paint(e, dimmed), hidden };
|
|
822
880
|
});
|
|
823
881
|
}
|
|
824
|
-
if (
|
|
825
|
-
|
|
826
|
-
|
|
882
|
+
if (selectedEdgeId) {
|
|
883
|
+
return baseEdges.map((e) => ({
|
|
884
|
+
...paint(e, e.id !== selectedEdgeId),
|
|
885
|
+
hidden: !edgeInView(e),
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
return baseEdges.map((e) => ({ ...e, hidden: !edgeInView(e) }));
|
|
889
|
+
}, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds, resolvedEdgeView]);
|
|
827
890
|
|
|
828
891
|
const onNodesChange = useCallback(
|
|
829
892
|
(changes: NodeChange[]) => {
|
|
@@ -902,6 +965,10 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
902
965
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
903
966
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
904
967
|
const hasWalkthroughs = useMemo(() => (walkthroughs?.length ?? 0) > 0, [walkthroughs]);
|
|
968
|
+
// Diagnostics mode: the bottom panel shows the issue list instead of the
|
|
969
|
+
// file tree / walkthroughs. Explicit `showIssues` wins; otherwise any issues
|
|
970
|
+
// present turn it on.
|
|
971
|
+
const issuesActive = showIssues ?? (issues?.length ?? 0) > 0;
|
|
905
972
|
const [collapsedRepos, setCollapsedRepos] = useState<Set<string>>(new Set());
|
|
906
973
|
const toggleRepoCollapsed = useCallback((key: string) => {
|
|
907
974
|
setCollapsedRepos((prev) => {
|
|
@@ -1325,8 +1392,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1325
1392
|
.filter((e) => {
|
|
1326
1393
|
if (e.hidden) return false;
|
|
1327
1394
|
const d = e?.data as { dimmed?: boolean } | undefined;
|
|
1328
|
-
|
|
1329
|
-
return true;
|
|
1395
|
+
return !d?.dimmed;
|
|
1330
1396
|
})
|
|
1331
1397
|
.map((e) => {
|
|
1332
1398
|
const d = e.data as {
|
|
@@ -1394,7 +1460,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1394
1460
|
{/* Sidebar: scrollable title/description on top, files or flows pinned below.
|
|
1395
1461
|
The description hides by default so files/flows get the room; the
|
|
1396
1462
|
title-row toggle reveals it, and the lower panel yields back to 50%. */}
|
|
1397
|
-
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1463
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1398
1464
|
<div
|
|
1399
1465
|
style={{
|
|
1400
1466
|
width: sidebarWidth,
|
|
@@ -1418,7 +1484,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1418
1484
|
}}
|
|
1419
1485
|
>
|
|
1420
1486
|
{sidebarExtra}
|
|
1421
|
-
{(title || description) && (
|
|
1487
|
+
{(title || description || diagnostic) && (
|
|
1422
1488
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
|
|
1423
1489
|
{title && (
|
|
1424
1490
|
<h2
|
|
@@ -1435,34 +1501,47 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1435
1501
|
{title}
|
|
1436
1502
|
</h2>
|
|
1437
1503
|
)}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1504
|
+
<div
|
|
1505
|
+
style={{
|
|
1506
|
+
display: 'flex',
|
|
1507
|
+
alignItems: 'center',
|
|
1508
|
+
gap: 4,
|
|
1509
|
+
flexShrink: 0,
|
|
1510
|
+
// Title's flex:1 already pushes this cluster right; without a
|
|
1511
|
+
// title, keep the actions flush right instead of flush-left.
|
|
1512
|
+
marginLeft: title ? undefined : 'auto',
|
|
1513
|
+
}}
|
|
1514
|
+
>
|
|
1515
|
+
{diagnostic && <SubsystemDiagnosticToggle {...diagnostic} />}
|
|
1516
|
+
{description && (
|
|
1517
|
+
<button
|
|
1518
|
+
type="button"
|
|
1519
|
+
aria-expanded={descriptionVisible}
|
|
1520
|
+
aria-label={descriptionVisible ? 'Hide description' : 'Show description'}
|
|
1521
|
+
title={descriptionVisible ? 'Hide description' : 'Show description'}
|
|
1522
|
+
onMouseEnter={() => setDescToggleHover(true)}
|
|
1523
|
+
onMouseLeave={() => setDescToggleHover(false)}
|
|
1524
|
+
onClick={() => setDescriptionVisible((v) => !v)}
|
|
1525
|
+
style={{
|
|
1526
|
+
display: 'inline-flex',
|
|
1527
|
+
alignItems: 'center',
|
|
1528
|
+
justifyContent: 'center',
|
|
1529
|
+
flexShrink: 0,
|
|
1530
|
+
width: 22,
|
|
1531
|
+
height: 22,
|
|
1532
|
+
padding: 0,
|
|
1533
|
+
border: 'none',
|
|
1534
|
+
borderRadius: 4,
|
|
1535
|
+
background: descToggleHover ? theme.colors.border : 'transparent',
|
|
1536
|
+
color: descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1537
|
+
cursor: 'pointer',
|
|
1538
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
1539
|
+
}}
|
|
1540
|
+
>
|
|
1541
|
+
{descriptionVisible ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
1542
|
+
</button>
|
|
1543
|
+
)}
|
|
1544
|
+
</div>
|
|
1466
1545
|
</div>
|
|
1467
1546
|
)}
|
|
1468
1547
|
{description && descriptionVisible && (
|
|
@@ -1483,7 +1562,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1483
1562
|
)}
|
|
1484
1563
|
{sidebarAfterDescription}
|
|
1485
1564
|
</div>
|
|
1486
|
-
{(treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1565
|
+
{(treeFilePaths.length > 0 || hasWalkthroughs || issuesActive) && (
|
|
1487
1566
|
<div
|
|
1488
1567
|
style={{
|
|
1489
1568
|
...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
|
|
@@ -1495,6 +1574,15 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1495
1574
|
overflow: 'hidden',
|
|
1496
1575
|
}}
|
|
1497
1576
|
>
|
|
1577
|
+
{issuesActive ? (
|
|
1578
|
+
<SubsystemIssueList
|
|
1579
|
+
issues={issues ?? []}
|
|
1580
|
+
onSelectIssue={onSelectIssue}
|
|
1581
|
+
onApplyFix={onApplyIssueFix}
|
|
1582
|
+
onHoverIssue={onHoverIssue}
|
|
1583
|
+
/>
|
|
1584
|
+
) : (
|
|
1585
|
+
<>
|
|
1498
1586
|
{hasWalkthroughs && (
|
|
1499
1587
|
<div
|
|
1500
1588
|
role="tablist"
|
|
@@ -1608,13 +1696,15 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1608
1696
|
})}
|
|
1609
1697
|
</>
|
|
1610
1698
|
) : null}
|
|
1699
|
+
</>
|
|
1700
|
+
)}
|
|
1611
1701
|
</div>
|
|
1612
1702
|
)}
|
|
1613
1703
|
</div>
|
|
1614
1704
|
)}
|
|
1615
1705
|
|
|
1616
1706
|
{/* Drag handle between sidebar and canvas — resize the left panel. */}
|
|
1617
|
-
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1707
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1618
1708
|
<div
|
|
1619
1709
|
onMouseDown={onSidebarResizeStart}
|
|
1620
1710
|
aria-label="Resize sidebar"
|
|
@@ -2403,6 +2493,12 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
2403
2493
|
return () => ro.disconnect();
|
|
2404
2494
|
}, []);
|
|
2405
2495
|
|
|
2496
|
+
const constructsOnly = isConstructsOnlyModel({
|
|
2497
|
+
components: props.components,
|
|
2498
|
+
relations: props.relations,
|
|
2499
|
+
walkthroughs: props.walkthroughs,
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2406
2502
|
return (
|
|
2407
2503
|
<div
|
|
2408
2504
|
ref={wrapRef}
|
|
@@ -2416,9 +2512,27 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
2416
2512
|
}}
|
|
2417
2513
|
>
|
|
2418
2514
|
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
2419
|
-
|
|
2420
|
-
<
|
|
2421
|
-
|
|
2515
|
+
{constructsOnly ? (
|
|
2516
|
+
<ConstructsCatalog
|
|
2517
|
+
components={props.components}
|
|
2518
|
+
onSelect={props.onSelect}
|
|
2519
|
+
title={props.title}
|
|
2520
|
+
hideSidebar={props.hideSidebar}
|
|
2521
|
+
description={props.description}
|
|
2522
|
+
diagnostic={props.diagnostic}
|
|
2523
|
+
sidebarExtra={props.sidebarExtra}
|
|
2524
|
+
sidebarAfterDescription={props.sidebarAfterDescription}
|
|
2525
|
+
renderFileViewer={props.renderFileViewer}
|
|
2526
|
+
renderFileView={props.renderFileView}
|
|
2527
|
+
onFileSelect={props.onFileSelect}
|
|
2528
|
+
onVerifyComponent={props.onVerifyComponent}
|
|
2529
|
+
componentVerification={props.componentVerification}
|
|
2530
|
+
/>
|
|
2531
|
+
) : (
|
|
2532
|
+
<ReactFlowProvider>
|
|
2533
|
+
<Inner {...props} measured={size} />
|
|
2534
|
+
</ReactFlowProvider>
|
|
2535
|
+
)}
|
|
2422
2536
|
</div>
|
|
2423
2537
|
</div>
|
|
2424
2538
|
);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildSubsystemGraph,
|
|
14
14
|
componentPackageKey,
|
|
15
15
|
deriveNameFromSymbol,
|
|
16
|
+
isConstructsOnlyModel,
|
|
16
17
|
constructBadgeLabel,
|
|
17
18
|
constructBadgeColor,
|
|
18
19
|
FRAMEWORK_BADGE_COLOR,
|
|
@@ -642,3 +643,36 @@ describe('subsystem graph model', () => {
|
|
|
642
643
|
expect(subsystemGraphLayoutKey(base)).toBe(subsystemGraphLayoutKey(withRef));
|
|
643
644
|
});
|
|
644
645
|
});
|
|
646
|
+
|
|
647
|
+
describe('isConstructsOnlyModel', () => {
|
|
648
|
+
test('true when components exist and there are no edges', () => {
|
|
649
|
+
expect(isConstructsOnlyModel({ components: comps, relations: [], walkthroughs: [] })).toBe(true);
|
|
650
|
+
expect(isConstructsOnlyModel({ components: comps })).toBe(true);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test('false when empty, or when relations or walkthrough hops exist', () => {
|
|
654
|
+
expect(isConstructsOnlyModel({ components: [], relations: [] })).toBe(false);
|
|
655
|
+
expect(isConstructsOnlyModel({ components: comps, relations })).toBe(false);
|
|
656
|
+
expect(
|
|
657
|
+
isConstructsOnlyModel({
|
|
658
|
+
components: comps,
|
|
659
|
+
relations: [],
|
|
660
|
+
walkthroughs: [
|
|
661
|
+
{
|
|
662
|
+
id: 'wt',
|
|
663
|
+
title: 'flow',
|
|
664
|
+
steps: [
|
|
665
|
+
{
|
|
666
|
+
from: 'transcript',
|
|
667
|
+
to: 'reader',
|
|
668
|
+
mechanism: 'calls',
|
|
669
|
+
file: 'transcript.ts',
|
|
670
|
+
line: 1,
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
}),
|
|
676
|
+
).toBe(false);
|
|
677
|
+
});
|
|
678
|
+
});
|
package/src/subsystem/model.ts
CHANGED
|
@@ -117,6 +117,14 @@ export type SubsystemEdgeMechanism =
|
|
|
117
117
|
| SubsystemRelationType
|
|
118
118
|
| SubsystemWalkthroughMechanism;
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Which edge vocabulary the canvas draws. The two vocabularies are disjoint;
|
|
122
|
+
* a view shows only edges (and their labels) from the selected source.
|
|
123
|
+
* - `relations`: only topology relation edges (`imports`, `extends`, …)
|
|
124
|
+
* - `walkthroughs`: only walkthrough hop edges (`calls`, `feeds`, …)
|
|
125
|
+
*/
|
|
126
|
+
export type SubsystemEdgeView = 'relations' | 'walkthroughs';
|
|
127
|
+
|
|
120
128
|
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
121
129
|
export interface SubsystemComponent {
|
|
122
130
|
id: string;
|
|
@@ -323,8 +331,8 @@ export function walkthroughStepGraphEdgeId(
|
|
|
323
331
|
}
|
|
324
332
|
|
|
325
333
|
export function deriveGraphEdges(doc: {
|
|
326
|
-
relations?: SubsystemRelation[];
|
|
327
|
-
walkthroughs?: SubsystemWalkthrough[];
|
|
334
|
+
relations?: readonly SubsystemRelation[];
|
|
335
|
+
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
328
336
|
}): SubsystemComponentEdge[] {
|
|
329
337
|
const byId = new Map<string, SubsystemComponentEdge>();
|
|
330
338
|
for (const r of doc.relations ?? []) {
|
|
@@ -355,6 +363,22 @@ export function deriveGraphEdges(doc: {
|
|
|
355
363
|
return [...byId.values()];
|
|
356
364
|
}
|
|
357
365
|
|
|
366
|
+
/**
|
|
367
|
+
* True when the model has components but no topology or walkthrough edges.
|
|
368
|
+
* Those snapshots are a catalog of declarations, not a graph.
|
|
369
|
+
*/
|
|
370
|
+
export function isConstructsOnlyModel(doc: {
|
|
371
|
+
components: readonly { id: string }[];
|
|
372
|
+
relations?: readonly SubsystemRelation[];
|
|
373
|
+
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
374
|
+
}): boolean {
|
|
375
|
+
if (doc.components.length === 0) return false;
|
|
376
|
+
return deriveGraphEdges({
|
|
377
|
+
relations: doc.relations,
|
|
378
|
+
walkthroughs: doc.walkthroughs,
|
|
379
|
+
}).length === 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
358
382
|
/**
|
|
359
383
|
* Derive a consistent display `name` from a code `symbol` + construct.
|
|
360
384
|
*
|
|
@@ -747,6 +771,52 @@ export interface SubsystemGraphEdgeData extends Record<string, unknown> {
|
|
|
747
771
|
|
|
748
772
|
export type SubsystemGraphEdge = Edge<SubsystemGraphEdgeData>;
|
|
749
773
|
|
|
774
|
+
/**
|
|
775
|
+
* Runtime vocabulary of relation types — mirrors `SubsystemRelationType`.
|
|
776
|
+
* Used to split derived display edges into their relation vs walkthrough
|
|
777
|
+
* source (the two unions are disjoint).
|
|
778
|
+
*/
|
|
779
|
+
export const SUBSYSTEM_RELATION_TYPES = [
|
|
780
|
+
'imports',
|
|
781
|
+
'extends',
|
|
782
|
+
'inherits',
|
|
783
|
+
'implements',
|
|
784
|
+
'mixes_in',
|
|
785
|
+
'method',
|
|
786
|
+
'references',
|
|
787
|
+
] as const satisfies readonly SubsystemRelationType[];
|
|
788
|
+
|
|
789
|
+
/** Runtime vocabulary of walkthrough hop mechanisms — mirrors `SubsystemWalkthroughMechanism`. */
|
|
790
|
+
export const SUBSYSTEM_WALKTHROUGH_MECHANISMS = [
|
|
791
|
+
'calls',
|
|
792
|
+
'uses',
|
|
793
|
+
'feeds',
|
|
794
|
+
'produces',
|
|
795
|
+
'writes',
|
|
796
|
+
'reads',
|
|
797
|
+
'watches',
|
|
798
|
+
'registers-into',
|
|
799
|
+
] as const satisfies readonly SubsystemWalkthroughMechanism[];
|
|
800
|
+
|
|
801
|
+
const RELATION_TYPE_SET: ReadonlySet<string> = new Set(SUBSYSTEM_RELATION_TYPES);
|
|
802
|
+
const WALKTHROUGH_MECHANISM_SET: ReadonlySet<string> = new Set(
|
|
803
|
+
SUBSYSTEM_WALKTHROUGH_MECHANISMS,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
/** True when a mechanism belongs to the topology relation vocabulary. */
|
|
807
|
+
export function isRelationMechanism(
|
|
808
|
+
mechanism: string,
|
|
809
|
+
): mechanism is SubsystemRelationType {
|
|
810
|
+
return RELATION_TYPE_SET.has(mechanism);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** True when a mechanism belongs to the walkthrough hop vocabulary. */
|
|
814
|
+
export function isWalkthroughMechanism(
|
|
815
|
+
mechanism: string,
|
|
816
|
+
): mechanism is SubsystemWalkthroughMechanism {
|
|
817
|
+
return WALKTHROUGH_MECHANISM_SET.has(mechanism);
|
|
818
|
+
}
|
|
819
|
+
|
|
750
820
|
export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
751
821
|
imports: '#0893d2', // blue
|
|
752
822
|
calls: '#4ec9b0', // teal
|