@principal-ai/subsystems-react 0.51.3 → 0.52.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 (70) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/C4Graph.d.ts +3 -7
  6. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  7. package/dist/subsystem/C4Graph.js +202 -63
  8. package/dist/subsystem/C4Graph.js.map +1 -1
  9. package/dist/subsystem/C4NodeCard.d.ts +26 -26
  10. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  11. package/dist/subsystem/C4NodeCard.js +56 -30
  12. package/dist/subsystem/C4NodeCard.js.map +1 -1
  13. package/dist/subsystem/IssueList.d.ts +1 -1
  14. package/dist/subsystem/IssueList.d.ts.map +1 -1
  15. package/dist/subsystem/IssueList.js +20 -1
  16. package/dist/subsystem/IssueList.js.map +1 -1
  17. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  18. package/dist/subsystem/SubsystemComponentGraph.js +74 -26
  19. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  20. package/dist/subsystem/c4.d.ts +48 -49
  21. package/dist/subsystem/c4.d.ts.map +1 -1
  22. package/dist/subsystem/c4.js +50 -0
  23. package/dist/subsystem/c4.js.map +1 -1
  24. package/dist/subsystem/c4Evidence.d.ts +88 -84
  25. package/dist/subsystem/c4Evidence.d.ts.map +1 -1
  26. package/dist/subsystem/c4Evidence.js +113 -166
  27. package/dist/subsystem/c4Evidence.js.map +1 -1
  28. package/dist/subsystem/model.d.ts +6 -0
  29. package/dist/subsystem/model.d.ts.map +1 -1
  30. package/dist/subsystem/model.js.map +1 -1
  31. package/dist/subsystem/nodes.d.ts.map +1 -1
  32. package/dist/subsystem/nodes.js +9 -7
  33. package/dist/subsystem/nodes.js.map +1 -1
  34. package/dist/subsystem/techIcons/index.d.ts +5 -0
  35. package/dist/subsystem/techIcons/index.d.ts.map +1 -1
  36. package/dist/subsystem/techIcons/index.js +3 -0
  37. package/dist/subsystem/techIcons/index.js.map +1 -1
  38. package/dist/utils/edgeLabel.d.ts +14 -0
  39. package/dist/utils/edgeLabel.d.ts.map +1 -1
  40. package/dist/utils/edgeLabel.js +24 -0
  41. package/dist/utils/edgeLabel.js.map +1 -1
  42. package/dist/utils/elkLayout.d.ts +65 -0
  43. package/dist/utils/elkLayout.d.ts.map +1 -1
  44. package/dist/utils/elkLayout.js +145 -11
  45. package/dist/utils/elkLayout.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/index.ts +8 -6
  48. package/src/stories/Subsystem/C4Graph/C4Component.stories.tsx +198 -0
  49. package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +42 -6
  50. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +9 -7
  51. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +686 -2
  52. package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +21 -1
  53. package/src/subsystem/C4Graph.tsx +296 -99
  54. package/src/subsystem/C4NodeCard.test.ts +32 -22
  55. package/src/subsystem/C4NodeCard.tsx +67 -39
  56. package/src/subsystem/IssueList.tsx +23 -0
  57. package/src/subsystem/SubsystemComponentGraph.tsx +79 -23
  58. package/src/subsystem/c4.protocolColor.test.ts +26 -0
  59. package/src/subsystem/c4.ts +76 -52
  60. package/src/subsystem/c4Evidence.test.ts +177 -237
  61. package/src/subsystem/c4Evidence.ts +160 -235
  62. package/src/subsystem/model.ts +6 -0
  63. package/src/subsystem/nodes.group.test.tsx +15 -0
  64. package/src/subsystem/nodes.tsx +12 -9
  65. package/src/subsystem/techIcons/index.tsx +8 -0
  66. package/src/utils/edgeLabel.test.ts +21 -2
  67. package/src/utils/edgeLabel.ts +28 -0
  68. package/src/utils/elkLayout.test.ts +50 -0
  69. package/src/utils/elkLayout.ts +191 -11
  70. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +0 -602
@@ -20,7 +20,7 @@ import { ReactFlow, ReactFlowProvider, useReactFlow, useViewport, applyNodeChang
20
20
  import { useTheme } from '@principal-ade/industry-theme';
21
21
  import { FileText, X } from 'lucide-react';
22
22
  import { IndustryMarkdownSlide } from 'themed-markdown';
23
- import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId, isTrailMechanism, edgeColor, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, trailStepGraphEdgeId, } from './model';
23
+ import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId, processGroupNodeId, isTrailMechanism, edgeColor, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, trailStepGraphEdgeId, } from './model';
24
24
  import { ConstructsCatalog } from './ConstructsCatalog';
