@principal-ai/subsystems-react 0.53.2 → 0.53.4

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/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/C4NodeCard.d.ts +45 -10
  6. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  7. package/dist/subsystem/C4NodeCard.js +51 -20
  8. package/dist/subsystem/C4NodeCard.js.map +1 -1
  9. package/dist/subsystem/IssueList.d.ts +10 -1
  10. package/dist/subsystem/IssueList.d.ts.map +1 -1
  11. package/dist/subsystem/IssueList.js +13 -5
  12. package/dist/subsystem/IssueList.js.map +1 -1
  13. package/dist/subsystem/SubsystemComponentGraph.d.ts +8 -1
  14. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  15. package/dist/subsystem/SubsystemComponentGraph.js +26 -9
  16. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  17. package/dist/subsystem/model.d.ts +13 -2
  18. package/dist/subsystem/model.d.ts.map +1 -1
  19. package/dist/subsystem/model.js.map +1 -1
  20. package/dist/subsystem/nodes.d.ts.map +1 -1
  21. package/dist/subsystem/nodes.js +14 -6
  22. package/dist/subsystem/nodes.js.map +1 -1
  23. package/dist/utils/elkLayout.d.ts.map +1 -1
  24. package/dist/utils/elkLayout.js +28 -6
  25. package/dist/utils/elkLayout.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/index.ts +1 -1
  28. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +1 -1
  29. package/src/stories/Subsystem/C4Graph/C4Transition.stories.tsx +74 -4
  30. package/src/subsystem/C4NodeCard.test.ts +47 -8
  31. package/src/subsystem/C4NodeCard.tsx +61 -20
  32. package/src/subsystem/IssueList.tsx +30 -5
  33. package/src/subsystem/SubsystemComponentGraph.tsx +36 -8
  34. package/src/subsystem/model.ts +13 -2
  35. package/src/subsystem/nodes.tsx +14 -6
  36. package/src/utils/elkLayout.ts +29 -6
@@ -74,6 +74,7 @@ import {
74
74
  issueRung,
75
75
  ISSUE_KIND_ICON,
76
76
  ISSUE_RUNG_ORDER,
77
+ type SubsystemIssueListProps,
77
78
  type SubsystemIssue,
78
79
  type SubsystemIssueCategory,
79
80
  } from './IssueList';
