@principal-ai/subsystems-react 0.24.27 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) 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 +11 -5
  18. package/dist/subsystem/model.d.ts.map +1 -1
  19. package/dist/subsystem/model.js +65 -36
  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 +3 -3
  24. package/dist/subsystem/nodes.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +13 -13
  27. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +17 -17
  28. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +2 -2
  29. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +3 -3
  30. package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +6 -6
  31. package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +8 -8
  32. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +11 -11
  33. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +6 -6
  34. package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +10 -10
  35. package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +7 -7
  36. package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +17 -17
  37. package/src/stories/Subsystem/ComponentGraph/NodeAnatomy.stories.tsx +16 -16
  38. package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +12 -12
  39. package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +1 -1
  40. package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +10 -10
  41. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +69 -69
  42. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +27 -27
  43. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +8 -8
  44. package/src/subsystem/ComponentDeclaration.tsx +2 -2
  45. package/src/subsystem/ConstructsCatalog.tsx +71 -75
  46. package/src/subsystem/SubsystemComponentGraph.tsx +28 -28
  47. package/src/subsystem/model.test.ts +97 -51
  48. package/src/subsystem/model.ts +77 -41
  49. package/src/subsystem/nodes.tsx +6 -6
@@ -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>
@@ -32,8 +32,8 @@ import {
32
32
  import type { SubsystemComponent, SubsystemComponentEdge } from './model';
33
33
 
34
34
  const comps: SubsystemComponent[] = [
35
- { id: 'reader', name: 'SessionReader', construct: 'class', file: 'SessionReader.ts', purl: 'pkg:github/principal-ai/agent-monitoring' },
36
- { 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' },
37
37
  ];
38
38
 
39
39
  const relations = [
@@ -63,7 +63,7 @@ describe('subsystem graph model', () => {
63
63
  const withExternal: SubsystemComponent[] = [
64
64
  ...comps,
65
65
  {
66
- id: "proposed-watcher",
66
+ alias: "proposed-watcher",
67
67
  name: "watchDir",
68
68
  construct: "external",
69
69
  // Intentionally omit file/purl — agents often leave these off for externals.
@@ -240,24 +240,24 @@ describe('subsystem graph model', () => {
240
240
  test('getSubsystemRegions groups by process, skipping process-less nodes', () => {
241
241
  const regions = getSubsystemRegions({
242
242
  components: [
243
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
244
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
245
- { id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/acme/app', process: 'app/renderer' },
246
- { 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' },
247
247
  ],
248
248
  });
249
249
  expect(regions.map((r) => r.key)).toEqual(['app/host', 'app/renderer']);
250
250
  expect(regions.every((r) => r.kind === 'process')).toBe(true);
251
- expect(regions[0]!.memberIds).toEqual(['a', 'b']);
251
+ expect(regions[0]!.memberAliases).toEqual(['a', 'b']);
252
252
  });
253
253
 
254
254
  test('getSubsystemModuleRegions groups by module, skipping module-less nodes', () => {
255
255
  const regions = getSubsystemModuleRegions({
256
256
  components: [
257
- { id: 'a', name: 'a', construct: 'function', file: 'transcript.ts', purl: 'pkg:github/acme/app', module: 'src/session/transcript.ts' },
258
- { id: 'b', name: 'b', construct: 'type_alias', file: 'transcript.ts', purl: 'pkg:github/acme/app', module: 'src/session/transcript.ts' },
259
- { id: 'c', name: 'c', construct: 'function', file: 'paths.ts', purl: 'pkg:github/acme/app', module: 'src/session/paths.ts' },
260
- { 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' },
261
261
  ],
262
262
  });
263
263
  expect(regions.map((r) => r.key)).toEqual([
@@ -265,14 +265,14 @@ describe('subsystem graph model', () => {
265
265
  'src/session/paths.ts',
266
266
  ]);
267
267
  expect(regions.every((r) => r.kind === 'module')).toBe(true);
268
- expect(regions[0]!.memberIds).toEqual(['a', 'b']);
268
+ expect(regions[0]!.memberAliases).toEqual(['a', 'b']);
269
269
  });
270
270
 
271
271
  test('convertSubsystemToNodes prefers module parentId over process', () => {
272
272
  const nodes = convertSubsystemToNodes({
273
273
  components: [
274
274
  {
275
- id: 'a',
275
+ alias: 'a',
276
276
  name: 'a',
277
277
  construct: 'function',
278
278
  file: 'a.ts',
@@ -280,7 +280,7 @@ describe('subsystem graph model', () => {
280
280
  process: 'app/host',
281
281
  module: 'src/a.ts',
282
282
  },
283
- { 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' },
284
284
  ],
285
285
  relations: [],
286
286
  });
@@ -293,8 +293,8 @@ describe('subsystem graph model', () => {
293
293
  test('convertSubsystemToNodes stamps parentId for process members only', () => {
294
294
  const nodes = convertSubsystemToNodes({
295
295
  components: [
296
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
297
- { 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' },
298
298
  ],
299
299
  relations: [],
300
300
  });
@@ -307,8 +307,8 @@ describe('subsystem graph model', () => {
307
307
  test('convertSubsystemToGroups emits one parent per process', () => {
308
308
  const groups = convertSubsystemToGroups({
309
309
  components: [
310
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
311
- { 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' },
312
312
  ],
313
313
  });
314
314
  expect(groups).toHaveLength(1);
@@ -320,7 +320,7 @@ describe('subsystem graph model', () => {
320
320
  const groups = convertSubsystemToGroups({
321
321
  components: [
322
322
  {
323
- id: 'a',
323
+ alias: 'a',
324
324
  name: 'a',
325
325
  construct: 'function',
326
326
  file: 'a.ts',
@@ -329,7 +329,7 @@ describe('subsystem graph model', () => {
329
329
  module: 'src/a.ts',
330
330
  },
331
331
  {
332
- id: 'b',
332
+ alias: 'b',
333
333
  name: 'b',
334
334
  construct: 'function',
335
335
  file: 'a.ts',
@@ -352,7 +352,7 @@ describe('subsystem graph model', () => {
352
352
  const { nodes, regions } = await buildSubsystemGraph({
353
353
  components: [
354
354
  {
355
- id: 'boot',
355
+ alias: 'boot',
356
356
  name: 'boot',
357
357
  construct: 'function',
358
358
  file: 'main.ts',
@@ -362,7 +362,7 @@ describe('subsystem graph model', () => {
362
362
  symbol: 'boot',
363
363
  },
364
364
  {
365
- id: 'create',
365
+ alias: 'create',
366
366
  name: 'createHost',
367
367
  construct: 'function',
368
368
  file: 'main.ts',
@@ -372,7 +372,7 @@ describe('subsystem graph model', () => {
372
372
  symbol: 'createHost',
373
373
  },
374
374
  {
375
- id: 'write',
375
+ alias: 'write',
376
376
  name: 'writeSession',
377
377
  construct: 'function',
378
378
  file: 'store.ts',
@@ -382,7 +382,7 @@ describe('subsystem graph model', () => {
382
382
  symbol: 'writeSession',
383
383
  },
384
384
  {
385
- id: 'store',
385
+ alias: 'store',
386
386
  name: 'SessionStore',
387
387
  construct: 'store',
388
388
  file: 'store.ts',
@@ -416,9 +416,9 @@ describe('subsystem graph model', () => {
416
416
  test('buildSubsystemGraph drops singleton process frames (no parentId, no group)', async () => {
417
417
  const { nodes, regions } = await buildSubsystemGraph({
418
418
  components: [
419
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
420
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
421
- { 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' },
422
422
  ],
423
423
  relations: [],
424
424
  walkthroughs: [{
@@ -436,7 +436,7 @@ describe('subsystem graph model', () => {
436
436
  const { nodes, regions } = await buildSubsystemGraph({
437
437
  components: [
438
438
  {
439
- id: 'rec',
439
+ alias: 'rec',
440
440
  name: 'CodexRolloutRecord',
441
441
  construct: 'type_alias',
442
442
  file: 'transcript.ts',
@@ -445,7 +445,7 @@ describe('subsystem graph model', () => {
445
445
  symbol: 'CodexRolloutRecord',
446
446
  },
447
447
  {
448
- id: 'parse',
448
+ alias: 'parse',
449
449
  name: 'parseTranscript',
450
450
  construct: 'function',
451
451
  file: 'transcript.ts',
@@ -454,7 +454,7 @@ describe('subsystem graph model', () => {
454
454
  symbol: 'parseTranscript',
455
455
  },
456
456
  {
457
- id: 'solo',
457
+ alias: 'solo',
458
458
  name: 'lonely',
459
459
  construct: 'function',
460
460
  file: 'solo.ts',
@@ -477,10 +477,10 @@ describe('subsystem graph model', () => {
477
477
  test('getSubsystemPackageRegions groups by purl repo key, skipping externals', () => {
478
478
  const regions = getSubsystemPackageRegions({
479
479
  components: [
480
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app#a.ts' },
481
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
482
- { id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
483
- { 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' },
484
484
  ],
485
485
  });
486
486
  expect(regions.map((r) => r.key).sort()).toEqual([
@@ -494,8 +494,8 @@ describe('subsystem graph model', () => {
494
494
  test('buildBoundaryLayoutGroups skips package frames for single-repo graphs', () => {
495
495
  const groups = buildBoundaryLayoutGroups({
496
496
  components: [
497
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
498
- { 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' },
499
499
  ],
500
500
  });
501
501
  expect(groups.filter((g) => g.region.kind === 'package')).toHaveLength(0);
@@ -504,10 +504,10 @@ describe('subsystem graph model', () => {
504
504
  test('buildBoundaryLayoutGroups frames packages when multi-repo', () => {
505
505
  const groups = buildBoundaryLayoutGroups({
506
506
  components: [
507
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
508
- { id: 'b', name: 'b', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app' },
509
- { id: 'c', name: 'c', construct: 'function', file: 'c.ts', purl: 'pkg:github/other/lib' },
510
- { 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' },
511
511
  ],
512
512
  });
513
513
  const pkgs = groups.filter((g) => g.region.kind === 'package');
@@ -521,7 +521,7 @@ describe('subsystem graph model', () => {
521
521
  const groups = buildBoundaryLayoutGroups({
522
522
  components: [
523
523
  {
524
- id: 'a',
524
+ alias: 'a',
525
525
  name: 'a',
526
526
  construct: 'function',
527
527
  file: 'a.ts',
@@ -529,7 +529,7 @@ describe('subsystem graph model', () => {
529
529
  process: 'app/host',
530
530
  },
531
531
  {
532
- id: 'b',
532
+ alias: 'b',
533
533
  name: 'b',
534
534
  construct: 'function',
535
535
  file: 'b.ts',
@@ -537,7 +537,7 @@ describe('subsystem graph model', () => {
537
537
  process: 'app/host',
538
538
  },
539
539
  {
540
- id: 'c',
540
+ alias: 'c',
541
541
  name: 'c',
542
542
  construct: 'function',
543
543
  file: 'c.ts',
@@ -545,7 +545,7 @@ describe('subsystem graph model', () => {
545
545
  process: 'lib/worker',
546
546
  },
547
547
  {
548
- id: 'd',
548
+ alias: 'd',
549
549
  name: 'd',
550
550
  construct: 'function',
551
551
  file: 'd.ts',
@@ -562,7 +562,7 @@ describe('subsystem graph model', () => {
562
562
  const { nodes } = await buildSubsystemGraph({
563
563
  components: [
564
564
  {
565
- id: 'a1',
565
+ alias: 'a1',
566
566
  name: 'a1',
567
567
  construct: 'function',
568
568
  file: 'a.ts',
@@ -571,7 +571,7 @@ describe('subsystem graph model', () => {
571
571
  module: 'src/a.ts',
572
572
  },
573
573
  {
574
- id: 'a2',
574
+ alias: 'a2',
575
575
  name: 'a2',
576
576
  construct: 'function',
577
577
  file: 'a.ts',
@@ -580,7 +580,7 @@ describe('subsystem graph model', () => {
580
580
  module: 'src/a.ts',
581
581
  },
582
582
  {
583
- id: 'b1',
583
+ alias: 'b1',
584
584
  name: 'b1',
585
585
  construct: 'function',
586
586
  file: 'b.ts',
@@ -589,7 +589,7 @@ describe('subsystem graph model', () => {
589
589
  module: 'src/b.ts',
590
590
  },
591
591
  {
592
- id: 'b2',
592
+ alias: 'b2',
593
593
  name: 'b2',
594
594
  construct: 'function',
595
595
  file: 'b.ts',
@@ -617,8 +617,8 @@ describe('subsystem graph model', () => {
617
617
  const groups = buildBoundaryLayoutGroups(
618
618
  {
619
619
  components: [
620
- { id: 'a', name: 'a', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app' },
621
- { 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' },
622
622
  ],
623
623
  },
624
624
  { packageFrames: 'always' },
@@ -626,6 +626,52 @@ describe('subsystem graph model', () => {
626
626
  expect(groups.filter((g) => g.region.kind === 'package')).toHaveLength(1);
627
627
  });
628
628
 
629
+ test('mixed-process module members live only in the module frame', () => {
630
+ // Same file framed under two processes (composed-model vocabulary drift):
631
+ // the module cannot nest, so its members must not also sit directly in
632
+ // the process frames — ELK throws on double-parented leaves.
633
+ const groups = buildBoundaryLayoutGroups({
634
+ components: [
635
+ { alias: 'a1', name: 'a1', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', module: 'src/a.ts', process: 'app/host' },
636
+ { alias: 'a2', name: 'a2', construct: 'function', file: 'a.ts', purl: 'pkg:github/acme/app', module: 'src/a.ts', process: 'app/renderer' },
637
+ { alias: 'b1', name: 'b1', construct: 'function', file: 'b.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
638
+ ],
639
+ });
640
+ const leafHits = new Map<string, string[]>();
641
+ for (const g of groups) {
642
+ for (const id of g.memberAliases) {
643
+ // Nested group ids legitimately appear in parents; only leaves count.
644
+ if (groups.some((h) => h.id === id)) continue;
645
+ leafHits.set(id, [...(leafHits.get(id) ?? []), g.id]);
646
+ }
647
+ }
648
+ for (const [leaf, owners] of leafHits) {
649
+ expect(`${leaf} in ${owners.length} groups: ${owners.join(',')}`).toBe(`${leaf} in 1 groups: ${owners[0]}`);
650
+ }
651
+ const mod = groups.find((g) => g.region.kind === 'module');
652
+ expect(mod?.memberAliases).toEqual(['a1', 'a2']);
653
+ });
654
+
655
+ test('module-less leaf in a mixed-package process lives only in the process frame', () => {
656
+ const groups = buildBoundaryLayoutGroups({
657
+ components: [
658
+ { alias: 'x', name: 'x', construct: 'function', file: 'x.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
659
+ { alias: 'y', name: 'y', construct: 'function', file: 'y.ts', purl: 'pkg:github/acme/app', process: 'app/host' },
660
+ { alias: 'z', name: 'z', construct: 'function', file: 'z.ts', purl: 'pkg:github/other/lib', process: 'app/host' },
661
+ ],
662
+ });
663
+ const leafHits = new Map<string, string[]>();
664
+ for (const g of groups) {
665
+ for (const id of g.memberAliases) {
666
+ if (groups.some((h) => h.id === id)) continue;
667
+ leafHits.set(id, [...(leafHits.get(id) ?? []), g.id]);
668
+ }
669
+ }
670
+ for (const [leaf, owners] of leafHits) {
671
+ expect(owners).toHaveLength(1);
672
+ }
673
+ });
674
+
629
675
  test('subsystemGraphLayoutKey ignores declarationRef-only changes', () => {
630
676
  const base = doc;
631
677
  const withRef = {