25
25
  import { SubsystemAgentsPanel, } from './AgentsPanel';
26
26
  import { MaintainLivePanel, } from '../components/maintain-events/MaintainLivePanel';
@@ -654,17 +654,25 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
654
654
  c.name === target.label ||
655
655
  c.symbol === target.label) ?? null);
656
656
  }, [components]);
657
- // A `module` issue target names a boundary frame, so focusing the card frames
658
- // that region on the canvas. Null when the target names no frame (a singleton
659
- // dropped from the layout, or a region key nothing declares).
660
- const issueModuleNodeId = useCallback((issue) => {
657
+ // A `module` / `process` issue target names a boundary frame, so focusing
658
+ // the card frames that region on the canvas. Null when the target names no
659
+ // frame (a singleton dropped from the layout, or a region key nothing
660
+ // declares).
661
+ const issueFrameNodeId = useCallback((issue) => {
661
662
  const target = issue.target;
662
- if (target?.kind !== 'module')
663
- return null;
664
- const key = target.id ?? target.label;
665
- if (!components.some((c) => c.module?.trim() === key))
666
- return null;
667
- return moduleGroupNodeId(key);
663
+ if (target?.kind === 'module') {
664
+ const key = target.id ?? target.label;
665
+ if (!components.some((c) => c.module?.trim() === key))
666
+ return null;
667
+ return moduleGroupNodeId(key);
668
+ }
669
+ if (target?.kind === 'process') {
670
+ const key = target.id ?? target.label;
671
+ if (!components.some((c) => c.process === key))
672
+ return null;
673
+ return processGroupNodeId(key);
674
+ }
675
+ return null;
668
676
  }, [components]);
669
677
  // A `step` issue target names ONE step of a trail — the trail id in
670
678
  // `id`, the 0-based step position in `stepIndex`. Validated against the loaded
@@ -709,7 +717,9 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
709
717
  return map;
710
718
  }, [issues, issueComponent]);
711
719
  // Per-frame diagnostics badge: the same fold, but for findings about a
712
- // BOUNDARY rather than a construct. Keyed by the region's React Flow node id.
720
+ // BOUNDARY rather than a construct. Keyed by the region's React Flow node
721
+ // id — a `module` target names a module frame, a `process` target a process
722
+ // frame (dynamic-topology findings, e.g. C4 process verification).
713
723
  // Only kinds with a dedicated frame icon qualify (`ISSUE_KIND_ICON`) — a
714
724
  // finding with no icon of its own has nothing to badge the frame with, and
715
725
  // borrowing its layer's icon would imply the frame is at fault for it.
@@ -719,9 +729,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
719
729
  return map;
720
730
  for (const issue of issues) {
721
731
  const target = issue.target;
722
- if (target?.kind !== 'module' || !ISSUE_KIND_ICON[issue.kind])
732
+ if (!target || !ISSUE_KIND_ICON[issue.kind])
733
+ continue;
734
+ if (target.kind !== 'module' && target.kind !== 'process')
723
735
  continue;
724
- const id = moduleGroupNodeId(target.id ?? target.label);
736
+ const id = target.kind === 'module'
737
+ ? moduleGroupNodeId(target.id ?? target.label)
738
+ : processGroupNodeId(target.id ?? target.label);
725
739
  const prev = map.get(id);
726
740
  map.set(id, {
727
741
  severity: prev?.severity === 'error' || issue.severity === 'error' ? 'error' : 'info',
@@ -762,6 +776,22 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
762
776
  }
763
777
  return map;
764
778
  }, [issues, trails, issueStep]);
779
+ // Edges backed by a step that ITSELF declares `proposed` — a planned seam
780
+ // between otherwise-real components. Distinct from a proposed ENDPOINT
781
+ // (the node is the placeholder there, and draws its own dashed border):
782
+ // here the seam is the claim that is not real yet, so the edge label draws
783
+ // dashed. Proposed steps are never verified, so this never competes with
784
+ // the step-issue badge.
785
+ const proposedEdgeIds = useMemo(() => {
786
+ const set = new Set();
787
+ for (const trail of trails ?? []) {
788
+ for (const step of trail.steps) {
789
+ if (step.proposed)
790
+ set.add(trailStepGraphEdgeId(step));
791
+ }
792
+ }
793
+ return set;
794
+ }, [trails]);
765
795
  // Expanded verification layers in the diagnostics list. When any layer with