@@ -354,6 +355,13 @@ export interface SubsystemComponentGraphProps {
354
355
  * plain issues view expands everything.
355
356
  */
356
357
  focusIssueCategory?: SubsystemIssueCategory;
358
+ /**
359
+ * Per-layer verification status from the host's lane summary (the same
360
+ * derivation the maintainer's lane icons use). Forwarded to the issues
361
+ * list so a layer with nothing to verify renders muted ("not applicable")
362
+ * instead of a green check for a verification that never ran.
363
+ */
364
+ laneStatus?: SubsystemIssueListProps['laneStatus'];
357
365
  /**
358
366
  * Click an issue. The graph first focuses the target on the canvas itself —
359
367
  * a component target is selected and framed (any edge / trail focus
@@ -528,7 +536,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
528
536
  measured: { w: number; h: number } | null;
529
537
  }
530
538
 
531
- function Inner({ components, trails, graphifyRelations, orderByLine, initialTrailId, onReorderTrails, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
539
+ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrailId, onReorderTrails, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
532
540
  const { theme } = useTheme();
533
541
  const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
534
542
  const viewport = useViewport();
@@ -1140,15 +1148,18 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1140
1148
  [trails],
1141
1149
  );
1142
1150
 
1143
- // Per-node diagnostics badge: fold each component-targeted finding that maps
1144
- // to a verification rung into one badge per node — worst severity wins, the
1145
- // chip shows the earliest failing rung, and the count tallies the findings.
1151
+ // Per-node diagnostics badge: fold each component-targeted finding into one
1152
+ // badge per node — worst severity wins, the chip shows the earliest failing
1153
+ // rung, and the count tallies the findings. A finding with no construct rung
1154
+ // still badges the node when it carries a dedicated kind icon (e.g. a C4
1155
+ // process gap): the chip wears the kind's icon instead of a rung's, so the
1156
+ // node says "diagnostics here" for every finding the sidebar lists.
1146
1157
  const issueBadgeByAlias = useMemo(() => {
1147
1158
  const map = new Map<string, SubsystemNodeIssue>();
1148
1159
  if (!issues?.length) return map;
1149
1160
  for (const issue of issues) {
1150
1161
  const rung = issueRung(issue.kind);
1151
- if (!rung) continue;
1162
+ if (!rung && !ISSUE_KIND_ICON[issue.kind]) continue;
1152
1163
  const comp = issueComponent(issue);
1153
1164
  if (!comp) continue;
1154
1165
  const prev = map.get(comp.alias);
@@ -1158,12 +1169,27 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1158
1169
  ? 'error'
1159
1170
  : 'info',
1160
1171
  rung:
1161
- prev != null && ISSUE_RUNG_ORDER[prev.rung] <= ISSUE_RUNG_ORDER[rung]
1162
- ? prev.rung
1163
- : rung,
1172
+ rung == null
1173
+ ? prev?.rung
1174
+ : prev?.rung != null &&
1175
+ ISSUE_RUNG_ORDER[prev.rung] <= ISSUE_RUNG_ORDER[rung]
1176
+ ? prev.rung
1177
+ : rung,
1164
1178
  count: (prev?.count ?? 0) + 1,
1165
1179
  });
1166
1180
  }
1181
+ // A rung finding wins the chip; only rung-less nodes wear a kind icon —
1182
+ // the first kind-iconed finding the sidebar lists for that node.
1183
+ for (const [alias, badge] of map) {
1184
+ if (badge.rung != null) continue;
1185
+ const kinded = issues.find(
1186
+ (i) =>
1187
+ !issueRung(i.kind) &&
1188
+ ISSUE_KIND_ICON[i.kind] &&
1189
+ issueComponent(i)?.alias === alias,
1190
+ );
1191
+ if (kinded) map.set(alias, { ...badge, kind: kinded.kind });
1192
+ }
1167
1193
  return map;
1168
1194
  }, [issues, issueComponent]);
1169
1195
 
@@ -2760,6 +2786,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2760
2786
  <SubsystemIssueList
2761
2787
  issues={issues ?? []}
2762
2788
  focusCategory={focusIssueCategory}
2789
+ laneStatus={laneStatus}
2763
2790
  onSelectIssue={focusIssueTarget}
2764
2791
  onDeselectIssue={unfocusIssueTarget}
2765
2792
  onApplyFix={onApplyIssueFix}
@@ -2772,6 +2799,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2772
2799
  <SubsystemIssueList
2773
2800
  issues={issues ?? []}
2774
2801
  focusCategory={focusIssueCategory}
2802
+ laneStatus={laneStatus}
2775
2803
  onSelectIssue={focusIssueTarget}
2776
2804
  onDeselectIssue={unfocusIssueTarget}
2777
2805
  onApplyFix={onApplyIssueFix}
@@ -1135,8 +1135,19 @@ export type SubsystemIssueRung =
1135
1135
  export interface SubsystemNodeIssue {
1136
1136
  /** Worst severity on the node — `error` (contradicts source) beats `info`. */
1137
1137
  severity: 'error' | 'info';
1138
- /** Earliest failing rung across the node's findings. */
1139
- rung: SubsystemIssueRung;
1138
+ /**
1139
+ * Earliest failing rung across the node's findings. Absent when none of the
1140
+ * node's findings sit on the construct ladder — a rung-less finding (e.g. a
1141
+ * process gap) carries its audit `kind` instead.
1142
+ */
1143
+ rung?: SubsystemIssueRung;
1144
+ /**
1145
+ * The audit kind of the node's finding when that finding has no construct
1146
+ * rung: the chip wears the kind's icon (`ISSUE_KIND_ICON`) rather than a
1147
+ * rung's. Set only when `rung` is absent — a rung finding is more specific
1148
+ * about where the construct fails, so it wins the chip.
1149
+ */
1150
+ kind?: string;
1140
1151
  /** Total findings on this node. */
1141
1152
  count: number;
1142
1153
  }
