@principal-ai/subsystems-react 0.41.0 → 0.43.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/C4Graph/c4Fixture.js +1 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +6 -9
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +15 -50
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +2 -3
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +2 -3
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +5 -5
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +9 -18
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +9 -12
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +42 -112
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +18 -51
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +2 -51
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/toC4.d.ts +4 -4
- package/dist/subsystem/toC4.d.ts.map +1 -1
- package/dist/subsystem/toC4.js +3 -7
- package/dist/subsystem/toC4.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +0 -4
- package/src/stories/Subsystem/AgentsPanel.stories.tsx +3 -5
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +1 -4
- package/src/stories/Subsystem/C4Graph/c4Fixture.ts +1 -1
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -4
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +19 -19
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +0 -2
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +2 -6
- package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +21 -21
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +0 -3
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +30 -32
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +1 -26
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +3 -25
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +7 -8
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +9 -11
- package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +3 -6
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +8 -6
- package/src/stories/Subsystem/ComponentGraph/ProposedWalkthroughs.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/Reorder.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +26 -28
- package/src/stories/Subsystem/ComponentGraph/WalkthroughAutoplay.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/WorkspacePackageFrames.stories.tsx +2 -3
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +18 -58
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +0 -2
- package/src/subsystem/C4Graph.tsx +2 -3
- package/src/subsystem/IssueList.tsx +12 -22
- package/src/subsystem/SubsystemComponentGraph.tsx +55 -136
- package/src/subsystem/model.test.ts +37 -24
- package/src/subsystem/model.ts +18 -101
- package/src/subsystem/toC4.test.ts +5 -12
- package/src/subsystem/toC4.ts +5 -9
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Prototype test case: feed it a `C4Model` from `toC4()` and it draws the
|
|
5
5
|
* system as a compound frame, its containers (or components) as boxes, and
|
|
6
|
-
* externals/actors outside the system.
|
|
7
|
-
* edges
|
|
8
|
-
* the source components it rolled up.
|
|
6
|
+
* externals/actors outside the system. `walkthrough` hops render as flow
|
|
7
|
+
* edges. Click a box to list the source components it rolled up.
|
|
9
8
|
*
|
|
10
9
|
* Same interaction language as the other graphs: React Flow + ELK + the shared
|
|
11
10
|
* `GRAPH_NAV_PROPS` chrome. Deliberately small — no measurement passes, no
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SubsystemIssueList — the diagnostics sidebar list.
|
|
3
3
|
*
|
|
4
4
|
* A catalogue of verification issues (audit findings), grouped by *target* so
|
|
5
|
-
* every broken component /
|
|
5
|
+
* every broken component / module / flow clusters together. Each
|
|
6
6
|
* card is severity-colored, names its kind, states the message, and offers the
|
|
7
7
|
* deterministic fix inline when one exists.
|
|
8
8
|
*
|
|
@@ -38,7 +38,6 @@ export type SubsystemIssueSeverity = 'error' | 'info';
|
|
|
38
38
|
|
|
39
39
|
export type SubsystemIssueTargetKind =
|
|
40
40
|
| 'component'
|
|
41
|
-
| 'relation'
|
|
42
41
|
| 'module'
|
|
43
42
|
| 'walkthrough'
|
|
44
43
|
| 'step'
|
|
@@ -48,8 +47,8 @@ export type SubsystemIssueTargetKind =
|
|
|
48
47
|
/**
|
|
49
48
|
* The four verification layers, in order. Mirrors the docs' progression:
|
|
50
49
|
* constructs → static topology → dynamic topology → walkthrough.
|
|
51
|
-
* Static topology =
|
|
52
|
-
*
|
|
50
|
+
* Static topology = package/module (containment); dynamic topology = process
|
|
51
|
+
* (runtime).
|
|
53
52
|
*/
|
|
54
53
|
export type SubsystemIssueCategory =
|
|
55
54
|
| 'repo'
|
|
@@ -90,7 +89,7 @@ export const SUBSYSTEM_ISSUE_CATEGORY_LABEL: Record<
|
|
|
90
89
|
walkthrough: 'Walkthrough',
|
|
91
90
|
};
|
|
92
91
|
|
|
93
|
-
/** Default `kind` → layer.
|
|
92
|
+
/** Default `kind` → layer. Module containment is static; process nest is dynamic. */
|
|
94
93
|
const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
|
|
95
94
|
missing_file: 'construct',
|
|
96
95
|
symbol_ambiguous: 'construct',
|
|
@@ -102,10 +101,7 @@ const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
|
|
|
102
101
|
signature_unconfirmed: 'construct',
|
|
103
102
|
repo_unresolved: 'repo',
|
|
104
103
|
graphify_unavailable: 'repo',
|
|
105
|
-
|
|
106
|
-
topology_import_unconfirmed: 'static-topology',
|
|
107
|
-
topology_relation_unconfirmed: 'static-topology',
|
|
108
|
-
boundary_module_file_mismatch: 'dynamic-topology',
|
|
104
|
+
boundary_module_file_mismatch: 'static-topology',
|
|
109
105
|
boundary_process_nest_disagree: 'dynamic-topology',
|
|
110
106
|
walkthrough: 'walkthrough',
|
|
111
107
|
};
|
|
@@ -189,7 +185,7 @@ export interface SubsystemIssueTarget {
|
|
|
189
185
|
id?: string;
|
|
190
186
|
/** Display label (component name, `from → to`, module path, flow title). */
|
|
191
187
|
label: string;
|
|
192
|
-
/** Optional sub-label (
|
|
188
|
+
/** Optional sub-label (step number, …). */
|
|
193
189
|
detail?: string;
|
|
194
190
|
/**
|
|
195
191
|
* For a `step` target: the walkthrough id, plus a 0-based index into that
|
|
@@ -231,8 +227,8 @@ export function humanizeIssueKind(kind: string): string {
|
|
|
231
227
|
* Curated labels. Construct-layer findings stay fully qualified ("Construct
|
|
232
228
|
* declaration file missing", "Construct type mismatch") rather than relying on
|
|
233
229
|
* the category header — this UI is new and the extra words read clearer.
|
|
234
|
-
*
|
|
235
|
-
*
|
|
230
|
+
* Boundary findings drop the `boundary_` prefix since the layer already
|
|
231
|
+
* scopes them. Explicit `issue.kindLabel` wins.
|
|
236
232
|
*/
|
|
237
233
|
const KIND_LABEL: Record<string, string> = {
|
|
238
234
|
missing_file: 'Construct declaration file missing',
|
|
@@ -245,9 +241,6 @@ const KIND_LABEL: Record<string, string> = {
|
|
|
245
241
|
signature_unconfirmed: 'Construct signature unconfirmed',
|
|
246
242
|
repo_unresolved: 'No repository clone',
|
|
247
243
|
graphify_unavailable: 'Graphify cache unavailable',
|
|
248
|
-
topology_broken_endpoint: 'Construct relationship broken',
|
|
249
|
-
topology_import_unconfirmed: 'Import unconfirmed',
|
|
250
|
-
topology_relation_unconfirmed: 'Relationship unconfirmed',
|
|
251
244
|
boundary_module_file_mismatch: 'Declaration file outside module',
|
|
252
245
|
boundary_process_nest_disagree: 'Module spans multiple process contexts',
|
|
253
246
|
walkthrough: 'Step issue',
|
|
@@ -261,8 +254,8 @@ export function issueKindLabel(issue: SubsystemIssue): string {
|
|
|
261
254
|
/**
|
|
262
255
|
* Display order within the list, by verification pipeline — not severity.
|
|
263
256
|
* Mirrors the per-component check order (file → symbol → declaration → type →
|
|
264
|
-
* signature), then
|
|
265
|
-
* remaining layers. Within a stage, "unconfirmed" precedes "mismatch": you
|
|
257
|
+
* signature), then the boundary layers (module membership → process nest) and
|
|
258
|
+
* the remaining layers. Within a stage, "unconfirmed" precedes "mismatch": you
|
|
266
259
|
* reach a verdict before you can find it wrong.
|
|
267
260
|
*/
|
|
268
261
|
const KIND_ORDER: Record<string, number> = {
|
|
@@ -278,12 +271,9 @@ const KIND_ORDER: Record<string, number> = {
|
|
|
278
271
|
construct_mismatch: 15,
|
|
279
272
|
signature_unconfirmed: 16,
|
|
280
273
|
signature_mismatch: 17,
|
|
281
|
-
// Static topology —
|
|
282
|
-
topology_broken_endpoint: 20,
|
|
283
|
-
topology_relation_unconfirmed: 21,
|
|
284
|
-
topology_import_unconfirmed: 22,
|
|
274
|
+
// Static topology — package/module membership
|
|
285
275
|
boundary_module_file_mismatch: 23,
|
|
286
|
-
// Runtime topology
|
|
276
|
+
// Runtime topology — process containment
|
|
287
277
|
boundary_process_nest_disagree: 30,
|
|
288
278
|
// Walkthrough
|
|
289
279
|
walkthrough: 40,
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
deriveGraphEdges,
|
|
40
40
|
isConstructsOnlyModel,
|
|
41
41
|
moduleGroupNodeId,
|
|
42
|
-
isRelationMechanism,
|
|
43
42
|
isWalkthroughMechanism,
|
|
44
43
|
edgeColor,
|
|
45
44
|
MECHANISM_DESCRIPTIONS,
|
|
@@ -52,7 +51,6 @@ import {
|
|
|
52
51
|
type SubsystemGraphifyRelation,
|
|
53
52
|
type SubsystemNodeIssue,
|
|
54
53
|
type SubsystemRegionIssue,
|
|
55
|
-
type SubsystemRelation,
|
|
56
54
|
type SubsystemWalkthrough,
|
|
57
55
|
} from './model';
|
|
58
56
|
import { ConstructsCatalog } from './ConstructsCatalog';
|
|
@@ -137,15 +135,18 @@ type DrawerTarget =
|
|
|
137
135
|
|
|
138
136
|
/**
|
|
139
137
|
* Per-model UI state persisted to `localStorage`, keyed by `persistKey`.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
138
|
+
* Stores the walkthrough working set (which flows are expanded and which
|
|
139
|
+
* flow/step is selected) and which sidebar/diagnostics tab is showing, so
|
|
140
|
+
* tabbing away from a model and back lands you where you left off. Transient
|
|
141
|
+
* things (hover, drag, camera) are not saved.
|
|
143
142
|
*/
|
|
144
143
|
interface PersistedViewState {
|
|
145
144
|
expandedWalkthroughs?: string[];
|
|
146
145
|
focusedWalkthroughId?: string | null;
|
|
147
146
|
focusedStepIndex?: number | null;
|
|
148
147
|
sidebarWidth?: number;
|
|
148
|
+
sidebarView?: 'files' | 'walkthroughs';
|
|
149
|
+
diagnosticsTab?: 'issues' | 'agents';
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
const VIEW_STATE_PREFIX = 'principal.subsystems.viewState.';
|
|
@@ -179,8 +180,6 @@ function writeViewState(
|
|
|
179
180
|
|
|
180
181
|
export interface SubsystemComponentGraphProps {
|
|
181
182
|
components: SubsystemComponent[];
|
|
182
|
-
/** Topology relations (structural / module / type). Display edges are derived with walkthrough hops. */
|
|
183
|
-
relations: SubsystemRelation[];
|
|
184
183
|
/**
|
|
185
184
|
* Ordered runtime walkthroughs — one walkthrough per flow. When present the
|
|
186
185
|
* sidebar's bottom half offers a Files/Walkthroughs toggle:
|
|
@@ -195,7 +194,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
195
194
|
* graphify-native relations (raw static-graph edges: `imports`, `contains`,
|
|
196
195
|
* `re_exports`, …). Display-only, drawn with the separate
|
|
197
196
|
* `GRAPHIFY_RELATION_COLOR` palette so they read as derived facts, distinct
|
|
198
|
-
* from authored subsystem mechanisms. Visible in the `
|
|
197
|
+
* from authored subsystem mechanisms. Visible in the `graphify` edge view.
|
|
199
198
|
*/
|
|
200
199
|
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
201
200
|
/**
|
|
@@ -236,16 +235,15 @@ export interface SubsystemComponentGraphProps {
|
|
|
236
235
|
*/
|
|
237
236
|
showSingletonFrames?: boolean;
|
|
238
237
|
/**
|
|
239
|
-
* Which edge
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* - `
|
|
238
|
+
* Which edge source the canvas draws. The graphify and walkthrough sources
|
|
239
|
+
* are disjoint, so a graph carrying both shows one or the other — never
|
|
240
|
+
* both. Edges outside the view are hidden (labels go too).
|
|
241
|
+
* - `graphify`: graphify-native static edges (`imports`, `contains`, …)
|
|
243
242
|
* - `walkthroughs`: walkthrough hop edges (`calls`, `feeds`, …), including
|
|
244
243
|
* step numbers when a flow is focused/hovered
|
|
245
244
|
* Leave unset to let the sidebar's Files / Walkthroughs tab drive it: Files
|
|
246
|
-
* draws
|
|
247
|
-
* tabs, defaults to `walkthroughs
|
|
248
|
-
* no relations), otherwise `relations`.
|
|
245
|
+
* draws graphify edges, Walkthroughs draws runtime hops. Without visible
|
|
246
|
+
* tabs, defaults to `walkthroughs`.
|
|
249
247
|
*/
|
|
250
248
|
edgeView?: SubsystemEdgeView;
|
|
251
249
|
/** Subsystem title displayed in the sidebar. */
|
|
@@ -345,7 +343,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
345
343
|
/**
|
|
346
344
|
* Click an issue. The graph first focuses the target on the canvas itself —
|
|
347
345
|
* a component target is selected and framed (any edge / walkthrough focus
|
|
348
|
-
* that would hide it is cleared),
|
|
346
|
+
* that would hide it is cleared), and a
|
|
349
347
|
* module target frames that boundary frame — then this fires so the host can
|
|
350
348
|
* open its own detail. Collapsing the card again reverses that (deselect +
|
|
351
349
|
* zoom out). Other target kinds (flow / repo) have no node to frame and just
|
|
@@ -519,7 +517,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
519
517
|
measured: { w: number; h: number } | null;
|
|
520
518
|
}
|
|
521
519
|
|
|
522
|
-
function Inner({ components,
|
|
520
|
+
function Inner({ components, 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, liveEvents, agentsPanel }: InnerProps) {
|
|
523
521
|
const { theme } = useTheme();
|
|
524
522
|
const { fitView, fitBounds } = useReactFlow();
|
|
525
523
|
const viewport = useViewport();
|
|
@@ -527,8 +525,8 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
527
525
|
// graph is its own tab/mount, so `persistKey` is stable for a mount.
|
|
528
526
|
const persisted = useMemo(() => readViewState(persistKey), [persistKey]);
|
|
529
527
|
const graphEdges = useMemo(
|
|
530
|
-
() => deriveGraphEdges({
|
|
531
|
-
[
|
|
528
|
+
() => deriveGraphEdges({ walkthroughs }),
|
|
529
|
+
[walkthroughs],
|
|
532
530
|
);
|
|
533
531
|
const [built, setBuilt] = useState<{
|
|
534
532
|
nodes: Node[];
|
|
@@ -577,31 +575,26 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
577
575
|
} | null>(null);
|
|
578
576
|
// Sidebar bottom half: which panel is shown when walkthroughs exist.
|
|
579
577
|
const [sidebarView, setSidebarView] = useState<'files' | 'walkthroughs'>(() =>
|
|
580
|
-
walkthroughs?.length ? 'walkthroughs' : 'files',
|
|
578
|
+
persisted.sidebarView ?? (walkthroughs?.length ? 'walkthroughs' : 'files'),
|
|
581
579
|
);
|
|
582
580
|
// Diagnostics area tab: issues list vs the Maintain agent pipeline.
|
|
583
|
-
const [diagnosticsTab, setDiagnosticsTab] = useState<'issues' | 'agents'>(
|
|
584
|
-
|
|
585
|
-
// belongs to the *other* vocabulary flips the canvas so the edge is actually
|
|
586
|
-
// visible; it reverts when the focus is undone (see `unfocusIssueTarget`) or
|
|
587
|
-
// when the diagnostics list closes. Null = use the host-pinned / tab view.
|
|
588
|
-
const [edgeViewOverride, setEdgeViewOverride] = useState<SubsystemEdgeView | null>(
|
|
589
|
-
null,
|
|
581
|
+
const [diagnosticsTab, setDiagnosticsTab] = useState<'issues' | 'agents'>(
|
|
582
|
+
() => persisted.diagnosticsTab ?? 'issues',
|
|
590
583
|
);
|
|
591
|
-
// One edge
|
|
592
|
-
// Files / Walkthroughs tab picks: Files draws
|
|
584
|
+
// One edge source at a time. When the caller doesn't pick, the sidebar's
|
|
585
|
+
// Files / Walkthroughs tab picks: Files draws graphify static edges,
|
|
593
586
|
// Walkthroughs draws runtime hop edges. Without visible tabs (no walkthroughs,
|
|
594
|
-
// or a sidebar-less embed) fall back to
|
|
595
|
-
// to walkthrough edges rather than empty, everything else to relations.
|
|
587
|
+
// or a sidebar-less embed) fall back to walkthrough edges.
|
|
596
588
|
const sidebarTabsVisible = !hideSidebar && (walkthroughs?.length ?? 0) > 0;
|
|
597
589
|
const resolvedEdgeView: SubsystemEdgeView =
|
|
598
|
-
edgeViewOverride ??
|
|
599
590
|
edgeView ??
|
|
600
591
|
(sidebarTabsVisible
|
|
601
|
-
? (sidebarView === 'walkthroughs' ? 'walkthroughs' : '
|
|
602
|
-
:
|
|
592
|
+
? (sidebarView === 'walkthroughs' ? 'walkthroughs' : 'graphify')
|
|
593
|
+
: (walkthroughs?.length ?? 0) > 0
|
|
603
594
|
? 'walkthroughs'
|
|
604
|
-
:
|
|
595
|
+
: (graphifyRelations?.length ?? 0) > 0
|
|
596
|
+
? 'graphify'
|
|
597
|
+
: 'walkthroughs');
|
|
605
598
|
// Walkthrough flows the user has expanded (via the title row). Closed by
|
|
606
599
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
607
600
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
@@ -681,6 +674,14 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
681
674
|
if (!persistKey) return;
|
|
682
675
|
writeViewState(persistKey, { focusedStepIndex });
|
|
683
676
|
}, [persistKey, focusedStepIndex]);
|
|
677
|
+
useEffect(() => {
|
|
678
|
+
if (!persistKey) return;
|
|
679
|
+
writeViewState(persistKey, { sidebarView });
|
|
680
|
+
}, [persistKey, sidebarView]);
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
if (!persistKey) return;
|
|
683
|
+
writeViewState(persistKey, { diagnosticsTab });
|
|
684
|
+
}, [persistKey, diagnosticsTab]);
|
|
684
685
|
// Ref mirror of `selected` so the SUBSYSTEM_CALLBACKS click handler (a
|
|
685
686
|
// closure over the effect deps) can toggle without a stale value.
|
|
686
687
|
const selectedRef = useRef<SubsystemComponent | null>(null);
|
|
@@ -794,15 +795,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
794
795
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
795
796
|
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
796
797
|
const layoutKey = useMemo(
|
|
797
|
-
() => subsystemGraphLayoutKey({ components,
|
|
798
|
-
[components,
|
|
798
|
+
() => subsystemGraphLayoutKey({ components, walkthroughs, graphifyRelations }),
|
|
799
|
+
[components, walkthroughs, graphifyRelations],
|
|
799
800
|
);
|
|
800
801
|
const componentsRef = useRef(components);
|
|
801
|
-
const relationsRef = useRef(relations);
|
|
802
802
|
const walkthroughsRef = useRef(walkthroughs);
|
|
803
803
|
const graphifyRelationsRef = useRef(graphifyRelations);
|
|
804
804
|
componentsRef.current = components;
|
|
805
|
-
relationsRef.current = relations;
|
|
806
805
|
walkthroughsRef.current = walkthroughs;
|
|
807
806
|
graphifyRelationsRef.current = graphifyRelations;
|
|
808
807
|
|
|
@@ -823,7 +822,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
823
822
|
pass2GenRef.current += 1;
|
|
824
823
|
const doc = {
|
|
825
824
|
components: componentsRef.current,
|
|
826
|
-
relations: relationsRef.current,
|
|
827
825
|
walkthroughs: walkthroughsRef.current,
|
|
828
826
|
};
|
|
829
827
|
void buildSubsystemGraph(doc, {
|
|
@@ -880,7 +878,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
880
878
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id)!.height]));
|
|
881
879
|
const gen = ++pass2GenRef.current;
|
|
882
880
|
void buildSubsystemGraph(
|
|
883
|
-
{ components,
|
|
881
|
+
{ components, walkthroughs },
|
|
884
882
|
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, graphifyRelations, orderByLine },
|
|
885
883
|
)
|
|
886
884
|
.then(({ nodes, edges: e, absoluteRects }) => {
|
|
@@ -894,7 +892,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
894
892
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
895
893
|
setLayoutReady(true);
|
|
896
894
|
});
|
|
897
|
-
}, [built.nodes, components,
|
|
895
|
+
}, [built.nodes, components, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels]);
|
|
898
896
|
|
|
899
897
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
900
898
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
@@ -978,7 +976,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
978
976
|
|
|
979
977
|
// Resolve an issue's component target to the matching node. `id` is the
|
|
980
978
|
// stable alias; `label` may be an alias, name, or symbol. Non-component
|
|
981
|
-
// targets (
|
|
979
|
+
// targets (module / flow / repo) have no single node → null.
|
|
982
980
|
const issueComponent = useCallback(
|
|
983
981
|
(issue: SubsystemIssue): SubsystemComponent | null => {
|
|
984
982
|
const target = issue.target;
|
|
@@ -996,33 +994,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
996
994
|
[components],
|
|
997
995
|
);
|
|
998
996
|
|
|
999
|
-
// Resolve a `relation` issue target to the display edge it flags. The target
|
|
1000
|
-
// names the two endpoints (`from → to`); match an edge joining both, in
|
|
1001
|
-
// either direction, preferring the target's `detail` mechanism when it names
|
|
1002
|
-
// one. Null when no display edge matches.
|
|
1003
|
-
const issueEdge = useCallback(
|
|
1004
|
-
(issue: SubsystemIssue): Edge | null => {
|
|
1005
|
-
const target = issue.target;
|
|
1006
|
-
if (target?.kind !== 'relation') return null;
|
|
1007
|
-
const parts = `${target.id ?? ''} → ${target.label}`
|
|
1008
|
-
.split('→')
|
|
1009
|
-
.map((p) => p.replace(/\(\)$/, '').trim())
|
|
1010
|
-
.filter(Boolean);
|
|
1011
|
-
const joins = (e: Edge): boolean =>
|
|
1012
|
-
parts.includes(e.source) && parts.includes(e.target);
|
|
1013
|
-
const matches = baseEdges.filter(joins);
|
|
1014
|
-
const byMechanism = target.detail
|
|
1015
|
-
? matches.filter(
|
|
1016
|
-
(e) =>
|
|
1017
|
-
(e.data as { mechanism?: string } | undefined)?.mechanism ===
|
|
1018
|
-
target.detail,
|
|
1019
|
-
)
|
|
1020
|
-
: matches;
|
|
1021
|
-
return byMechanism[0] ?? matches[0] ?? null;
|
|
1022
|
-
},
|
|
1023
|
-
[baseEdges],
|
|
1024
|
-
);
|
|
1025
|
-
|
|
1026
997
|
// A `module` issue target names a boundary frame, so focusing the card frames
|
|
1027
998
|
// that region on the canvas. Null when the target names no frame (a singleton
|
|
1028
999
|
// dropped from the layout, or a region key nothing declares).
|
|
@@ -1147,7 +1118,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1147
1118
|
// the canvas keeps while everything else dims. Derived from each finding's
|
|
1148
1119
|
// target:
|
|
1149
1120
|
// component → that node
|
|
1150
|
-
// relation → the edge it flags (+ its two endpoint nodes)
|
|
1151
1121
|
// module → every component in that module
|
|
1152
1122
|
// walkthrough → the flow's nodes + its hop edges
|
|
1153
1123
|
// repo → every component of that repo
|
|
@@ -1167,20 +1137,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1167
1137
|
if (t.kind === 'component') {
|
|
1168
1138
|
const a = resolveAlias(t.id ?? t.label);
|
|
1169
1139
|
if (a) nodeIds.add(a);
|
|
1170
|
-
} else if (t.kind === 'relation') {
|
|
1171
|
-
// The target names the two endpoints (`from → to`); light the edge and
|
|
1172
|
-
// its endpoints. Falls back to endpoints alone if no edge matches.
|
|
1173
|
-
const edge = issueEdge(issue);
|
|
1174
|
-
if (edge) {
|
|
1175
|
-
edgeIds.add(edge.id);
|
|
1176
|
-
nodeIds.add(edge.source);
|
|
1177
|
-
nodeIds.add(edge.target);
|
|
1178
|
-
} else {
|
|
1179
|
-
for (const part of `${t.id ?? ''} → ${t.label}`.split('→')) {
|
|
1180
|
-
const a = resolveAlias(part);
|
|
1181
|
-
if (a) nodeIds.add(a);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
1140
|
} else if (t.kind === 'module') {
|
|
1185
1141
|
const key = t.id ?? t.label;
|
|
1186
1142
|
const prefix = `${key.replace(/\/$/, '')}/`;
|
|
@@ -1219,7 +1175,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1219
1175
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) edgeIds.add(e.id);
|
|
1220
1176
|
}
|
|
1221
1177
|
return { nodeIds, edgeIds };
|
|
1222
|
-
}, [issues, expandedIssueCategories, components, walkthroughs,
|
|
1178
|
+
}, [issues, expandedIssueCategories, components, walkthroughs, resolveAlias]);
|
|
1223
1179
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
1224
1180
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
1225
1181
|
|
|
@@ -1466,15 +1422,15 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1466
1422
|
};
|
|
1467
1423
|
};
|
|
1468
1424
|
// Edges outside the selected view are hidden entirely (labels included).
|
|
1469
|
-
// graphify-native edges belong to the
|
|
1425
|
+
// graphify-native edges belong to the graphify view (they are static
|
|
1470
1426
|
// topology, not runtime hops) regardless of their verb.
|
|
1471
1427
|
const edgeInView = (e: Edge): boolean => {
|
|
1472
1428
|
const d = e.data as
|
|
1473
1429
|
| { mechanism?: string; provenance?: SubsystemEdgeProvenance }
|
|
1474
1430
|
| undefined;
|
|
1475
1431
|
const mechanism = d?.mechanism ?? 'uses';
|
|
1476
|
-
if (resolvedEdgeView === '
|
|
1477
|
-
return d?.provenance === 'graphify'
|
|
1432
|
+
if (resolvedEdgeView === 'graphify') {
|
|
1433
|
+
return d?.provenance === 'graphify';
|
|
1478
1434
|
}
|
|
1479
1435
|
return isWalkthroughMechanism(mechanism);
|
|
1480
1436
|
};
|
|
@@ -1573,7 +1529,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1573
1529
|
setSelectedEdgeId(null);
|
|
1574
1530
|
setFocusedWalkthroughId(null);
|
|
1575
1531
|
setFocusedStepIndex(null);
|
|
1576
|
-
setEdgeViewOverride(null);
|
|
1577
1532
|
issueModuleFocusRef.current = null;
|
|
1578
1533
|
}, []);
|
|
1579
1534
|
|
|
@@ -1599,7 +1554,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1599
1554
|
useEffect(() => {
|
|
1600
1555
|
if (!issuesActive) {
|
|
1601
1556
|
setExpandedIssueCategories([]);
|
|
1602
|
-
setEdgeViewOverride(null);
|
|
1603
1557
|
}
|
|
1604
1558
|
}, [issuesActive]);
|
|
1605
1559
|
const toggleIssues = () => {
|
|
@@ -2068,11 +2022,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2068
2022
|
if (playable.length === 0) return;
|
|
2069
2023
|
// Only run the cycles that can actually be shown in the current edge view.
|
|
2070
2024
|
const inView = playable.filter((tl) =>
|
|
2071
|
-
tl.steps.some((s) =>
|
|
2072
|
-
resolvedEdgeView === 'relations'
|
|
2073
|
-
? isRelationMechanism(s.mechanism)
|
|
2074
|
-
: isWalkthroughMechanism(s.mechanism),
|
|
2075
|
-
),
|
|
2025
|
+
tl.steps.some((s) => isWalkthroughMechanism(s.mechanism)),
|
|
2076
2026
|
);
|
|
2077
2027
|
if (inView.length === 0) return;
|
|
2078
2028
|
const cyc = inView;
|
|
@@ -2241,16 +2191,14 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2241
2191
|
);
|
|
2242
2192
|
|
|
2243
2193
|
// Issue click → select + frame the target on the canvas. Component targets
|
|
2244
|
-
// frame their node;
|
|
2245
|
-
//
|
|
2246
|
-
//
|
|
2247
|
-
//
|
|
2248
|
-
// still fires afterwards for its own detail.
|
|
2194
|
+
// frame their node; other kinds (module / flow / repo) have no single element
|
|
2195
|
+
// and just forward. Selections / focus that would hide the target are cleared
|
|
2196
|
+
// first, then the camera flies to it on the next frame. The host's
|
|
2197
|
+
// `onSelectIssue` still fires afterwards for its own detail.
|
|
2249
2198
|
const focusIssueTarget = useCallback(
|
|
2250
2199
|
(issue: SubsystemIssue) => {
|
|
2251
2200
|
const comp = issueComponent(issue);
|
|
2252
|
-
const
|
|
2253
|
-
const step = comp || edge ? null : issueStep(issue);
|
|
2201
|
+
const step = comp ? null : issueStep(issue);
|
|
2254
2202
|
if (comp) {
|
|
2255
2203
|
setSelected(comp);
|
|
2256
2204
|
setSelectedEdgeId(null);
|
|
@@ -2268,27 +2216,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2268
2216
|
maxZoom: 1.5,
|
|
2269
2217
|
});
|
|
2270
2218
|
});
|
|
2271
|
-
} else if (edge) {
|
|
2272
|
-
setSelected(null);
|
|
2273
|
-
setFocusedWalkthroughId(null);
|
|
2274
|
-
setFocusedStepIndex(null);
|
|
2275
|
-
setHoveredWalkthroughStep(null);
|
|
2276
|
-
setSelectedEdgeId(edge.id);
|
|
2277
|
-
issueModuleFocusRef.current = null;
|
|
2278
|
-
// Flip to the vocabulary this edge belongs to, so a hop flagged in the
|
|
2279
|
-
// relations view (or vice versa) is actually drawn while focused.
|
|
2280
|
-
const d = edge.data as
|
|
2281
|
-
| { mechanism?: string; provenance?: SubsystemEdgeProvenance }
|
|
2282
|
-
| undefined;
|
|
2283
|
-
const mechanism = d?.mechanism ?? 'uses';
|
|
2284
|
-
setEdgeViewOverride(
|
|
2285
|
-
d?.provenance === 'graphify' || isRelationMechanism(mechanism)
|
|
2286
|
-
? 'relations'
|
|
2287
|
-
: 'walkthroughs',
|
|
2288
|
-
);
|
|
2289
|
-
requestAnimationFrame(() => {
|
|
2290
|
-
fitFocusBounds(new Set([edge.id]));
|
|
2291
|
-
});
|
|
2292
2219
|
} else if (step) {
|
|
2293
2220
|
// A step finding focuses the step itself, through the same entry point
|
|
2294
2221
|
// the walkthrough panel uses — so the drawer, the step numbering, the
|
|
@@ -2316,14 +2243,12 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2316
2243
|
},
|
|
2317
2244
|
[
|
|
2318
2245
|
issueComponent,
|
|
2319
|
-
issueEdge,
|
|
2320
2246
|
issueStep,
|
|
2321
2247
|
issueModuleNodeId,
|
|
2322
2248
|
focusWalkthroughStep,
|
|
2323
2249
|
onSelect,
|
|
2324
2250
|
onSelectIssue,
|
|
2325
2251
|
fitView,
|
|
2326
|
-
fitFocusBounds,
|
|
2327
2252
|
fitNodeRects,
|
|
2328
2253
|
],
|
|
2329
2254
|
);
|
|
@@ -2342,14 +2267,9 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2342
2267
|
setSelected(null);
|
|
2343
2268
|
setSelectedEdgeId(null);
|
|
2344
2269
|
} else {
|
|
2345
|
-
const
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
if (edge) {
|
|
2349
|
-
if (selectedEdgeIdRef.current !== edge.id) return;
|
|
2350
|
-
setSelectedEdgeId(null);
|
|
2351
|
-
setEdgeViewOverride(null);
|
|
2352
|
-
} else if (step) {
|
|
2270
|
+
const step = issueStep(issue);
|
|
2271
|
+
const moduleNodeId = step ? null : issueModuleNodeId(issue);
|
|
2272
|
+
if (step) {
|
|
2353
2273
|
// Only unwind a step focus this card still owns. A dim-mode graph
|
|
2354
2274
|
// keeps its step focus in hover state instead (owned by the pointer,
|
|
2355
2275
|
// not the card), and correctly leaves that alone here.
|
|
@@ -2375,7 +2295,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2375
2295
|
fitView({ padding: 0.1, duration: 300, minZoom: 0.05, maxZoom: 2 });
|
|
2376
2296
|
});
|
|
2377
2297
|
},
|
|
2378
|
-
[issueComponent,
|
|
2298
|
+
[issueComponent, issueStep, issueModuleNodeId, fitView],
|
|
2379
2299
|
);
|
|
2380
2300
|
|
|
2381
2301
|
// Construct tokens inside a walkthrough snippet. A step's line is an edge
|
|
@@ -3349,7 +3269,6 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3349
3269
|
|
|
3350
3270
|
const constructsOnly = isConstructsOnlyModel({
|
|
3351
3271
|
components: props.components,
|
|
3352
|
-
relations: props.relations,
|
|
3353
3272
|
walkthroughs: props.walkthroughs,
|
|
3354
3273
|
graphifyRelations: props.graphifyRelations,
|
|
3355
3274
|
});
|