766
796
  // findings is expanded, the canvas dims everything that layer does not
767
797
  // implicate (see `issueFocus`). The list publishes this through
@@ -787,6 +817,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
787
817
  // target:
788
818
  // component → that node
789
819
  // module → every component in that module
820
+ // process → every component with that `process` key
790
821
  // trail → the flow's nodes + its step edges
791
822
  // repo → every component of that repo
792
823
  // graph → whole-graph finding; implicates nothing specific
@@ -822,6 +853,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
822
853
  }
823
854
  }
824
855
  }
856
+ else if (t.kind === 'process') {
857
+ const key = t.id ?? t.label;
858
+ for (const c of components) {
859
+ if (c.process === key)
860
+ nodeIds.add(c.alias);
861
+ }
862
+ }
825
863
  else if (t.kind === 'trail') {
826
864
  const wt = (trails ?? []).find((w) => w.id === t.id || w.title === t.label);
827
865
  if (wt) {
@@ -1017,7 +1055,8 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1017
1055
  // Host override wins over the library's derived frame color.
1018
1056
  const color = region?.key != null ? boundaryColors?.[region.key] : undefined;
1019
1057
  // Boundary findings badge the FRAME, not a member — a region's shape is
1020
- // the fault, so no leaf construct carries it.
1058
+ // the fault, so no leaf construct carries it. Process verification
1059
+ // findings ride the same path: their target kind is `process`.
1021
1060
  const regionIssue = regionIssueByNodeId.get(n.id);
1022
1061
  return {
1023
1062
  ...n,
@@ -1070,6 +1109,11 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1070
1109
  }, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds, previewNodeIds, issueFocusNodeIds, issueBadgeByAlias, regionIssueByNodeId, boundaryColors]);
1071
1110
  const baseNodesKey = useMemo(() => nodes.map((n) => n.id).sort().join(','), [nodes]);
1072
1111
  const baseEdgesKey = useMemo(() => convertedEdges.map((e) => e.id).sort().join(','), [convertedEdges]);
