@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
@@ -127,7 +127,13 @@ export type SubsystemEdgeView = 'relations' | 'walkthroughs';
127
127
 
128
128
  /** A component node — the named unit, construct-tagged; `file` is its location. */
129
129
  export interface SubsystemComponent {
130
- id: string;
130
+ /**
131
+ * Model-local stable alias, unique per model. Referenced by relation /
132
+ * walkthrough `from` / `to`; edges point at the alias, not the location.
133
+ * Code identity (for composed multi-model views) lives on
134
+ * `purl` + `file` + `symbol`, not here.
135
+ */
136
+ alias: string;
131
137
  name: string;
132
138
  /**
133
139
  * The node's construct — what it IS as a declaration (class, function,
@@ -268,8 +274,8 @@ export interface SubsystemRelation {
268
274
  */
269
275
  export interface SubsystemComponentEdge {
270
276
  id: string;
271
- from: string; // component id
272
- to: string; // component id or external target label
277
+ from: string; // component alias
278
+ to: string; // component alias or external target label
273
279
  mechanism: SubsystemEdgeMechanism;
274
280
  /** Concrete file/symbol refs backing the edge (the seam). */
275
281
  refs?: string[];
@@ -374,7 +380,7 @@ export function deriveGraphEdges(doc: {
374
380
  * Those snapshots are a catalog of declarations, not a graph.
375
381
  */
376
382
  export function isConstructsOnlyModel(doc: {
377
- components: readonly { id: string }[];
383
+ components: readonly { alias: string }[];
378
384
  relations?: readonly SubsystemRelation[];
379
385
  walkthroughs?: readonly SubsystemWalkthrough[];
380
386
  }): boolean {
@@ -473,7 +479,7 @@ export interface SubsystemProcessRegion {
473
479
  /** Display label for the boundary frame. */
474
480
  label: string;
475
481
  /** Component ids that are members of this region. */
476
- memberIds: string[];
482
+ memberAliases: string[];
477
483
  }
478
484
 
479
485
  /** Options for which boundary frames are kept. */
@@ -598,14 +604,14 @@ export function getSubsystemRegions(
598
604
  const p = c.process?.trim();
599
605
  if (!p) continue;
600
606
  const list = byProcess.get(p) ?? [];
601
- list.push(c.id);
607
+ list.push(c.alias);
602
608
  byProcess.set(p, list);
603
609
  }
604
- return [...byProcess.entries()].map(([key, memberIds]) => ({
610
+ return [...byProcess.entries()].map(([key, memberAliases]) => ({
605
611
  kind: 'process' as const,
606
612
  key,
607
613
  label: key,
608
- memberIds,
614
+ memberAliases,
609
615
  }));
610
616
  }
611
617
 
@@ -621,14 +627,14 @@ export function getSubsystemModuleRegions(
621
627
  const m = c.module?.trim();
622
628
  if (!m) continue;
623
629
  const list = byModule.get(m) ?? [];
624
- list.push(c.id);
630
+ list.push(c.alias);
625
631
  byModule.set(m, list);
626
632
  }
627
- return [...byModule.entries()].map(([key, memberIds]) => ({
633
+ return [...byModule.entries()].map(([key, memberAliases]) => ({
628
634
  kind: 'module' as const,
629
635
  key,
630
636
  label: key,
631
- memberIds,
637
+ memberAliases,
632
638
  }));
633
639
  }
634
640
 
@@ -645,14 +651,14 @@ export function getSubsystemPackageRegions(
645
651
  const key = componentPackageKey(c);
646
652
  if (!key) continue;
647
653
  const list = byPackage.get(key) ?? [];
648
- list.push(c.id);
654
+ list.push(c.alias);
649
655
  byPackage.set(key, list);
650
656
  }
651
- return [...byPackage.entries()].map(([key, memberIds]) => ({
657
+ return [...byPackage.entries()].map(([key, memberAliases]) => ({
652
658
  kind: 'package' as const,
653
659
  key,
654
660
  label: packageRegionLabel(key),
655
- memberIds,
661
+ memberAliases,
656
662
  }));
657
663
  }
658
664
 
@@ -662,7 +668,7 @@ export function getSubsystemPackageRegions(
662
668
  */
663
669
  export interface BoundaryLayoutGroup {
664
670
  id: string;
665
- memberIds: string[];
671
+ memberAliases: string[];
666
672
  /** When set, this group is a child of another boundary group. */
667
673
  parentId?: string;
668
674
  region: SubsystemProcessRegion;
@@ -672,7 +678,7 @@ function keepRegion(
672
678
  r: SubsystemProcessRegion,
673
679
  showSingletons: boolean,
674
680
  ): boolean {
675
- return showSingletons || r.memberIds.length >= 2;
681
+ return showSingletons || r.memberAliases.length >= 2;
676
682
  }
677
683
 
678
684
  /**
@@ -689,7 +695,7 @@ export function buildBoundaryLayoutGroups(
689
695
  ): BoundaryLayoutGroup[] {
690
696
  const showSingletons = opts.showSingletonFrames === true;
691
697
  const packageMode = opts.packageFrames ?? 'multi-repo';
692
- const byId = new Map(doc.components.map((c) => [c.id, c]));
698
+ const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
693
699
 
694
700
  const allPackages = getSubsystemPackageRegions(doc);
695
701
  const packageEligible =
@@ -711,11 +717,35 @@ export function buildBoundaryLayoutGroups(
711
717
  );
712
718
  const keptProcessKeys = new Set(processes.map((r) => r.key));
713
719
 
720
+ // Leaves owned by a multi-member module belong to that module's frame
721
+ // exclusively. Multi-member modules are always built by ELK (never skipped
722
+ // as singletons), so letting a process/package group also list them
723
+ // directly would parent the same leaf twice and ELK throws
724
+ // ("value already present"). Mixed-process modules (common in composed
725
+ // graphs, where models frame one file under different processes) never
726
+ // nest — without this claim they land in both frames.
727
+ const claimedByModule = new Set<string>();
728
+ for (const r of modules) {
729
+ if (r.memberAliases.length >= 2) {
730
+ for (const alias of r.memberAliases) claimedByModule.add(alias);
731
+ }
732
+ }
733
+ // Same rule one level up: a leaf owned by a multi-member process frame
734
+ // must not also sit directly in a package frame. (Singleton processes
735
+ // never claim — ELK skips them and promotes the member upward, so the
736
+ // member has to stay reachable through its package or ungrouped.)
737
+ const claimedByProcess = new Set<string>();
738
+ for (const r of processes) {
739
+ if (r.memberAliases.length >= 2) {
740
+ for (const alias of r.memberAliases) claimedByProcess.add(alias);
741
+ }
742
+ }
743
+
714
744
  const moduleGroups: BoundaryLayoutGroup[] = modules.map((r) => {
715
745
  const processesOfMembers = new Set<string>();
716
746
  const packagesOfMembers = new Set<string>();
717
- for (const id of r.memberIds) {
718
- const c = byId.get(id);
747
+ for (const alias of r.memberAliases) {
748
+ const c = byAlias.get(alias);
719
749
  const p = c?.process?.trim();
720
750
  if (p) processesOfMembers.add(p);
721
751
  const pkg = c ? componentPackageKey(c) : undefined;
@@ -732,7 +762,7 @@ export function buildBoundaryLayoutGroups(
732
762
  }
733
763
  return {
734
764
  id: moduleGroupNodeId(r.key),
735
- memberIds: [...r.memberIds],
765
+ memberAliases: [...r.memberAliases],
736
766
  parentId,
737
767
  region: r,
738
768
  };
@@ -742,15 +772,16 @@ export function buildBoundaryLayoutGroups(
742
772
  const processId = processGroupNodeId(r.key);
743
773
  const nestedModules = moduleGroups.filter((m) => m.parentId === processId);
744
774
  const nestedModuleKeys = new Set(nestedModules.map((m) => m.region.key));
745
- const directLeaves = r.memberIds.filter((id) => {
746
- const mod = byId.get(id)?.module?.trim();
775
+ const directLeaves = r.memberAliases.filter((alias) => {
776
+ if (claimedByModule.has(alias)) return false;
777
+ const mod = byAlias.get(alias)?.module?.trim();
747
778
  if (!mod) return true;
748
779
  return !nestedModuleKeys.has(mod);
749
780
  });
750
781
 
751
782
  const packagesOfMembers = new Set<string>();
752
- for (const id of r.memberIds) {
753
- const c = byId.get(id);
783
+ for (const alias of r.memberAliases) {
784
+ const c = byAlias.get(alias);
754
785
  const pkg = c ? componentPackageKey(c) : undefined;
755
786
  if (pkg) packagesOfMembers.add(pkg);
756
787
  }
@@ -762,7 +793,7 @@ export function buildBoundaryLayoutGroups(
762
793
 
763
794
  return {
764
795
  id: processId,
765
- memberIds: [...nestedModules.map((m) => m.id), ...directLeaves],
796
+ memberAliases: [...nestedModules.map((m) => m.id), ...directLeaves],
766
797
  parentId,
767
798
  region: r,
768
799
  };
@@ -774,15 +805,20 @@ export function buildBoundaryLayoutGroups(
774
805
  const nestedModules = moduleGroups.filter((m) => m.parentId === packageId);
775
806
  const claimed = new Set<string>();
776
807
  for (const p of nestedProcesses) {
777
- for (const id of p.region.memberIds) claimed.add(id);
808
+ for (const alias of p.region.memberAliases) claimed.add(alias);
778
809
  }
779
810
  for (const m of nestedModules) {
780
- for (const id of m.region.memberIds) claimed.add(id);
811
+ for (const alias of m.region.memberAliases) claimed.add(alias);
781
812
  }
782
- const directLeaves = r.memberIds.filter((id) => !claimed.has(id));
813
+ const directLeaves = r.memberAliases.filter(
814
+ (alias) =>
815
+ !claimed.has(alias) &&
816
+ !claimedByModule.has(alias) &&
817
+ !claimedByProcess.has(alias),
818
+ );
783
819
  return {
784
820
  id: packageId,
785
- memberIds: [
821
+ memberAliases: [
786
822
  ...nestedProcesses.map((p) => p.id),
787
823
  ...nestedModules.map((m) => m.id),
788
824
  ...directLeaves,
@@ -1209,7 +1245,7 @@ export function convertSubsystemToNodes(
1209
1245
  ? processGroupNodeId(processKey)
1210
1246
  : undefined;
1211
1247
  nodes.push({
1212
- id: c.id,
1248
+ id: c.alias,
1213
1249
  type: 'subsystem-component',
1214
1250
  ...(parentId ? { parentId } : {}),
1215
1251
  position: { x: PAD + col * COL_W, y: cursorY + row * ROW_H },
@@ -1245,18 +1281,18 @@ export function convertSubsystemToGroups(
1245
1281
 
1246
1282
  /**
1247
1283
  * Convert a subsystem graph document into React Flow edges. Edges whose target
1248
- * is an external label (not a component id) point at a synthetic stub so the
1284
+ * is an external label (not a component alias) point at a synthetic stub so the
1249
1285
  * relationship is visible without a member node.
1250
1286
  */
1251
1287
  export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemGraphEdge[] {
1252
- const compIds = new Set(doc.components.map((c) => c.id));
1288
+ const compAliases = new Set(doc.components.map((c) => c.alias));
1253
1289
  const edges: SubsystemGraphEdge[] = [];
1254
1290
 
1255
1291
  for (const e of deriveGraphEdges(doc)) {
1256
1292
  const color = MECHANISM_COLOR[e.mechanism];
1257
1293
  const style = MECHANISM_STYLE[e.mechanism];
1258
1294
  // If `to` is a real component, connect directly; otherwise point at a stub node.
1259
- const isExternal = !compIds.has(e.to);
1295
+ const isExternal = !compAliases.has(e.to);
1260
1296
  const targetId = isExternal ? `external:${e.to}` : e.to;
1261
1297
 
1262
1298
  edges.push({
@@ -1282,8 +1318,8 @@ export function subsystemGraphLayoutKey(
1282
1318
  doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'>,
1283
1319
  ): string {
1284
1320
  const components = doc.components
1285
- .map(({ id, purl, name, symbol, construct, file, purpose, process, module }) =>
1286
- [id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? '', module ?? ''].join('\0'))
1321
+ .map(({ alias, purl, name, symbol, construct, file, purpose, process, module }) =>
1322
+ [alias, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? '', module ?? ''].join('\0'))
1287
1323
  .sort()
1288
1324
  .join('\n');
1289
1325
  const edgeKey = deriveGraphEdges(doc)
@@ -1326,7 +1362,7 @@ export async function buildSubsystemGraph(
1326
1362
  const layoutGroups = buildBoundaryLayoutGroups(doc, frameOpts);
1327
1363
  const regions = layoutGroups.map((g) => g.region);
1328
1364
  const regionIds = new Set(layoutGroups.map((g) => g.id));
1329
- const byId = new Map(doc.components.map((c) => [c.id, c]));
1365
+ const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
1330
1366
 
1331
1367
  // Resolve leaf parentIds against kept frames: module → process → package.
1332
1368
  // Stamp package parents here (not in convertSubsystemToNodes) so single-repo
@@ -1335,7 +1371,7 @@ export async function buildSubsystemGraph(
1335
1371
  if (n.type !== 'subsystem-component') continue;
1336
1372
  const parentId = (n as { parentId?: string }).parentId;
1337
1373
  if (parentId && regionIds.has(parentId)) continue;
1338
- const comp = (n.data as SubsystemGraphNodeData).component ?? byId.get(n.id);
1374
+ const comp = (n.data as SubsystemGraphNodeData).component ?? byAlias.get(n.id);
1339
1375
  const processKey = comp?.process?.trim();
1340
1376
  const processId = processKey ? processGroupNodeId(processKey) : undefined;
1341
1377
  if (processId && regionIds.has(processId)) {
@@ -1353,10 +1389,10 @@ export async function buildSubsystemGraph(
1353
1389
 
1354
1390
  // External edge targets that aren't real components → create stub nodes so
1355
1391
  // cross-package edges have something to land on.
1356
- const realIds = new Set(doc.components.map((c) => c.id));
1392
+ const realAliases = new Set(doc.components.map((c) => c.alias));
1357
1393
  const externalIds: string[] = [];
1358
1394
  for (const e of deriveGraphEdges(doc)) {
1359
- if (!realIds.has(e.to)) {
1395
+ if (!realAliases.has(e.to)) {
1360
1396
  const extId = `external:${e.to}`;
1361
1397
  if (!externalIds.includes(extId)) externalIds.push(extId);
1362
1398
  }
@@ -1372,7 +1408,7 @@ export async function buildSubsystemGraph(
1372
1408
  height: 60,
1373
1409
  data: {
1374
1410
  component: {
1375
- id: extId,
1411
+ alias: extId,
1376
1412
  name: label,
1377
1413
  construct: 'external',
1378
1414
  purl: 'external',
@@ -1416,7 +1452,7 @@ export async function buildSubsystemGraph(
1416
1452
  edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
1417
1453
  groups: layoutGroups.map((g) => ({
1418
1454
  id: g.id,
1419
- memberIds: g.memberIds,
1455
+ memberIds: g.memberAliases,
1420
1456
  parentId: g.parentId,
1421
1457
  minWidth: g.region.kind === 'module' ? moduleMinWidthForBadge(g.region.label) : undefined,
1422
1458
  })),
@@ -69,14 +69,14 @@ function breakWords(s: string): string {
69
69
 
70
70
  export interface SubsystemGraphCallbacks {
71
71
  /** Click a component — open its file/entry point. */
72
- onSelect?: (componentId: string) => void;
72
+ onSelect?: (componentAlias: string) => void;
73
73
  /** Click the filename badge — open that component's file in the drawer
74
74
  * (same path as clicking the file link in the declaration panel). */
75
- onOpenFile?: (componentId: string) => void;
75
+ onOpenFile?: (componentAlias: string) => void;
76
76
  /** Click an edge (or its label) — select the relationship. */
77
77
  onEdgeSelect?: (edgeId: string) => void;
78
78
  /** Hover a component (null on leave) — associates it with the file tree. */
79
- onHover?: (componentId: string | null) => void;
79
+ onHover?: (componentAlias: string | null) => void;
80
80
  /** Upper bound for node width; nodes grow with content up to this, then wrap. */
81
81
  maxNodeWidth?: number;
82
82
  }
@@ -131,7 +131,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
131
131
  <div
132
132
  onMouseEnter={() => {
133
133
  setHover(true);
134
- SUBSYSTEM_CALLBACKS.onHover?.(c.id);
134
+ SUBSYSTEM_CALLBACKS.onHover?.(c.alias);
135
135
  }}
136
136
  onMouseLeave={() => {
137
137
  setHover(false);
@@ -139,7 +139,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
139
139
  }}
140
140
  onClick={(e) => {
141
141
  e.stopPropagation();
142
- SUBSYSTEM_CALLBACKS.onSelect?.(c.id);
142
+ SUBSYSTEM_CALLBACKS.onSelect?.(c.alias);
143
143
  }}
144
144
  style={{
145
145
  position: 'relative',
@@ -275,7 +275,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
275
275
  onClick={(e) => {
276
276
  // Open the file directly; don't also toggle node selection.
277
277
  e.stopPropagation();
278
- SUBSYSTEM_CALLBACKS.onOpenFile?.(c.id);
278
+ SUBSYSTEM_CALLBACKS.onOpenFile?.(c.alias);
279
279
  }}
280
280
  >
281
281
  {c.file.split('/').pop()}