@principal-ai/subsystems-react 0.24.26 → 0.25.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/stories/Subsystem/ComponentGraph/fixtures.d.ts +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +7 -7
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +1 -1
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +2 -2
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +62 -62
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +27 -27
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +21 -5
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +72 -38
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +3 -3
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +39 -11
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/paths.d.ts +1 -1
- package/dist/subsystem/paths.js +1 -1
- package/package.json +1 -1
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +183 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +8 -8
- package/src/subsystem/ComponentDeclaration.tsx +2 -2
- package/src/subsystem/ConstructsCatalog.tsx +71 -75
- package/src/subsystem/SubsystemComponentGraph.tsx +28 -28
- package/src/subsystem/model.test.ts +103 -51
- package/src/subsystem/model.ts +83 -43
- package/src/subsystem/nodes.tsx +57 -18
- package/src/subsystem/paths.ts +1 -1
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
applyNodeChanges,
|
|
36
36
|
} from '@xyflow/react';
|
|
37
37
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
38
|
-
import {
|
|
38
|
+
import { FileText, Pause, Play, X } from 'lucide-react';
|
|
39
39
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
40
40
|
import {
|
|
41
41
|
buildSubsystemGraph,
|
|
@@ -101,7 +101,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
101
101
|
* (unselected ones dimmed); everything else is hidden.
|
|
102
102
|
*/
|
|
103
103
|
walkthroughs?: SubsystemWalkthrough[];
|
|
104
|
-
onSelect?: (
|
|
104
|
+
onSelect?: (componentAlias: string) => void;
|
|
105
105
|
/** Called when an edge is clicked (relationship / mechanism + refs seam). */
|
|
106
106
|
onEdgeSelect?: (edge: SubsystemComponentEdge) => void;
|
|
107
107
|
/** Upper bound for node width in px; nodes grow with content up to this,
|
|
@@ -217,7 +217,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
217
217
|
*/
|
|
218
218
|
onFileSelect?: (file: string) => void;
|
|
219
219
|
/** Verify the selected component against graphify (declaration panel). */
|
|
220
|
-
onVerifyComponent?: (
|
|
220
|
+
onVerifyComponent?: (componentAlias: string) => void;
|
|
221
221
|
/** Live verification status for the selected component. */
|
|
222
222
|
componentVerification?: ComponentVerificationState | null;
|
|
223
223
|
}
|
|
@@ -335,7 +335,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
335
335
|
// Pending deferred fit after opening the drawer; cancelled on re-entry / unmount.
|
|
336
336
|
const pendingFocusFitRef = useRef<number | null>(null);
|
|
337
337
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
338
|
-
const [
|
|
338
|
+
const [hoveredComponentAlias, setHoveredComponentId] = useState<string | null>(null);
|
|
339
339
|
// Walkthrough focus — selected flow (or step) is full strength; other
|
|
340
340
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
341
341
|
const [focusedWalkthroughId, setFocusedWalkthroughId] = useState<string | null>(null);
|
|
@@ -496,7 +496,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
496
496
|
// writes back declarationRef).
|
|
497
497
|
useEffect(() => {
|
|
498
498
|
if (!selected) return;
|
|
499
|
-
const fresh = components.find((c) => c.
|
|
499
|
+
const fresh = components.find((c) => c.alias === selected.alias);
|
|
500
500
|
if (fresh && fresh !== selected) setSelected(fresh);
|
|
501
501
|
}, [components, selected]);
|
|
502
502
|
|
|
@@ -646,12 +646,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
646
646
|
);
|
|
647
647
|
|
|
648
648
|
useEffect(() => {
|
|
649
|
-
SUBSYSTEM_CALLBACKS.onSelect = (
|
|
650
|
-
const comp = components.find((c) => c.
|
|
649
|
+
SUBSYSTEM_CALLBACKS.onSelect = (alias: string) => {
|
|
650
|
+
const comp = components.find((c) => c.alias === alias);
|
|
651
651
|
if (comp) {
|
|
652
652
|
// Clicking the already-selected node unselects it (toggle off).
|
|
653
653
|
// Selection is independent of the file drawer — nodes never open it.
|
|
654
|
-
if (selectedRef.current?.
|
|
654
|
+
if (selectedRef.current?.alias === comp.alias) {
|
|
655
655
|
setSelected(null);
|
|
656
656
|
setSelectedEdgeId(null);
|
|
657
657
|
return;
|
|
@@ -660,7 +660,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
660
660
|
setSelectedEdgeId(null);
|
|
661
661
|
setFocusedWalkthroughId(null);
|
|
662
662
|
setFocusedStepIndex(null);
|
|
663
|
-
onSelect?.(
|
|
663
|
+
onSelect?.(alias);
|
|
664
664
|
}
|
|
665
665
|
};
|
|
666
666
|
SUBSYSTEM_CALLBACKS.onEdgeSelect = selectEdge;
|
|
@@ -800,15 +800,15 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
800
800
|
// Boundary frames follow their members: hidden when no member is
|
|
801
801
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
802
802
|
if (n.type === 'subsystem-group') {
|
|
803
|
-
const
|
|
803
|
+
const memberAliases = ((n.data as { region?: { memberAliases?: string[] } } | undefined)?.region?.memberAliases) ?? [];
|
|
804
804
|
const vis = flowElementVisibility({
|
|
805
|
-
inOpened:
|
|
806
|
-
inSelected:
|
|
805
|
+
inOpened: memberAliases.some((alias) => openedNodeIds?.has(alias) === true),
|
|
806
|
+
inSelected: memberAliases.some((alias) => brightNodeIds?.has(alias) === true),
|
|
807
807
|
anyOpened: openedNodeIds != null,
|
|
808
808
|
anySelected: brightNodeIds != null,
|
|
809
809
|
});
|
|
810
810
|
const dimmed = previewNodeIds
|
|
811
|
-
? vis.hidden || !
|
|
811
|
+
? vis.hidden || !memberAliases.some((alias) => previewNodeIds.has(alias))
|
|
812
812
|
: vis.dimmed;
|
|
813
813
|
const hidden = vis.hidden;
|
|
814
814
|
return {
|
|
@@ -823,7 +823,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
823
823
|
}
|
|
824
824
|
const comp = (n.data as { component?: SubsystemComponent } | undefined)?.component;
|
|
825
825
|
const fileMatch = fileMatchForNode(comp?.file, openFile, focusNodeIds?.has(n.id) === true);
|
|
826
|
-
const isSelected = selected?.
|
|
826
|
+
const isSelected = selected?.alias !== undefined && comp?.alias === selected.alias;
|
|
827
827
|
const vis = flowElementVisibility({
|
|
828
828
|
inOpened: openedNodeIds?.has(n.id) === true,
|
|
829
829
|
inSelected: brightNodeIds?.has(n.id) === true,
|
|
@@ -944,12 +944,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
944
944
|
if (node.type === 'subsystem-component' && comp) {
|
|
945
945
|
// Clicking the already-selected node unselects it (toggle off).
|
|
946
946
|
// Selection is independent of the file drawer — nodes never open it.
|
|
947
|
-
if (selected?.
|
|
947
|
+
if (selected?.alias === comp.alias) {
|
|
948
948
|
setSelected(null);
|
|
949
949
|
return;
|
|
950
950
|
}
|
|
951
951
|
setSelected(comp);
|
|
952
|
-
if (comp.
|
|
952
|
+
if (comp.alias) onSelect?.(comp.alias);
|
|
953
953
|
}
|
|
954
954
|
},
|
|
955
955
|
[onSelect, selected],
|
|
@@ -1384,8 +1384,8 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1384
1384
|
// Filename-badge clicks on nodes open the drawer through the same path as
|
|
1385
1385
|
// the declaration panel's file link (toggle + tree sync, no start line).
|
|
1386
1386
|
useEffect(() => {
|
|
1387
|
-
SUBSYSTEM_CALLBACKS.onOpenFile = (
|
|
1388
|
-
const comp = components.find((c) => c.
|
|
1387
|
+
SUBSYSTEM_CALLBACKS.onOpenFile = (componentAlias: string) => {
|
|
1388
|
+
const comp = components.find((c) => c.alias === componentAlias);
|
|
1389
1389
|
if (comp?.file) onOpenDeclarationFile(comp.file);
|
|
1390
1390
|
};
|
|
1391
1391
|
return () => {
|
|
@@ -1394,23 +1394,23 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1394
1394
|
}, [components, onOpenDeclarationFile]);
|
|
1395
1395
|
|
|
1396
1396
|
// Detail-panel links: related-name clicks select the matching component —
|
|
1397
|
-
// resolved by
|
|
1397
|
+
// resolved by alias, name, or symbol (call labels may carry a trailing `()`).
|
|
1398
1398
|
const resolveRelatedComponent = useCallback(
|
|
1399
1399
|
(ref: string) => {
|
|
1400
1400
|
const clean = ref.replace(/\(\)$/, '');
|
|
1401
1401
|
const comp = components.find(
|
|
1402
1402
|
(c) =>
|
|
1403
|
-
c.
|
|
1403
|
+
c.alias === clean ||
|
|
1404
1404
|
c.name === clean ||
|
|
1405
1405
|
c.symbol === clean ||
|
|
1406
1406
|
c.symbol?.replace(/\(\)$/, '') === clean,
|
|
1407
1407
|
);
|
|
1408
|
-
if (!comp || comp.
|
|
1408
|
+
if (!comp || comp.alias === selectedRef.current?.alias) return;
|
|
1409
1409
|
setSelected(comp);
|
|
1410
1410
|
setSelectedEdgeId(null);
|
|
1411
1411
|
setFocusedWalkthroughId(null);
|
|
1412
1412
|
setFocusedStepIndex(null);
|
|
1413
|
-
onSelect?.(comp.
|
|
1413
|
+
onSelect?.(comp.alias);
|
|
1414
1414
|
},
|
|
1415
1415
|
[components, onSelect],
|
|
1416
1416
|
);
|
|
@@ -1450,9 +1450,9 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1450
1450
|
|
|
1451
1451
|
// The hovered node's file (null when not hovering / file-less component).
|
|
1452
1452
|
const hoveredFile = useMemo(() => {
|
|
1453
|
-
if (!
|
|
1454
|
-
return components.find((c) => c.
|
|
1455
|
-
}, [components,
|
|
1453
|
+
if (!hoveredComponentAlias) return null;
|
|
1454
|
+
return components.find((c) => c.alias === hoveredComponentAlias)?.file ?? null;
|
|
1455
|
+
}, [components, hoveredComponentAlias]);
|
|
1456
1456
|
|
|
1457
1457
|
// Drawer content renderer: prefer the path-keyed viewer; fall back to the
|
|
1458
1458
|
// legacy component-keyed one via a file → first-component lookup.
|
|
@@ -1569,13 +1569,13 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1569
1569
|
padding: 0,
|
|
1570
1570
|
border: 'none',
|
|
1571
1571
|
borderRadius: 4,
|
|
1572
|
-
background: descToggleHover ? theme.colors.border : 'transparent',
|
|
1573
|
-
color: descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1572
|
+
background: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
|
|
1573
|
+
color: descriptionVisible || descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1574
1574
|
cursor: 'pointer',
|
|
1575
1575
|
transition: 'background 120ms ease, color 120ms ease',
|
|
1576
1576
|
}}
|
|
1577
1577
|
>
|
|
1578
|
-
|
|
1578
|
+
<FileText size={14} />
|
|
1579
1579
|
</button>
|
|
1580
1580
|
)}
|
|
1581
1581
|
</div>
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
processGroupNodeId,
|
|
10
10
|
moduleGroupNodeId,
|
|
11
11
|
moduleBadgeLabel,
|
|
12
|
+
moduleBadgeHoverLabel,
|
|
13
|
+
moduleBadgeWidth,
|
|
12
14
|
moduleMinWidthForBadge,
|
|
13
15
|
packageGroupNodeId,
|
|
14
16
|
buildBoundaryLayoutGroups,
|
|
@@ -30,8 +32,8 @@ import {
|
|
|
30
32
|
import type { SubsystemComponent, SubsystemComponentEdge } from './model';
|
|
31
33
|
|
|
32
34
|
const comps: SubsystemComponent[] = [
|
|
33
|
-
{
|
|
34
|
-
{
|
|
35
|
+
{ alias: 'reader', name: 'SessionReader', construct: 'class', file: 'SessionReader.ts', purl: 'pkg:github/principal-ai/agent-monitoring' },
|
|
36
|
+
{ alias: 'transcript', name: 'transcript', construct: 'function', file: 'transcript.ts', purl: 'pkg:github/principal-ai/agent-monitoring' },
|
|
35
37
|
];
|
|
36
38
|
|
|
37
39
|
const relations = [
|
|
@@ -61,7 +63,7 @@ describe('subsystem graph model', () => {
|
|
|
61
63
|
const withExternal: SubsystemComponent[] = [
|
|
62
64
|
...comps,
|
|
63
65
|
{
|
|
64
|
-
|
|
66
|
+
alias: "proposed-watcher",
|
|
65
67
|
name: "watchDir",
|
|
66
68
|
construct: "external",
|
|
67
69
|
// Intentionally omit file/purl — agents often leave these off for externals.
|
|
@@ -238,24 +240,24 @@ describe('subsystem graph model', () => {
|
|
|
238
240
|
test('getSubsystemRegions groups by process, skipping process-less nodes', () => {
|
|
239
241
|
const regions = getSubsystemRegions({
|
|
240
242
|
components: [
|
|
241
|
-
{
|
|
242
|
-
{
|
|
243
|
-
{
|
|
244
|
-
{
|
|
243
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
|
|
244
|
+
{ alias: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
|
|
245
|
+
{ alias: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/acme/app', process: 'app/renderer' },
|
|
246
|
+
{ alias: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/acme/app' },
|
|
245
247
|
],
|
|
246
248
|
});
|
|
247
249
|
expect(regions.map((r) => r.key)).toEqual(['app/host', 'app/renderer']);
|
|
248
250
|
expect(regions.every((r) => r.kind === 'process')).toBe(true);
|
|
249
|
-
expect(regions[0]!.
|
|
251
|
+
expect(regions[0]!.memberAliases).toEqual(['a', 'b']);
|
|
250
252
|
});
|
|
251
253
|
|
|
252
254
|
test('getSubsystemModuleRegions groups by module, skipping module-less nodes', () => {
|
|
253
255
|
const regions = getSubsystemModuleRegions({
|
|
254
256
|
components: [
|
|
255
|
-
{
|
|
256
|
-
{
|
|
257
|
-
{
|
|
258
|
-
{
|
|
257
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'transcript.ts', purl: 'pkg:github/acme/app', module: 'src/session/transcript.ts' },
|
|
258
|
+
{ alias: 'b', name: 'b', construct: 'type_alias', file: 'transcript.ts', purl: 'pkg:github/acme/app', module: 'src/session/transcript.ts' },
|
|
259
|
+
{ alias: 'c', name: 'c', construct: 'function', file: 'paths.ts', purl: 'pkg:github/acme/app', module: 'src/session/paths.ts' },
|
|
260
|
+
{ alias: 'd', name: 'd', construct: 'function', file: 'other.ts', purl: 'pkg:github/acme/app' },
|
|
259
261
|
],
|
|
260
262
|
});
|
|
261
263
|
expect(regions.map((r) => r.key)).toEqual([
|
|
@@ -263,14 +265,14 @@ describe('subsystem graph model', () => {
|
|
|
263
265
|
'src/session/paths.ts',
|
|
264
266
|
]);
|
|
265
267
|
expect(regions.every((r) => r.kind === 'module')).toBe(true);
|
|
266
|
-
expect(regions[0]!.
|
|
268
|
+
expect(regions[0]!.memberAliases).toEqual(['a', 'b']);
|
|
267
269
|
});
|
|
268
270
|
|
|
269
271
|
test('convertSubsystemToNodes prefers module parentId over process', () => {
|
|
270
272
|
const nodes = convertSubsystemToNodes({
|
|
271
273
|
components: [
|
|
272
274
|
{
|
|
273
|
-
|
|
275
|
+
alias: 'a',
|
|
274
276
|
name: 'a',
|
|
275
277
|
construct: 'function',
|
|
276
278
|
file: 'a.ts',
|
|
@@ -278,7 +280,7 @@ describe('subsystem graph model', () => {
|
|
|
278
280
|
process: 'app/host',
|
|
279
281
|
module: 'src/a.ts',
|
|
280
282
|
},
|
|
281
|
-
{
|
|
283
|
+
{ alias: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/acme/app' },
|
|
282
284
|
],
|
|
283
285
|
relations: [],
|
|
284
286
|
});
|
|
@@ -291,8 +293,8 @@ describe('subsystem graph model', () => {
|
|
|
291
293
|
test('convertSubsystemToNodes stamps parentId for process members only', () => {
|
|
292
294
|
const nodes = convertSubsystemToNodes({
|
|
293
295
|
components: [
|
|
294
|
-
{
|
|
295
|
-
{
|
|
296
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
|
|
297
|
+
{ alias: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/acme/app' },
|
|
296
298
|
],
|
|
297
299
|
relations: [],
|
|
298
300
|
});
|
|
@@ -305,8 +307,8 @@ describe('subsystem graph model', () => {
|
|
|
305
307
|
test('convertSubsystemToGroups emits one parent per process', () => {
|
|
306
308
|
const groups = convertSubsystemToGroups({
|
|
307
309
|
components: [
|
|
308
|
-
{
|
|
309
|
-
{
|
|
310
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
|
|
311
|
+
{ alias: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
|
|
310
312
|
],
|
|
311
313
|
});
|
|
312
314
|
expect(groups).toHaveLength(1);
|
|
@@ -318,7 +320,7 @@ describe('subsystem graph model', () => {
|
|
|
318
320
|
const groups = convertSubsystemToGroups({
|
|
319
321
|
components: [
|
|
320
322
|
{
|
|
321
|
-
|
|
323
|
+
alias: 'a',
|
|
322
324
|
name: 'a',
|
|
323
325
|
construct: 'function',
|
|
324
326
|
file: 'a.ts',
|
|
@@ -327,7 +329,7 @@ describe('subsystem graph model', () => {
|
|
|
327
329
|
module: 'src/a.ts',
|
|
328
330
|
},
|
|
329
331
|
{
|
|
330
|
-
|
|
332
|
+
alias: 'b',
|
|
331
333
|
name: 'b',
|
|
332
334
|
construct: 'function',
|
|
333
335
|
file: 'a.ts',
|
|
@@ -350,7 +352,7 @@ describe('subsystem graph model', () => {
|
|
|
350
352
|
const { nodes, regions } = await buildSubsystemGraph({
|
|
351
353
|
components: [
|
|
352
354
|
{
|
|
353
|
-
|
|
355
|
+
alias: 'boot',
|
|
354
356
|
name: 'boot',
|
|
355
357
|
construct: 'function',
|
|
356
358
|
file: 'main.ts',
|
|
@@ -360,7 +362,7 @@ describe('subsystem graph model', () => {
|
|
|
360
362
|
symbol: 'boot',
|
|
361
363
|
},
|
|
362
364
|
{
|
|
363
|
-
|
|
365
|
+
alias: 'create',
|
|
364
366
|
name: 'createHost',
|
|
365
367
|
construct: 'function',
|
|
366
368
|
file: 'main.ts',
|
|
@@ -370,7 +372,7 @@ describe('subsystem graph model', () => {
|
|
|
370
372
|
symbol: 'createHost',
|
|
371
373
|
},
|
|
372
374
|
{
|
|
373
|
-
|
|
375
|
+
alias: 'write',
|
|
374
376
|
name: 'writeSession',
|
|
375
377
|
construct: 'function',
|
|
376
378
|
file: 'store.ts',
|
|
@@ -380,7 +382,7 @@ describe('subsystem graph model', () => {
|
|
|
380
382
|
symbol: 'writeSession',
|
|
381
383
|
},
|
|
382
384
|
{
|
|
383
|
-
|
|
385
|
+
alias: 'store',
|
|
384
386
|
name: 'SessionStore',
|
|
385
387
|
construct: 'store',
|
|
386
388
|
file: 'store.ts',
|
|
@@ -414,9 +416,9 @@ describe('subsystem graph model', () => {
|
|
|
414
416
|
test('buildSubsystemGraph drops singleton process frames (no parentId, no group)', async () => {
|
|
415
417
|
const { nodes, regions } = await buildSubsystemGraph({
|
|
416
418
|
components: [
|
|
417
|
-
{
|
|
418
|
-
{
|
|
419
|
-
{
|
|
419
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
|
|
420
|
+
{ alias: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
|
|
421
|
+
{ alias: 'solo', name: 'solo', construct: 'function', file: 's.ts', purl: 'pkg:github/acme/app', process: 'app/lonely' },
|
|
420
422
|
],
|
|
421
423
|
relations: [],
|
|
422
424
|
walkthroughs: [{
|
|
@@ -434,7 +436,7 @@ describe('subsystem graph model', () => {
|
|
|
434
436
|
const { nodes, regions } = await buildSubsystemGraph({
|
|
435
437
|
components: [
|
|
436
438
|
{
|
|
437
|
-
|
|
439
|
+
alias: 'rec',
|
|
438
440
|
name: 'CodexRolloutRecord',
|
|
439
441
|
construct: 'type_alias',
|
|
440
442
|
file: 'transcript.ts',
|
|
@@ -443,7 +445,7 @@ describe('subsystem graph model', () => {
|
|
|
443
445
|
symbol: 'CodexRolloutRecord',
|
|
444
446
|
},
|
|
445
447
|
{
|
|
446
|
-
|
|
448
|
+
alias: 'parse',
|
|
447
449
|
name: 'parseTranscript',
|
|
448
450
|
construct: 'function',
|
|
449
451
|
file: 'transcript.ts',
|
|
@@ -452,7 +454,7 @@ describe('subsystem graph model', () => {
|
|
|
452
454
|
symbol: 'parseTranscript',
|
|
453
455
|
},
|
|
454
456
|
{
|
|
455
|
-
|
|
457
|
+
alias: 'solo',
|
|
456
458
|
name: 'lonely',
|
|
457
459
|
construct: 'function',
|
|
458
460
|
file: 'solo.ts',
|
|
@@ -475,10 +477,10 @@ describe('subsystem graph model', () => {
|
|
|
475
477
|
test('getSubsystemPackageRegions groups by purl repo key, skipping externals', () => {
|
|
476
478
|
const regions = getSubsystemPackageRegions({
|
|
477
479
|
components: [
|
|
478
|
-
{
|
|
479
|
-
{
|
|
480
|
-
{
|
|
481
|
-
{
|
|
480
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app#a.ts' },
|
|
481
|
+
{ alias: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
482
|
+
{ alias: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
|
|
483
|
+
{ alias: 'ext', name: 'Stripe', construct: 'external', file: '', purl: 'pkg:npm/stripe' },
|
|
482
484
|
],
|
|
483
485
|
});
|
|
484
486
|
expect(regions.map((r) => r.key).sort()).toEqual([
|
|
@@ -492,8 +494,8 @@ describe('subsystem graph model', () => {
|
|
|
492
494
|
test('buildBoundaryLayoutGroups skips package frames for single-repo graphs', () => {
|
|
493
495
|
const groups = buildBoundaryLayoutGroups({
|
|
494
496
|
components: [
|
|
495
|
-
{
|
|
496
|
-
{
|
|
497
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
|
|
498
|
+
{ alias: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
497
499
|
],
|
|
498
500
|
});
|
|
499
501
|
expect(groups.filter((g) => g.region.kind === 'package')).toHaveLength(0);
|
|
@@ -502,10 +504,10 @@ describe('subsystem graph model', () => {
|
|
|
502
504
|
test('buildBoundaryLayoutGroups frames packages when multi-repo', () => {
|
|
503
505
|
const groups = buildBoundaryLayoutGroups({
|
|
504
506
|
components: [
|
|
505
|
-
{
|
|
506
|
-
{
|
|
507
|
-
{
|
|
508
|
-
{
|
|
507
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
|
|
508
|
+
{ alias: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
509
|
+
{ alias: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
|
|
510
|
+
{ alias: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/other/lib' },
|
|
509
511
|
],
|
|
510
512
|
});
|
|
511
513
|
const pkgs = groups.filter((g) => g.region.kind === 'package');
|
|
@@ -519,7 +521,7 @@ describe('subsystem graph model', () => {
|
|
|
519
521
|
const groups = buildBoundaryLayoutGroups({
|
|
520
522
|
components: [
|
|
521
523
|
{
|
|
522
|
-
|
|
524
|
+
alias: 'a',
|
|
523
525
|
name: 'a',
|
|
524
526
|
construct: 'function',
|
|
525
527
|
file: 'a.ts',
|
|
@@ -527,7 +529,7 @@ describe('subsystem graph model', () => {
|
|
|
527
529
|
process: 'app/host',
|
|
528
530
|
},
|
|
529
531
|
{
|
|
530
|
-
|
|
532
|
+
alias: 'b',
|
|
531
533
|
name: 'b',
|
|
532
534
|
construct: 'function',
|
|
533
535
|
file: 'b.ts',
|
|
@@ -535,7 +537,7 @@ describe('subsystem graph model', () => {
|
|
|
535
537
|
process: 'app/host',
|
|
536
538
|
},
|
|
537
539
|
{
|
|
538
|
-
|
|
540
|
+
alias: 'c',
|
|
539
541
|
name: 'c',
|
|
540
542
|
construct: 'function',
|
|
541
543
|
file: 'c.ts',
|
|
@@ -543,7 +545,7 @@ describe('subsystem graph model', () => {
|
|
|
543
545
|
process: 'lib/worker',
|
|
544
546
|
},
|
|
545
547
|
{
|
|
546
|
-
|
|
548
|
+
alias: 'd',
|
|
547
549
|
name: 'd',
|
|
548
550
|
construct: 'function',
|
|
549
551
|
file: 'd.ts',
|
|
@@ -560,7 +562,7 @@ describe('subsystem graph model', () => {
|
|
|
560
562
|
const { nodes } = await buildSubsystemGraph({
|
|
561
563
|
components: [
|
|
562
564
|
{
|
|
563
|
-
|
|
565
|
+
alias: 'a1',
|
|
564
566
|
name: 'a1',
|
|
565
567
|
construct: 'function',
|
|
566
568
|
file: 'a.ts',
|
|
@@ -569,7 +571,7 @@ describe('subsystem graph model', () => {
|
|
|
569
571
|
module: 'src/a.ts',
|
|
570
572
|
},
|
|
571
573
|
{
|
|
572
|
-
|
|
574
|
+
alias: 'a2',
|
|
573
575
|
name: 'a2',
|
|
574
576
|
construct: 'function',
|
|
575
577
|
file: 'a.ts',
|
|
@@ -578,7 +580,7 @@ describe('subsystem graph model', () => {
|
|
|
578
580
|
module: 'src/a.ts',
|
|
579
581
|
},
|
|
580
582
|
{
|
|
581
|
-
|
|
583
|
+
alias: 'b1',
|
|
582
584
|
name: 'b1',
|
|
583
585
|
construct: 'function',
|
|
584
586
|
file: 'b.ts',
|
|
@@ -587,7 +589,7 @@ describe('subsystem graph model', () => {
|
|
|
587
589
|
module: 'src/b.ts',
|
|
588
590
|
},
|
|
589
591
|
{
|
|
590
|
-
|
|
592
|
+
alias: 'b2',
|
|
591
593
|
name: 'b2',
|
|
592
594
|
construct: 'function',
|
|
593
595
|
file: 'b.ts',
|
|
@@ -615,8 +617,8 @@ describe('subsystem graph model', () => {
|
|
|
615
617
|
const groups = buildBoundaryLayoutGroups(
|
|
616
618
|
{
|
|
617
619
|
components: [
|
|
618
|
-
{
|
|
619
|
-
{
|
|
620
|
+
{ alias: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
|
|
621
|
+
{ alias: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
|
|
620
622
|
],
|
|
621
623
|
},
|
|
622
624
|
{ packageFrames: 'always' },
|
|
@@ -678,3 +680,53 @@ describe('isConstructsOnlyModel', () => {
|
|
|
678
680
|
).toBe(false);
|
|
679
681
|
});
|
|
680
682
|
});
|
|
683
|
+
|
|
684
|
+
describe('module badge labels', () => {
|
|
685
|
+
test('moduleBadgeLabel keeps the full path when it fits the module width', () => {
|
|
686
|
+
expect(moduleBadgeLabel('src/main.ts', 1000)).toBe('src/main.ts');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test('moduleBadgeLabel keeps short paths even with a tiny module', () => {
|
|
690
|
+
expect(moduleBadgeLabel('main.ts', 0)).toBe('main.ts');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test('moduleBadgeLabel collapses deep paths to first and last segment', () => {
|
|
694
|
+
expect(moduleBadgeLabel('packages/subsystems-react/src/subsystem/nodes.tsx', 0))
|
|
695
|
+
.toBe('packages/…/nodes.tsx');
|
|
696
|
+
expect(moduleBadgeLabel('src/session/transcript.ts', 0)).toBe('src/…/transcript.ts');
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test('moduleMinWidthForBadge is generous enough for the first + last segments', () => {
|
|
700
|
+
for (const path of ['main.ts', 'packages/subsystems-react/src/subsystem/nodes.tsx']) {
|
|
701
|
+
const min = moduleMinWidthForBadge(path);
|
|
702
|
+
expect(min).toBeGreaterThanOrEqual(moduleMinWidthForBadge('main.ts'));
|
|
703
|
+
// The min width reserves enough for an actual non-collapsed label.
|
|
704
|
+
expect(moduleBadgeLabel('main.ts', min)).toBe('main.ts');
|
|
705
|
+
}
|
|
706
|
+
// A deep path would still collapse at its own min width — that's the point.
|
|
707
|
+
expect(moduleBadgeLabel('packages/subsystems-react/src/subsystem/nodes.tsx', moduleMinWidthForBadge('packages/subsystems-react/src/subsystem/nodes.tsx')))
|
|
708
|
+
.toMatch(/^packages\/…\/nodes\.tsx$/);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test('moduleBadgeHoverLabel reveals more trailing segments as width allows, never the full path', () => {
|
|
712
|
+
const deep = 'packages/subsystems-react/src/subsystem/nodes.tsx';
|
|
713
|
+
const fullWidth = moduleBadgeWidth(deep);
|
|
714
|
+
// Enough room for one more middle segment, but not the full path.
|
|
715
|
+
const oneMore = 'packages/…/subsystem/nodes.tsx';
|
|
716
|
+
const roomy = moduleBadgeWidth(oneMore) + 10;
|
|
717
|
+
const hovered = moduleBadgeHoverLabel(deep, roomy);
|
|
718
|
+
expect(hovered).toContain('…');
|
|
719
|
+
expect(hovered).not.toBe(deep);
|
|
720
|
+
expect(hovered.startsWith('packages/…/')).toBe(true);
|
|
721
|
+
// Reveals more than the bare first + last, without passing the budget.
|
|
722
|
+
expect(moduleBadgeWidth(hovered)).toBeGreaterThan(moduleBadgeWidth('packages/…/nodes.tsx'));
|
|
723
|
+
expect(moduleBadgeWidth(hovered)).toBeLessThanOrEqual(roomy);
|
|
724
|
+
expect(moduleBadgeWidth(hovered)).toBeLessThanOrEqual(fullWidth);
|
|
725
|
+
// With plenty of room it returns the full path.
|
|
726
|
+
expect(moduleBadgeHoverLabel(deep, fullWidth + 8)).toBe(deep);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('moduleBadgeWidth estimates a label strictly wider than its shorter precursor', () => {
|
|
730
|
+
expect(moduleBadgeWidth('src/…/nodes.tsx')).toBeLessThan(moduleBadgeWidth('packages/…/nodes.tsx'));
|
|
731
|
+
});
|
|
732
|
+
});
|