1112
+ // Stamp `proposedStep` onto edge data so the SVG edge itself (not just its
1113
+ // label) draws dashed when a backing trail step declares itself proposed.
1114
+ const proposedStampedEdges = useMemo(() => baseEdges.map((e) => proposedEdgeIds.has(e.id)
1115
+ ? { ...e, data: { ...e.data, proposedStep: true } }
1116
+ : e), [baseEdges, proposedEdgeIds]);
1073
1117
  const dispEdges = useMemo(() => {
1074
1118
  const paint = (e, dimmed) => {
1075
1119
  const markerEnd = e.markerEnd;
@@ -1094,7 +1138,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1094
1138
  return isTrailMechanism(mechanism);
1095
1139
  };
1096
1140
  if (openedEdgeIds || focusEdgeIds || previewEdgeIds || issueFocusEdgeIds) {
1097
- return baseEdges.map((e) => {
1141
+ return proposedStampedEdges.map((e) => {
1098
1142
  const vis = flowElementVisibility({
1099
1143
  inOpened: openedEdgeIds?.has(e.id) === true,
1100
1144
  inSelected: focusEdgeIds?.has(e.id) === true,
@@ -1108,13 +1152,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1108
1152
  });
1109
1153
  }
1110
1154
  if (selectedEdgeId) {
1111
- return baseEdges.map((e) => ({
1155
+ return proposedStampedEdges.map((e) => ({
1112
1156
  ...paint(e, e.id !== selectedEdgeId),
1113
1157
  hidden: !edgeInView(e),
1114
1158
  }));
1115
1159
  }
1116
- return baseEdges.map((e) => ({ ...e, hidden: !edgeInView(e) }));
1117
- }, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds, issueFocusEdgeIds, resolvedEdgeView]);
1160
+ return proposedStampedEdges.map((e) => ({ ...e, hidden: !edgeInView(e) }));
1161
+ }, [proposedStampedEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds, issueFocusEdgeIds, resolvedEdgeView]);
1118
1162
  const onNodesChange = useCallback((changes) => {
1119
1163
  // Capture dimension changes (React Flow's measurement callback).
1120
1164
  // Group parents are ELK-sized — ignore their measurements.
@@ -1805,7 +1849,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1805
1849
  else {
1806
1850
  // Nothing selectable — but a module target names a boundary frame, so
1807
1851
  // frame that region.
1808
- const moduleNodeId = issueModuleNodeId(issue);
1852
+ const moduleNodeId = issueFrameNodeId(issue);
1809
1853
  if (moduleNodeId) {
1810
1854
  setSelected(null);
1811
1855
  setSelectedEdgeId(null);
@@ -1822,7 +1866,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1822
1866
  }, [
1823
1867
  issueComponent,
1824
1868
  issueStep,
1825
- issueModuleNodeId,
1869
+ issueFrameNodeId,
1826
1870
  focusTrailStep,
1827
1871
  onSelect,
1828
1872
  onSelectIssue,
@@ -1845,7 +1889,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1845
1889
  }
1846
1890
  else {
1847
1891
  const step = issueStep(issue);
1848
- const moduleNodeId = step ? null : issueModuleNodeId(issue);
1892
+ const moduleNodeId = step ? null : issueFrameNodeId(issue);
1849
1893
  if (step) {
1850
1894
  // Only unwind a step focus this card still owns. A dim-mode graph
1851
1895
  // keeps its step focus in hover state instead (owned by the pointer,
@@ -1872,7 +1916,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1872
1916
  requestAnimationFrame(() => {
1873
1917
  fitView({ padding: 0.1, duration: 300, minZoom: 0.05, maxZoom: 2 });
1874
1918
  });
1875
- }, [issueComponent, issueStep, issueModuleNodeId, fitView]);
1919
+ }, [issueComponent, issueStep, issueFrameNodeId, fitView]);
1876
1920
  // Construct tokens inside a trail snippet. A step's line is an edge
1877
1921
  // between its `from`/`to` components, so a token naming either of those
1878
1922
  // constructs should navigate to that construct's declaration. Index the
@@ -1931,6 +1975,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1931
1975
  .map((e) => {
1932
1976
  const d = e.data;
1933
1977
  const stepIssue = stepIssueBadgeByEdgeId.get(e.id);
1978
+ const proposed = proposedEdgeIds.has(e.id);
1934
1979
  return {
1935
1980
  id: e.id,
1936
1981
  mechanism: d?.mechanism ?? 'uses',
@@ -1941,10 +1986,11 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1941
1986
  pathLength: d?.pathLength ?? 0,
1942
1987
  stepNos: selectedFlowStepNos?.get(e.id),
1943
1988
  stepIssue,
1989
+ proposed,
1944
1990
  };
1945
1991
  });
1946
1992
  return labels;
1947
- }, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId]);
1993
+ }, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId, proposedEdgeIds]);
1948
1994
  // Unique source files across components → sidebar file trees.
1949
1995
  const treeFilePaths = treeFiles;
1950
1996
  // The hovered node's file (null when not hovering / file-less component).
@@ -2130,7 +2176,9 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2130
2176
  position: 'absolute',
2131
2177
  inset: 0,
2132
2178
  borderRadius: 4,
2133
- border: `0.5px solid ${color}`,
2179
+ border: lbl.proposed
2180
+ ? `1.5px dashed ${color}`
2181
+ : `0.5px solid ${color}`,
2134
2182
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
2135
2183
  pointerEvents: 'none',
2136
2184
  } })) : (_jsx("svg", { "aria-hidden": true, width: EDGE_LABEL_WIDTH, height: EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP, viewBox: `0 0 ${EDGE_LABEL_WIDTH} ${EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP}`, style: {
@@ -2138,7 +2186,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2138
2186
  left: 0,
2139
2187
  top: -EDGE_LABEL_CLOUD_EXTRA_TOP,
2140
2188
  pointerEvents: 'none',
2141
- }, children: _jsx("path", { d: EDGE_LABEL_CLOUD_PATH, fill: theme.colors.backgroundSecondary ?? theme.colors.background, stroke: hexWithAlpha(color, 0.75), strokeWidth: 1.2, strokeLinejoin: "round" }) })), _jsx("span", { style: {
2189
+ }, children: _jsx("path", { d: EDGE_LABEL_CLOUD_PATH, fill: theme.colors.backgroundSecondary ?? theme.colors.background, stroke: hexWithAlpha(color, 0.75), strokeWidth: 1.2, strokeLinejoin: "round", strokeDasharray: lbl.proposed ? '4 3' : undefined }) })), _jsx("span", { style: {
2142
2190
  position: 'relative',
2143
2191
  display: 'block',
2144
2192
  maxWidth: '100%',