@@ -46,6 +46,7 @@ import {
46
46
  import { componentColor } from '../pierre/constructColors';
47
47
  import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
48
48
  import { ISSUE_KIND_ICON, ISSUE_RUNG_ICON } from './IssueList';
49
+ import { Search } from 'lucide-react';
49
50
  import type { LucideIcon } from 'lucide-react';
50
51
 
51
52
  export const CONSTRUCT_LABEL: Record<string, string> = {
@@ -122,11 +123,13 @@ function useSubsystemCallbacks(): SubsystemGraphCallbacks {
122
123
 
123
124
  /**
124
125
  * The diagnostics overlay for a component node: the node's own border turns
125
- * dotted (see the node's `borderStyle`) and an earliest-failing-rung corner
126
- * chip (file → symbol → declaration → type → signature) carries the severity
127
- * color, with a count when the node has more than one finding. The chip is
128
- * purely additive and non-interactive; the border stays construct-colored, so
129
- * severity never has to compete with the construct palette.
126
+ * dotted (see the node's `borderStyle`) and a severity-colored corner chip
127
+ * carries the count. The chip's icon is the earliest-failing construct rung
128
+ * (file → symbol → declaration → type → signature); a finding with no rung
129
+ * (e.g. a C4 process gap) wears its kind icon instead — see
130
+ * `SubsystemNodeIssue.kind`. The chip is purely additive and non-interactive;
131
+ * the border stays construct-colored, so severity never has to compete with
132
+ * the construct palette.
130
133
  */
131
134
  function NodeIssueOverlay({ issue }: { issue: SubsystemNodeIssue }) {
132
135
  const { theme } = useTheme();
@@ -134,10 +137,15 @@ function NodeIssueOverlay({ issue }: { issue: SubsystemNodeIssue }) {
134
137
  const color = isError
135
138
  ? (theme.colors.error ?? '#e5534b')
136
139
  : (theme.colors.warning ?? '#d4a017');
140
+ const Icon = issue.rung
141
+ ? ISSUE_RUNG_ICON[issue.rung]
142
+ : issue.kind
143
+ ? (ISSUE_KIND_ICON[issue.kind] ?? Search)
144
+ : Search;
137
145
  return (
138
146
  <IssueChip
139
147
  color={color}
140
- Icon={ISSUE_RUNG_ICON[issue.rung]}
148
+ Icon={Icon}
141
149
  count={issue.count}
142
150
  anchor={{ right: -9, bottom: -8 }}
143
151
  />
@@ -715,6 +715,28 @@ export function planCompoundGroups(
715
715
  return { built, skipped: [...skipped] };
716
716
  }
717
717
 
718
+ /**
719
+ * The four side ports every node in the graph carries, named
720
+ * `<id>_top|_right|_bottom|_left` — the ids `getPortSide` resolves an edge
721
+ * endpoint to.
722
+ *
723
+ * Shared by leaves and compound parents on purpose. A parent is still an ELK
724
+ * *node* that outside edges terminate on, and ELK rejects the whole graph if an
725
+ * edge names a port that was never declared: `JsonImportException: Referenced
726
+ * shape does not exist`. Declaring ports only on leaves silently works right up
727
+ * until the first edge crosses into a node that later becomes a group — which is
728
+ * why the drill-down only broke once a container was opened *and* something
729
+ * outside the boundary pointed at it.
730
+ */
731
+ function cardinalPorts(id: string) {
732
+ return [
733
+ { id: `${id}_top`, properties: { 'port.side': 'NORTH' } },
734
+ { id: `${id}_right`, properties: { 'port.side': 'EAST' } },
735
+ { id: `${id}_bottom`, properties: { 'port.side': 'SOUTH' } },
736
+ { id: `${id}_left`, properties: { 'port.side': 'WEST' } },
737
+ ];
738
+ }
739
+
718
740
  /**
719
741
  * Compute ELK layout for nodes and edges
720
742
  *
@@ -774,12 +796,7 @@ export async function computeElkLayout(
774
796
  x: node.position.x,
775
797
  y,
776
798
  // Add ports on each side for edge connections
777
- ports: [
778
- { id: `${node.id}_top`, properties: { 'port.side': 'NORTH' } },
779
- { id: `${node.id}_right`, properties: { 'port.side': 'EAST' } },
780
- { id: `${node.id}_bottom`, properties: { 'port.side': 'SOUTH' } },
781
- { id: `${node.id}_left`, properties: { 'port.side': 'WEST' } },
782
- ],
799
+ ports: cardinalPorts(node.id),
783
800
  properties: {
784
801
  'portConstraints': 'FIXED_SIDE',
785
802
  ...(layer !== undefined ? { 'layering.layer': String(layer) } : {}),
@@ -999,6 +1016,12 @@ export async function computeElkLayout(
999
1016
  builtGroups.set(g.id, {
1000
1017
  id: g.id,
1001
1018
  children,
1019
+ // Same side ports a leaf gets. An edge from outside the group terminates
1020
+ // on the group, and ELK throws on a port reference it cannot resolve.
1021
+ // Deliberately NOT `portConstraints: FIXED_SIDE` the way leaves are: each
1022
+ // port already names its own `port.side`, which is what routes the edge,
1023
+ // and pinning a parent could fight the layout of the children it holds.
1024
+ ports: cardinalPorts(g.id),
1002
1025
  layoutOptions:
1003
1026
  Object.keys(sizeOptions).length > 0
1004
1027
  ? { ...compoundLayoutOptions, ...sizeOptions }