@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.
Files changed (36) hide show
  1. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +1 -1
  2. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  3. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +7 -7
  4. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  5. package/dist/subsystem/ComponentDeclaration.d.ts +1 -1
  6. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  7. package/dist/subsystem/ComponentDeclaration.js +1 -1
  8. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  9. package/dist/subsystem/ConstructsCatalog.d.ts +2 -2
  10. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  11. package/dist/subsystem/ConstructsCatalog.js +62 -62
  12. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  13. package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -2
  14. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  15. package/dist/subsystem/SubsystemComponentGraph.js +27 -27
  16. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  17. package/dist/subsystem/model.d.ts +21 -5
  18. package/dist/subsystem/model.d.ts.map +1 -1
  19. package/dist/subsystem/model.js +72 -38
  20. package/dist/subsystem/model.js.map +1 -1
  21. package/dist/subsystem/nodes.d.ts +3 -3
  22. package/dist/subsystem/nodes.d.ts.map +1 -1
  23. package/dist/subsystem/nodes.js +39 -11
  24. package/dist/subsystem/nodes.js.map +1 -1
  25. package/dist/subsystem/paths.d.ts +1 -1
  26. package/dist/subsystem/paths.js +1 -1
  27. package/package.json +1 -1
  28. package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +183 -0
  29. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +8 -8
  30. package/src/subsystem/ComponentDeclaration.tsx +2 -2
  31. package/src/subsystem/ConstructsCatalog.tsx +71 -75
  32. package/src/subsystem/SubsystemComponentGraph.tsx +28 -28
  33. package/src/subsystem/model.test.ts +103 -51
  34. package/src/subsystem/model.ts +83 -43
  35. package/src/subsystem/nodes.tsx +57 -18
  36. 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 { ChevronDown, ChevronUp, Pause, Play, X } from 'lucide-react';
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?: (componentId: string) => void;
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?: (componentId: string) => void;
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 [hoveredComponentId, setHoveredComponentId] = useState<string | null>(null);
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.id === selected.id);
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 = (id: string) => {
650
- const comp = components.find((c) => c.id === id);
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?.id === comp.id) {
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?.(id);
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 memberIds = ((n.data as { region?: { memberIds?: string[] } } | undefined)?.region?.memberIds) ?? [];
803
+ const memberAliases = ((n.data as { region?: { memberAliases?: string[] } } | undefined)?.region?.memberAliases) ?? [];
804
804
  const vis = flowElementVisibility({
805
- inOpened: memberIds.some((id) => openedNodeIds?.has(id) === true),
806
- inSelected: memberIds.some((id) => brightNodeIds?.has(id) === true),
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 || !memberIds.some((id) => previewNodeIds.has(id))
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?.id !== undefined && comp?.id === selected.id;
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?.id === comp.id) {
947
+ if (selected?.alias === comp.alias) {
948
948
  setSelected(null);
949
949
  return;
950
950
  }
951
951
  setSelected(comp);
952
- if (comp.id) onSelect?.(comp.id);
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 = (componentId: string) => {
1388
- const comp = components.find((c) => c.id === componentId);
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 id, name, or symbol (call labels may carry a trailing `()`).
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.id === clean ||
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.id === selectedRef.current?.id) return;
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.id);
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 (!hoveredComponentId) return null;
1454
- return components.find((c) => c.id === hoveredComponentId)?.file ?? null;
1455
- }, [components, hoveredComponentId]);
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
- {descriptionVisible ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
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
- { id: 'reader', name: 'SessionReader', construct: 'class', file: 'SessionReader.ts', purl: 'pkg:github/principal-ai/agent-monitoring' },
34
- { id: 'transcript', name: 'transcript', construct: 'function', file: 'transcript.ts', purl: 'pkg:github/principal-ai/agent-monitoring' },
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
- id: "proposed-watcher",
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
242
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
243
- { id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/acme/app', process: 'app/renderer' },
244
- { id: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/acme/app' },
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]!.memberIds).toEqual(['a', 'b']);
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
- { id: 'a', name: 'a', construct: 'function', file: 'transcript.ts', purl: 'pkg:github/acme/app', module: 'src/session/transcript.ts' },
256
- { id: 'b', name: 'b', construct: 'type_alias', file: 'transcript.ts', purl: 'pkg:github/acme/app', module: 'src/session/transcript.ts' },
257
- { id: 'c', name: 'c', construct: 'function', file: 'paths.ts', purl: 'pkg:github/acme/app', module: 'src/session/paths.ts' },
258
- { id: 'd', name: 'd', construct: 'function', file: 'other.ts', purl: 'pkg:github/acme/app' },
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]!.memberIds).toEqual(['a', 'b']);
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
- id: 'a',
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
- { id: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/acme/app' },
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
295
- { id: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/acme/app' },
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
309
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
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
- id: 'a',
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
- id: 'b',
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
- id: 'boot',
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
- id: 'create',
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
- id: 'write',
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
- id: 'store',
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
418
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
419
- { id: 'solo', name: 'solo', construct: 'function', file: 's.ts', purl: 'pkg:github/acme/app', process: 'app/lonely' },
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
- id: 'rec',
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
- id: 'parse',
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
- id: 'solo',
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app#a.ts' },
479
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
480
- { id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
481
- { id: 'ext', name: 'Stripe', construct: 'external', file: '', purl: 'pkg:npm/stripe' },
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
496
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
506
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
507
- { id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
508
- { id: 'd', name: 'd', construct: 'function', file: 'd.ts', purl: 'pkg:github/other/lib' },
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
- id: 'a',
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
- id: 'b',
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
- id: 'c',
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
- id: 'd',
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
- id: 'a1',
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
- id: 'a2',
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
- id: 'b1',
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
- id: 'b2',
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
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
619
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
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
+ });