@principal-ai/subsystems-react 0.17.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.
- package/README.md +93 -0
- package/dist/components/ConfigurationSelector.d.ts +35 -0
- package/dist/components/ConfigurationSelector.d.ts.map +1 -0
- package/dist/components/ConfigurationSelector.js +63 -0
- package/dist/components/ConfigurationSelector.js.map +1 -0
- package/dist/components/GraphRenderer.d.ts +295 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -0
- package/dist/components/GraphRenderer.js +2008 -0
- package/dist/components/GraphRenderer.js.map +1 -0
- package/dist/components/MultiCanvasRenderer.d.ts +75 -0
- package/dist/components/MultiCanvasRenderer.d.ts.map +1 -0
- package/dist/components/MultiCanvasRenderer.js +203 -0
- package/dist/components/MultiCanvasRenderer.js.map +1 -0
- package/dist/components/NodeTooltip.d.ts +24 -0
- package/dist/components/NodeTooltip.d.ts.map +1 -0
- package/dist/components/NodeTooltip.js +160 -0
- package/dist/components/NodeTooltip.js.map +1 -0
- package/dist/components/SelectionSidebar.d.ts +18 -0
- package/dist/components/SelectionSidebar.d.ts.map +1 -0
- package/dist/components/SelectionSidebar.js +190 -0
- package/dist/components/SelectionSidebar.js.map +1 -0
- package/dist/components/SequenceDiagramRenderer.d.ts +77 -0
- package/dist/components/SequenceDiagramRenderer.d.ts.map +1 -0
- package/dist/components/SequenceDiagramRenderer.js +604 -0
- package/dist/components/SequenceDiagramRenderer.js.map +1 -0
- package/dist/components/WorkflowSequenceDiagram.d.ts +63 -0
- package/dist/components/WorkflowSequenceDiagram.d.ts.map +1 -0
- package/dist/components/WorkflowSequenceDiagram.js +147 -0
- package/dist/components/WorkflowSequenceDiagram.js.map +1 -0
- package/dist/components/dashboard/DashboardRenderer.d.ts +10 -0
- package/dist/components/dashboard/DashboardRenderer.d.ts.map +1 -0
- package/dist/components/dashboard/DashboardRenderer.js +179 -0
- package/dist/components/dashboard/DashboardRenderer.js.map +1 -0
- package/dist/components/dashboard/MetricPanel.d.ts +10 -0
- package/dist/components/dashboard/MetricPanel.d.ts.map +1 -0
- package/dist/components/dashboard/MetricPanel.js +103 -0
- package/dist/components/dashboard/MetricPanel.js.map +1 -0
- package/dist/components/dashboard/MockDataProvider.d.ts +30 -0
- package/dist/components/dashboard/MockDataProvider.d.ts.map +1 -0
- package/dist/components/dashboard/MockDataProvider.js +270 -0
- package/dist/components/dashboard/MockDataProvider.js.map +1 -0
- package/dist/components/dashboard/components/BarChart.d.ts +10 -0
- package/dist/components/dashboard/components/BarChart.d.ts.map +1 -0
- package/dist/components/dashboard/components/BarChart.js +167 -0
- package/dist/components/dashboard/components/BarChart.js.map +1 -0
- package/dist/components/dashboard/components/LineChart.d.ts +10 -0
- package/dist/components/dashboard/components/LineChart.d.ts.map +1 -0
- package/dist/components/dashboard/components/LineChart.js +141 -0
- package/dist/components/dashboard/components/LineChart.js.map +1 -0
- package/dist/components/dashboard/components/MetricCard.d.ts +9 -0
- package/dist/components/dashboard/components/MetricCard.d.ts.map +1 -0
- package/dist/components/dashboard/components/MetricCard.js +163 -0
- package/dist/components/dashboard/components/MetricCard.js.map +1 -0
- package/dist/components/dashboard/components/SourceLink.d.ts +9 -0
- package/dist/components/dashboard/components/SourceLink.d.ts.map +1 -0
- package/dist/components/dashboard/components/SourceLink.js +39 -0
- package/dist/components/dashboard/components/SourceLink.js.map +1 -0
- package/dist/components/dashboard/components/TimeRangeSelector.d.ts +9 -0
- package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +1 -0
- package/dist/components/dashboard/components/TimeRangeSelector.js +167 -0
- package/dist/components/dashboard/components/TimeRangeSelector.js.map +1 -0
- package/dist/components/dashboard/components/index.d.ts +6 -0
- package/dist/components/dashboard/components/index.d.ts.map +1 -0
- package/dist/components/dashboard/components/index.js +6 -0
- package/dist/components/dashboard/components/index.js.map +1 -0
- package/dist/components/dashboard/index.d.ts +6 -0
- package/dist/components/dashboard/index.d.ts.map +1 -0
- package/dist/components/dashboard/index.js +8 -0
- package/dist/components/dashboard/index.js.map +1 -0
- package/dist/components/dashboard/types.d.ts +74 -0
- package/dist/components/dashboard/types.d.ts.map +1 -0
- package/dist/components/dashboard/types.js +8 -0
- package/dist/components/dashboard/types.js.map +1 -0
- package/dist/components/session-events/SessionEventFeed.d.ts +34 -0
- package/dist/components/session-events/SessionEventFeed.d.ts.map +1 -0
- package/dist/components/session-events/SessionEventFeed.js +1196 -0
- package/dist/components/session-events/SessionEventFeed.js.map +1 -0
- package/dist/components/session-events/index.d.ts +7 -0
- package/dist/components/session-events/index.d.ts.map +1 -0
- package/dist/components/session-events/index.js +6 -0
- package/dist/components/session-events/index.js.map +1 -0
- package/dist/components/state-view/PipelineView.d.ts +14 -0
- package/dist/components/state-view/PipelineView.d.ts.map +1 -0
- package/dist/components/state-view/PipelineView.js +195 -0
- package/dist/components/state-view/PipelineView.js.map +1 -0
- package/dist/components/state-view/index.d.ts +14 -0
- package/dist/components/state-view/index.d.ts.map +1 -0
- package/dist/components/state-view/index.js +12 -0
- package/dist/components/state-view/index.js.map +1 -0
- package/dist/components/state-view/types.d.ts +188 -0
- package/dist/components/state-view/types.d.ts.map +1 -0
- package/dist/components/state-view/types.js +10 -0
- package/dist/components/state-view/types.js.map +1 -0
- package/dist/components/state-view/useStateView.d.ts +32 -0
- package/dist/components/state-view/useStateView.d.ts.map +1 -0
- package/dist/components/state-view/useStateView.js +129 -0
- package/dist/components/state-view/useStateView.js.map +1 -0
- package/dist/contexts/GraphEditContext.d.ts +20 -0
- package/dist/contexts/GraphEditContext.d.ts.map +1 -0
- package/dist/contexts/GraphEditContext.js +8 -0
- package/dist/contexts/GraphEditContext.js.map +1 -0
- package/dist/contexts/TooltipPortalContext.d.ts +8 -0
- package/dist/contexts/TooltipPortalContext.d.ts.map +1 -0
- package/dist/contexts/TooltipPortalContext.js +8 -0
- package/dist/contexts/TooltipPortalContext.js.map +1 -0
- package/dist/edges/CustomEdge.d.ts +24 -0
- package/dist/edges/CustomEdge.d.ts.map +1 -0
- package/dist/edges/CustomEdge.js +222 -0
- package/dist/edges/CustomEdge.js.map +1 -0
- package/dist/edges/GenericEdge.d.ts +18 -0
- package/dist/edges/GenericEdge.d.ts.map +1 -0
- package/dist/edges/GenericEdge.js +10 -0
- package/dist/edges/GenericEdge.js.map +1 -0
- package/dist/graphify/anchor.d.ts +28 -0
- package/dist/graphify/anchor.d.ts.map +1 -0
- package/dist/graphify/anchor.js +132 -0
- package/dist/graphify/anchor.js.map +1 -0
- package/dist/graphify/consolidated.d.ts +210 -0
- package/dist/graphify/consolidated.d.ts.map +1 -0
- package/dist/graphify/consolidated.js +11 -0
- package/dist/graphify/consolidated.js.map +1 -0
- package/dist/graphify/ids.d.ts +19 -0
- package/dist/graphify/ids.d.ts.map +1 -0
- package/dist/graphify/ids.js +46 -0
- package/dist/graphify/ids.js.map +1 -0
- package/dist/graphify/index.d.ts +18 -0
- package/dist/graphify/index.d.ts.map +1 -0
- package/dist/graphify/index.js +12 -0
- package/dist/graphify/index.js.map +1 -0
- package/dist/graphify/kind.d.ts +23 -0
- package/dist/graphify/kind.d.ts.map +1 -0
- package/dist/graphify/kind.js +77 -0
- package/dist/graphify/kind.js.map +1 -0
- package/dist/graphify/resolve.d.ts +48 -0
- package/dist/graphify/resolve.d.ts.map +1 -0
- package/dist/graphify/resolve.js +83 -0
- package/dist/graphify/resolve.js.map +1 -0
- package/dist/graphify/signature.d.ts +87 -0
- package/dist/graphify/signature.d.ts.map +1 -0
- package/dist/graphify/signature.js +276 -0
- package/dist/graphify/signature.js.map +1 -0
- package/dist/graphify/types.d.ts +141 -0
- package/dist/graphify/types.d.ts.map +1 -0
- package/dist/graphify/types.js +8 -0
- package/dist/graphify/types.js.map +1 -0
- package/dist/hooks/useElkLayout.d.ts +66 -0
- package/dist/hooks/useElkLayout.d.ts.map +1 -0
- package/dist/hooks/useElkLayout.js +136 -0
- package/dist/hooks/useElkLayout.js.map +1 -0
- package/dist/hooks/usePathBasedEvents.d.ts +42 -0
- package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
- package/dist/hooks/usePathBasedEvents.js +119 -0
- package/dist/hooks/usePathBasedEvents.js.map +1 -0
- package/dist/hooks/useSequenceLayout.d.ts +186 -0
- package/dist/hooks/useSequenceLayout.d.ts.map +1 -0
- package/dist/hooks/useSequenceLayout.js +327 -0
- package/dist/hooks/useSequenceLayout.js.map +1 -0
- package/dist/hooks/useUndoRedo.d.ts +80 -0
- package/dist/hooks/useUndoRedo.d.ts.map +1 -0
- package/dist/hooks/useUndoRedo.js +70 -0
- package/dist/hooks/useUndoRedo.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts +28 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -0
- package/dist/nodes/CustomNode.js +820 -0
- package/dist/nodes/CustomNode.js.map +1 -0
- package/dist/nodes/GenericNode.d.ts +20 -0
- package/dist/nodes/GenericNode.d.ts.map +1 -0
- package/dist/nodes/GenericNode.js +20 -0
- package/dist/nodes/GenericNode.js.map +1 -0
- package/dist/nodes/otel/OtelBoundaryNode.d.ts +55 -0
- package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelBoundaryNode.js +81 -0
- package/dist/nodes/otel/OtelBoundaryNode.js.map +1 -0
- package/dist/nodes/otel/OtelEventNode.d.ts +59 -0
- package/dist/nodes/otel/OtelEventNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelEventNode.js +82 -0
- package/dist/nodes/otel/OtelEventNode.js.map +1 -0
- package/dist/nodes/otel/OtelResourceNode.d.ts +53 -0
- package/dist/nodes/otel/OtelResourceNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelResourceNode.js +105 -0
- package/dist/nodes/otel/OtelResourceNode.js.map +1 -0
- package/dist/nodes/otel/OtelScopeNode.d.ts +53 -0
- package/dist/nodes/otel/OtelScopeNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelScopeNode.js +81 -0
- package/dist/nodes/otel/OtelScopeNode.js.map +1 -0
- package/dist/nodes/otel/OtelSpanConventionNode.d.ts +61 -0
- package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +1 -0
- package/dist/nodes/otel/OtelSpanConventionNode.js +131 -0
- package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -0
- package/dist/nodes/otel/index.d.ts +17 -0
- package/dist/nodes/otel/index.d.ts.map +1 -0
- package/dist/nodes/otel/index.js +13 -0
- package/dist/nodes/otel/index.js.map +1 -0
- package/dist/nodes/otel/shared/NodeBadges.d.ts +10 -0
- package/dist/nodes/otel/shared/NodeBadges.d.ts.map +1 -0
- package/dist/nodes/otel/shared/NodeBadges.js +178 -0
- package/dist/nodes/otel/shared/NodeBadges.js.map +1 -0
- package/dist/nodes/otel/shared/NodeContent.d.ts +10 -0
- package/dist/nodes/otel/shared/NodeContent.d.ts.map +1 -0
- package/dist/nodes/otel/shared/NodeContent.js +96 -0
- package/dist/nodes/otel/shared/NodeContent.js.map +1 -0
- package/dist/nodes/otel/shared/index.d.ts +8 -0
- package/dist/nodes/otel/shared/index.d.ts.map +1 -0
- package/dist/nodes/otel/shared/index.js +8 -0
- package/dist/nodes/otel/shared/index.js.map +1 -0
- package/dist/nodes/otel/shared/types.d.ts +129 -0
- package/dist/nodes/otel/shared/types.d.ts.map +1 -0
- package/dist/nodes/otel/shared/types.js +5 -0
- package/dist/nodes/otel/shared/types.js.map +1 -0
- package/dist/nodes/otel/shared/useNodeBehavior.d.ts +42 -0
- package/dist/nodes/otel/shared/useNodeBehavior.d.ts.map +1 -0
- package/dist/nodes/otel/shared/useNodeBehavior.js +61 -0
- package/dist/nodes/otel/shared/useNodeBehavior.js.map +1 -0
- package/dist/pierre/PierreFileView.d.ts +17 -0
- package/dist/pierre/PierreFileView.d.ts.map +1 -0
- package/dist/pierre/PierreFileView.js +46 -0
- package/dist/pierre/PierreFileView.js.map +1 -0
- package/dist/pierre/PierreSnippetView.d.ts +28 -0
- package/dist/pierre/PierreSnippetView.d.ts.map +1 -0
- package/dist/pierre/PierreSnippetView.js +105 -0
- package/dist/pierre/PierreSnippetView.js.map +1 -0
- package/dist/pierre/PierreThroughlineCodeView.d.ts +22 -0
- package/dist/pierre/PierreThroughlineCodeView.d.ts.map +1 -0
- package/dist/pierre/PierreThroughlineCodeView.js +206 -0
- package/dist/pierre/PierreThroughlineCodeView.js.map +1 -0
- package/dist/pierre/constructColors.d.ts +24 -0
- package/dist/pierre/constructColors.d.ts.map +1 -0
- package/dist/pierre/constructColors.js +91 -0
- package/dist/pierre/constructColors.js.map +1 -0
- package/dist/pierre/index.d.ts +12 -0
- package/dist/pierre/index.d.ts.map +1 -0
- package/dist/pierre/index.js +7 -0
- package/dist/pierre/index.js.map +1 -0
- package/dist/pierre/pierreBackground.d.ts +15 -0
- package/dist/pierre/pierreBackground.d.ts.map +1 -0
- package/dist/pierre/pierreBackground.js +31 -0
- package/dist/pierre/pierreBackground.js.map +1 -0
- package/dist/pierre/pierreFileLang.d.ts +27 -0
- package/dist/pierre/pierreFileLang.d.ts.map +1 -0
- package/dist/pierre/pierreFileLang.js +52 -0
- package/dist/pierre/pierreFileLang.js.map +1 -0
- package/dist/pierre/pierreSyntaxTheme.d.ts +11 -0
- package/dist/pierre/pierreSyntaxTheme.d.ts.map +1 -0
- package/dist/pierre/pierreSyntaxTheme.js +20 -0
- package/dist/pierre/pierreSyntaxTheme.js.map +1 -0
- package/dist/pierre/scrollAnchor.d.ts +5 -0
- package/dist/pierre/scrollAnchor.d.ts.map +1 -0
- package/dist/pierre/scrollAnchor.js +9 -0
- package/dist/pierre/scrollAnchor.js.map +1 -0
- package/dist/pierre/sliceSnippet.d.ts +10 -0
- package/dist/pierre/sliceSnippet.d.ts.map +1 -0
- package/dist/pierre/sliceSnippet.js +16 -0
- package/dist/pierre/sliceSnippet.js.map +1 -0
- package/dist/pierre/syntaxTokens.d.ts +11 -0
- package/dist/pierre/syntaxTokens.d.ts.map +1 -0
- package/dist/pierre/syntaxTokens.js +51 -0
- package/dist/pierre/syntaxTokens.js.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
- package/dist/subsystem/ComponentDeclaration.d.ts +87 -0
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -0
- package/dist/subsystem/ComponentDeclaration.js +374 -0
- package/dist/subsystem/ComponentDeclaration.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +17 -0
- package/dist/subsystem/FileDrawer.d.ts.map +1 -0
- package/dist/subsystem/FileDrawer.js +72 -0
- package/dist/subsystem/FileDrawer.js.map +1 -0
- package/dist/subsystem/GraphLayoutCover.d.ts +20 -0
- package/dist/subsystem/GraphLayoutCover.d.ts.map +1 -0
- package/dist/subsystem/GraphLayoutCover.js +42 -0
- package/dist/subsystem/GraphLayoutCover.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +96 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.js +1059 -0
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -0
- package/dist/subsystem/SubsystemFileTree.d.ts +24 -0
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -0
- package/dist/subsystem/SubsystemFileTree.js +150 -0
- package/dist/subsystem/SubsystemFileTree.js.map +1 -0
- package/dist/subsystem/declarationRef.d.ts +39 -0
- package/dist/subsystem/declarationRef.d.ts.map +1 -0
- package/dist/subsystem/declarationRef.js +32 -0
- package/dist/subsystem/declarationRef.js.map +1 -0
- package/dist/subsystem/formatDeclaration.d.ts +11 -0
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -0
- package/dist/subsystem/formatDeclaration.js +146 -0
- package/dist/subsystem/formatDeclaration.js.map +1 -0
- package/dist/subsystem/model.d.ts +353 -0
- package/dist/subsystem/model.d.ts.map +1 -0
- package/dist/subsystem/model.js +503 -0
- package/dist/subsystem/model.js.map +1 -0
- package/dist/subsystem/nodes.d.ts +69 -0
- package/dist/subsystem/nodes.d.ts.map +1 -0
- package/dist/subsystem/nodes.js +289 -0
- package/dist/subsystem/nodes.js.map +1 -0
- package/dist/subsystem/paths.d.ts +63 -0
- package/dist/subsystem/paths.d.ts.map +1 -0
- package/dist/subsystem/paths.js +86 -0
- package/dist/subsystem/paths.js.map +1 -0
- package/dist/subsystem/tokenizeComponent.d.ts +20 -0
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -0
- package/dist/subsystem/tokenizeComponent.js +56 -0
- package/dist/subsystem/tokenizeComponent.js.map +1 -0
- package/dist/subsystem/tokenizeFormatted.d.ts +12 -0
- package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -0
- package/dist/subsystem/tokenizeFormatted.js +13 -0
- package/dist/subsystem/tokenizeFormatted.js.map +1 -0
- package/dist/utils/animationMapping.d.ts +53 -0
- package/dist/utils/animationMapping.d.ts.map +1 -0
- package/dist/utils/animationMapping.js +126 -0
- package/dist/utils/animationMapping.js.map +1 -0
- package/dist/utils/canvasBounds.d.ts +50 -0
- package/dist/utils/canvasBounds.d.ts.map +1 -0
- package/dist/utils/canvasBounds.js +88 -0
- package/dist/utils/canvasBounds.js.map +1 -0
- package/dist/utils/elkLayout.d.ts +137 -0
- package/dist/utils/elkLayout.d.ts.map +1 -0
- package/dist/utils/elkLayout.js +596 -0
- package/dist/utils/elkLayout.js.map +1 -0
- package/dist/utils/graphConverter.d.ts +18 -0
- package/dist/utils/graphConverter.d.ts.map +1 -0
- package/dist/utils/graphConverter.js +114 -0
- package/dist/utils/graphConverter.js.map +1 -0
- package/dist/utils/iconResolver.d.ts +29 -0
- package/dist/utils/iconResolver.d.ts.map +1 -0
- package/dist/utils/iconResolver.js +30 -0
- package/dist/utils/iconResolver.js.map +1 -0
- package/dist/utils/orientationUtils.d.ts +19 -0
- package/dist/utils/orientationUtils.d.ts.map +1 -0
- package/dist/utils/orientationUtils.js +62 -0
- package/dist/utils/orientationUtils.js.map +1 -0
- package/package.json +80 -0
- package/src/components/ConfigurationSelector.tsx +133 -0
- package/src/components/GraphRenderer.tsx +3195 -0
- package/src/components/MultiCanvasRenderer.tsx +295 -0
- package/src/components/NodeTooltip.tsx +279 -0
- package/src/components/PendingChanges.test.tsx +375 -0
- package/src/components/SelectionSidebar.tsx +352 -0
- package/src/components/SequenceDiagramRenderer.tsx +1156 -0
- package/src/components/WorkflowSequenceDiagram.tsx +270 -0
- package/src/components/dashboard/DashboardRenderer.tsx +317 -0
- package/src/components/dashboard/MetricPanel.tsx +254 -0
- package/src/components/dashboard/MockDataProvider.ts +330 -0
- package/src/components/dashboard/components/BarChart.tsx +299 -0
- package/src/components/dashboard/components/LineChart.tsx +279 -0
- package/src/components/dashboard/components/MetricCard.tsx +270 -0
- package/src/components/dashboard/components/SourceLink.tsx +63 -0
- package/src/components/dashboard/components/TimeRangeSelector.tsx +280 -0
- package/src/components/dashboard/components/index.ts +5 -0
- package/src/components/dashboard/index.ts +47 -0
- package/src/components/dashboard/types.ts +126 -0
- package/src/components/session-events/SessionEventFeed.tsx +1797 -0
- package/src/components/session-events/index.ts +13 -0
- package/src/components/state-view/PipelineView.tsx +347 -0
- package/src/components/state-view/index.ts +14 -0
- package/src/components/state-view/types.ts +261 -0
- package/src/components/state-view/useStateView.ts +205 -0
- package/src/contexts/GraphEditContext.tsx +27 -0
- package/src/contexts/TooltipPortalContext.ts +8 -0
- package/src/edges/CustomEdge.tsx +402 -0
- package/src/edges/GenericEdge.tsx +36 -0
- package/src/graphify/anchor.test.ts +124 -0
- package/src/graphify/anchor.ts +160 -0
- package/src/graphify/consolidated.ts +246 -0
- package/src/graphify/ids.ts +48 -0
- package/src/graphify/index.ts +72 -0
- package/src/graphify/kind.test.ts +108 -0
- package/src/graphify/kind.ts +120 -0
- package/src/graphify/resolve.test.ts +89 -0
- package/src/graphify/resolve.ts +115 -0
- package/src/graphify/signature.test.ts +278 -0
- package/src/graphify/signature.ts +354 -0
- package/src/graphify/types.ts +164 -0
- package/src/hooks/useElkLayout.test.ts +134 -0
- package/src/hooks/useElkLayout.ts +191 -0
- package/src/hooks/usePathBasedEvents.ts +192 -0
- package/src/hooks/useSequenceLayout.ts +543 -0
- package/src/hooks/useUndoRedo.ts +144 -0
- package/src/index.ts +341 -0
- package/src/nodes/CustomNode.tsx +1231 -0
- package/src/nodes/GenericNode.tsx +55 -0
- package/src/nodes/otel/OtelBoundaryNode.tsx +224 -0
- package/src/nodes/otel/OtelEventNode.tsx +225 -0
- package/src/nodes/otel/OtelResourceNode.tsx +251 -0
- package/src/nodes/otel/OtelScopeNode.tsx +221 -0
- package/src/nodes/otel/OtelSpanConventionNode.tsx +295 -0
- package/src/nodes/otel/index.ts +23 -0
- package/src/nodes/otel/shared/NodeBadges.tsx +295 -0
- package/src/nodes/otel/shared/NodeContent.tsx +204 -0
- package/src/nodes/otel/shared/index.ts +8 -0
- package/src/nodes/otel/shared/types.ts +138 -0
- package/src/nodes/otel/shared/useNodeBehavior.ts +114 -0
- package/src/pierre/PierreFileView.tsx +81 -0
- package/src/pierre/PierreSnippetView.tsx +184 -0
- package/src/pierre/PierreThroughlineCodeView.tsx +304 -0
- package/src/pierre/constructColors.test.ts +52 -0
- package/src/pierre/constructColors.ts +112 -0
- package/src/pierre/index.ts +18 -0
- package/src/pierre/pierreBackground.ts +33 -0
- package/src/pierre/pierreFileLang.test.ts +26 -0
- package/src/pierre/pierreFileLang.ts +55 -0
- package/src/pierre/pierreSyntaxTheme.ts +23 -0
- package/src/pierre/scrollAnchor.test.ts +13 -0
- package/src/pierre/scrollAnchor.ts +11 -0
- package/src/pierre/sliceSnippet.test.ts +21 -0
- package/src/pierre/sliceSnippet.ts +31 -0
- package/src/pierre/syntaxTokens.test.ts +35 -0
- package/src/pierre/syntaxTokens.ts +60 -0
- package/src/stories/Audit/BoundaryNodes.stories.tsx +385 -0
- package/src/stories/Audit/CanvasEdgeTypes.stories.tsx +1448 -0
- package/src/stories/Audit/CanvasNodeTypes.stories.tsx +1049 -0
- package/src/stories/Audit/ColorPriority.stories.tsx +895 -0
- package/src/stories/Audit/DiamondBadges.stories.tsx +629 -0
- package/src/stories/Audit/NodeDimensionsTesting.stories.tsx +724 -0
- package/src/stories/Audit/NodeFieldsAudit.stories.tsx +996 -0
- package/src/stories/Audit/NodeFontSizeTesting.stories.tsx +715 -0
- package/src/stories/Audit/NodeShapes.stories.tsx +1208 -0
- package/src/stories/Audit/WideNodesTesting.stories.tsx +1045 -0
- package/src/stories/Components/DashboardRenderer.stories.tsx +263 -0
- package/src/stories/Components/GraphRenderer.stories.tsx +3236 -0
- package/src/stories/Components/MultiCanvasRenderer.stories.tsx +370 -0
- package/src/stories/Components/SequenceDiagram.stories.tsx +255 -0
- package/src/stories/Components/StateView.stories.tsx +417 -0
- package/src/stories/Components/TraceViewer.stories.tsx +442 -0
- package/src/stories/Components/WorkflowSequenceDiagram.stories.tsx +802 -0
- package/src/stories/Features/AnimationWorkshop.stories.tsx +727 -0
- package/src/stories/Features/ChangeTypeVisual.stories.tsx +67 -0
- package/src/stories/Features/EventDrivenAnimations.stories.tsx +513 -0
- package/src/stories/Features/MultiConfig.stories.tsx +539 -0
- package/src/stories/Introduction.mdx +34 -0
- package/src/stories/Layout/ElkEdgeRouting.stories.tsx +539 -0
- package/src/stories/Layout/GraphOrientation.stories.tsx +500 -0
- package/src/stories/Layout/MultiDirectionalConnections.stories.tsx +352 -0
- package/src/stories/Nodes/NodeDefinitionComparison.stories.tsx +560 -0
- package/src/stories/Nodes/NodeNameFallbacks.stories.tsx +491 -0
- package/src/stories/Nodes/NumericColors.stories.tsx +440 -0
- package/src/stories/OTEL/EventNameDisplay.stories.tsx +694 -0
- package/src/stories/OTEL/EventNameFallback.stories.tsx +578 -0
- package/src/stories/OTEL/OtelComponents.stories.tsx +422 -0
- package/src/stories/OTEL/OtelNodeTypesPrototype.stories.tsx +806 -0
- package/src/stories/OTEL/RawEventCards.stories.tsx +57 -0
- package/src/stories/OTEL/SpanBadges.stories.tsx +840 -0
- package/src/stories/OTEL/ValidationEventsCanvas.stories.tsx +209 -0
- package/src/stories/Pierre/CodeView.stories.tsx +219 -0
- package/src/stories/RealWorld/BacklogMdCanvases.stories.tsx +721 -0
- package/src/stories/RealWorld/FileCitySequence.stories.tsx +516 -0
- package/src/stories/RealWorld/RealCanvasFiles.stories.tsx +336 -0
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +321 -0
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +239 -0
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +216 -0
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +70 -0
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +68 -0
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +681 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
- package/src/stories/__traces__/test-run.canvas.json +241 -0
- package/src/stories/data/activity-feed-analytics.dashboard.json +300 -0
- package/src/stories/data/file-city-images-transformed.otel.canvas.json +295 -0
- package/src/stories/data/file-city-workflows.json +269 -0
- package/src/stories/data/graph-converter-test-execution.json +443 -0
- package/src/stories/data/graph-converter-test.workflow.json +105 -0
- package/src/stories/data/graph-converter-validated-execution.json +58 -0
- package/src/stories/data/opencode-session-raw-events.json +273396 -0
- package/src/subsystem/ComponentDeclaration.tsx +643 -0
- package/src/subsystem/FileDrawer.tsx +106 -0
- package/src/subsystem/GraphLayoutCover.tsx +69 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +1644 -0
- package/src/subsystem/SubsystemFileTree.tsx +190 -0
- package/src/subsystem/declarationRef.test.ts +33 -0
- package/src/subsystem/declarationRef.ts +63 -0
- package/src/subsystem/formatDeclaration.ts +174 -0
- package/src/subsystem/model.test.ts +244 -0
- package/src/subsystem/model.ts +850 -0
- package/src/subsystem/nodes.test.ts +56 -0
- package/src/subsystem/nodes.tsx +443 -0
- package/src/subsystem/paths.test.ts +123 -0
- package/src/subsystem/paths.ts +127 -0
- package/src/subsystem/tokenizeComponent.ts +70 -0
- package/src/subsystem/tokenizeFormatted.ts +19 -0
- package/src/utils/animationMapping.ts +166 -0
- package/src/utils/canvasBounds.ts +146 -0
- package/src/utils/elkLayout.test.ts +240 -0
- package/src/utils/elkLayout.ts +779 -0
- package/src/utils/graphConverter.ts +148 -0
- package/src/utils/iconResolver.tsx +53 -0
- package/src/utils/orientationUtils.ts +73 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subsystem component-graph data model + converters.
|
|
3
|
+
*
|
|
4
|
+
* A subsystem snapshot (see the "Subsystem artifact: facets" topic) is captured
|
|
5
|
+
* as component nodes (construct-tagged source units), file refs, integration edges,
|
|
6
|
+
* and entry points. This module defines the minimal document shape for the
|
|
7
|
+
* *graph* facet and converts it to React Flow nodes/edges — packages render as
|
|
8
|
+
* subgraphs, only cross-package edges leave the box, and shared seams
|
|
9
|
+
* (registries/barrels/facades) are edge targets, never member nodes.
|
|
10
|
+
*
|
|
11
|
+
* Self-contained in this package (not yet promoted to `@principal-ai/core`), so
|
|
12
|
+
* we can iterate on the UI + story without publishing a dependency.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
MarkerType,
|
|
17
|
+
type Edge,
|
|
18
|
+
type Node,
|
|
19
|
+
} from '@xyflow/react';
|
|
20
|
+
import { computeElkLayout, calculatePathLength } from '../utils/elkLayout';
|
|
21
|
+
import type { GraphifyComponentDetail } from '../graphify';
|
|
22
|
+
import type { SubsystemDeclarationRef } from './declarationRef';
|
|
23
|
+
|
|
24
|
+
export type SubsystemComponentConstruct =
|
|
25
|
+
| 'class'
|
|
26
|
+
| 'function'
|
|
27
|
+
| 'method'
|
|
28
|
+
| 'interface'
|
|
29
|
+
| 'type_alias'
|
|
30
|
+
| 'enum'
|
|
31
|
+
| 'module'
|
|
32
|
+
| 'store'
|
|
33
|
+
| 'external';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Semantic role — where the node sits in the topology, orthogonal to
|
|
37
|
+
* `construct` (what it is). Roles have *inherited* anatomy: an entry renders
|
|
38
|
+
* as its real code shape (function dispatcher, type contract); a service has
|
|
39
|
+
* no source at all (`construct: 'external'` + purl identity). Contrast with
|
|
40
|
+
* `construct: 'store'`, which introduces its own anatomy (the state block) —
|
|
41
|
+
* that is why store is a construct and not a role. Role nodes must stay anchored to real code: an
|
|
42
|
+
* `entry` is a boundary element (route dispatcher, message contract) carrying
|
|
43
|
+
* the wire address as identity; a `service` is an external system the process
|
|
44
|
+
* calls out to (identity via purl, no `process` — it belongs to no region).
|
|
45
|
+
*/
|
|
46
|
+
export type SubsystemComponentRole = 'entry' | 'service';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Framework that owns a stereotype vocabulary (open string).
|
|
50
|
+
* Examples: `react`, `vue`, `nestjs`, `django`, `spring`.
|
|
51
|
+
* Empty when the node is language-only / framework-agnostic.
|
|
52
|
+
*/
|
|
53
|
+
export type SubsystemFramework = string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Framework-level pattern stamped on a language construct (open string).
|
|
57
|
+
* Examples: `component`, `hook`, `middleware`, `controller`, `guard`.
|
|
58
|
+
* Empty when no framework pattern applies. Pair with `framework` when set —
|
|
59
|
+
* a React component stays `construct: 'function'` with
|
|
60
|
+
* `framework: 'react'` + `stereotype: 'component'`.
|
|
61
|
+
*/
|
|
62
|
+
export type SubsystemStereotype = string;
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Declaration tokens — structured source representation
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export type SubsystemDeclTokenKind =
|
|
69
|
+
| 'keyword'
|
|
70
|
+
| 'name'
|
|
71
|
+
| 'member'
|
|
72
|
+
| 'type'
|
|
73
|
+
| 'punctuation'
|
|
74
|
+
| 'string'
|
|
75
|
+
| 'newline';
|
|
76
|
+
|
|
77
|
+
export interface SubsystemDeclToken {
|
|
78
|
+
text: string;
|
|
79
|
+
kind: SubsystemDeclTokenKind;
|
|
80
|
+
/** Shiki/Pierre foreground when tokenized client-side; omitted on legacy wire tokens. */
|
|
81
|
+
color?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type SubsystemEdgeMechanism =
|
|
85
|
+
| 'imports'
|
|
86
|
+
| 'imports_from'
|
|
87
|
+
| 're_exports'
|
|
88
|
+
| 'defines'
|
|
89
|
+
| 'calls'
|
|
90
|
+
| 'extends'
|
|
91
|
+
| 'inherits'
|
|
92
|
+
| 'implements'
|
|
93
|
+
| 'mixes_in'
|
|
94
|
+
| 'uses'
|
|
95
|
+
| 'method'
|
|
96
|
+
| 'references'
|
|
97
|
+
| 'contains'
|
|
98
|
+
| 'feeds'
|
|
99
|
+
| 'produces'
|
|
100
|
+
| 'writes'
|
|
101
|
+
| 'reads'
|
|
102
|
+
| 'watches'
|
|
103
|
+
| 'registers-into';
|
|
104
|
+
|
|
105
|
+
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
106
|
+
export interface SubsystemComponent {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
/**
|
|
110
|
+
* The node's construct — what it IS as a declaration (class, function,
|
|
111
|
+
* method, interface, type alias, enum, store, external), driving node
|
|
112
|
+
* anatomy, color, badge, and the verification strategy. Every construct
|
|
113
|
+
* anchors to a definition; runtime occurrences (variables, activations,
|
|
114
|
+
* instances) are NOT constructs — they belong to a future execution-mode
|
|
115
|
+
* graph whose occurrence nodes reference these definitions. Ontology:
|
|
116
|
+
* construct = what it is, framework + stereotype = which framework pattern
|
|
117
|
+
* it plays, role = where it sits, process = where it runs. Prefer
|
|
118
|
+
* `framework` + `stereotype` over inventing framework-specific constructs
|
|
119
|
+
* (a React component is still `construct: 'function'`).
|
|
120
|
+
*/
|
|
121
|
+
construct: SubsystemComponentConstruct;
|
|
122
|
+
/** Source location the component lives in (repo-root-relative path). */
|
|
123
|
+
file: string;
|
|
124
|
+
/** PURL identifying the repo or package this component lives in (for subgraph grouping). */
|
|
125
|
+
purl: string;
|
|
126
|
+
/** One-line purpose shown on the node. */
|
|
127
|
+
purpose?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Semantic role — where the node sits in the topology (boundary element,
|
|
130
|
+
* external system), orthogonal to `construct` (what it is). Drives the
|
|
131
|
+
* glyph and the edge-pairing rules: boundary-crossing edges must terminate
|
|
132
|
+
* at an entry or a service. Retained state is NOT a role — it is
|
|
133
|
+
* `construct: 'store'` (state-block anatomy, `writes`/`reads`/`watches`
|
|
134
|
+
* inbound, `produces` outbound).
|
|
135
|
+
*/
|
|
136
|
+
role?: SubsystemComponentRole;
|
|
137
|
+
/**
|
|
138
|
+
* Framework that owns the stereotype vocabulary (e.g. `react`, `nestjs`).
|
|
139
|
+
* Orthogonal to `construct` — leave empty for language-only units.
|
|
140
|
+
*/
|
|
141
|
+
framework?: SubsystemFramework;
|
|
142
|
+
/**
|
|
143
|
+
* Framework pattern this declaration plays (e.g. `component`, `hook`).
|
|
144
|
+
* When set, the node badge prefers this label over the construct name so
|
|
145
|
+
* a React UI unit reads as "component" rather than "function".
|
|
146
|
+
*/
|
|
147
|
+
stereotype?: SubsystemStereotype;
|
|
148
|
+
/**
|
|
149
|
+
* Runtime process membership — which deployment unit this node is a
|
|
150
|
+
* member of (e.g. `principal-studio/host`, `principal-studio/renderer`). Nodes
|
|
151
|
+
* sharing a `process` are drawn inside one boundary region (grouping is
|
|
152
|
+
* `process ?? purl`); nodes without one sit outside every boundary
|
|
153
|
+
* (external actors, services, libraries).
|
|
154
|
+
*/
|
|
155
|
+
process?: string;
|
|
156
|
+
/** A symbol this component exposes / is (the node's identity). */
|
|
157
|
+
symbol?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Semantic layer/phase for the layout (e.g. `1` = input, `2` = processing,
|
|
160
|
+
* `3` = output). When set, ELK places the component in this layer so the
|
|
161
|
+
* graph reads as a left-to-right pipeline and *conveys the idea* rather than
|
|
162
|
+
* letting ELK guess the order.
|
|
163
|
+
*/
|
|
164
|
+
layer?: number;
|
|
165
|
+
/**
|
|
166
|
+
* How this session *occupied* the component — `edited` (modified it) vs
|
|
167
|
+
* `analyzed` (read deeply / built an understanding) vs `referenced`
|
|
168
|
+
* (touched in passing). Drives clustering: edits strengthen a subsystem,
|
|
169
|
+
* but a read-only investigation can still form one around an analyzed seam.
|
|
170
|
+
*/
|
|
171
|
+
capture?: 'edited' | 'analyzed' | 'referenced';
|
|
172
|
+
/**
|
|
173
|
+
* Graphify drill-down detail for this component, when the facet is anchored
|
|
174
|
+
* to a graphify node. Discriminated by kind (class/function/type/module/store/
|
|
175
|
+
* external); rendered in the detail panel on click.
|
|
176
|
+
*/
|
|
177
|
+
detail?: GraphifyComponentDetail;
|
|
178
|
+
/**
|
|
179
|
+
* Where the drill-down `detail` came from. `verified` = extracted from
|
|
180
|
+
* source by tooling (graphify AST, signature extraction) — may be trusted
|
|
181
|
+
* as matching the code. `authored` = written by the authoring agent/human
|
|
182
|
+
* to highlight specific inputs/outputs — informative, not checked against
|
|
183
|
+
* source. Detail without provenance is treated as `authored`; only tooling
|
|
184
|
+
* may claim `verified`.
|
|
185
|
+
*/
|
|
186
|
+
detailProvenance?: 'verified' | 'authored';
|
|
187
|
+
/**
|
|
188
|
+
* Pre-tokenized declaration for the detail panel. When present, the
|
|
189
|
+
* renderer skips client-side tokenization. Tokens are language-agnostic;
|
|
190
|
+
* a different language just needs a different tokenizer and text joiner.
|
|
191
|
+
*/
|
|
192
|
+
tokens?: SubsystemDeclToken[];
|
|
193
|
+
/**
|
|
194
|
+
* Anchored declaration location: graphify start line + hash of that line's
|
|
195
|
+
* content at capture time. Populated by verify when an exact anchor resolves.
|
|
196
|
+
*/
|
|
197
|
+
declarationRef?: SubsystemDeclarationRef;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A cross-component edge in the subsystem graph. */
|
|
201
|
+
export interface SubsystemComponentEdge {
|
|
202
|
+
id: string;
|
|
203
|
+
from: string; // component id
|
|
204
|
+
to: string; // component id or external target label
|
|
205
|
+
mechanism: SubsystemEdgeMechanism;
|
|
206
|
+
/** Concrete file/symbol refs backing the edge (the seam). */
|
|
207
|
+
refs?: string[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A single site on an existing edge — the exact `file:line` where that edge's
|
|
212
|
+
* seam manifests for a given flow. The edge stays the abstract contract
|
|
213
|
+
* (`from`, `to`, `mechanism`); a throughline step picks the concrete
|
|
214
|
+
* manifestation. One edge can appear in many steps.
|
|
215
|
+
*/
|
|
216
|
+
export interface SubsystemThroughlineStep {
|
|
217
|
+
/** Id of the existing edge this hop traverses. */
|
|
218
|
+
edgeId: string;
|
|
219
|
+
/** Repo-root-relative path of the file where the edge fires. */
|
|
220
|
+
file: string;
|
|
221
|
+
/** 1-based line of the site within `file`. */
|
|
222
|
+
line: number;
|
|
223
|
+
/**
|
|
224
|
+
* Frame name for this hop — the function/method/symbol on the stack at
|
|
225
|
+
* this site. Optional so existing throughlines keep working; when set the
|
|
226
|
+
* flows list shows it instead of mechanism + filename.
|
|
227
|
+
*/
|
|
228
|
+
symbol?: string;
|
|
229
|
+
/**
|
|
230
|
+
* Free-text note anchored to this hop's site line. Optional — informative
|
|
231
|
+
* only, never verified against source; the Pierre throughline code view
|
|
232
|
+
* surfaces it in the annotation column next to the highlighted line.
|
|
233
|
+
*/
|
|
234
|
+
annotation?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* An ordered execution story over a graph's edges — each step references an
|
|
239
|
+
* existing edge and the exact site where that relationship fires for a flow;
|
|
240
|
+
* ordering is the array. One throughline per flow (save flow, load flow, …).
|
|
241
|
+
*/
|
|
242
|
+
export interface SubsystemThroughline {
|
|
243
|
+
id: string;
|
|
244
|
+
title: string;
|
|
245
|
+
steps: SubsystemThroughlineStep[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Derive a consistent display `name` from a code `symbol` + construct.
|
|
250
|
+
*
|
|
251
|
+
* `symbol` is the source of truth (fully-qualified code identity). The name
|
|
252
|
+
* is the symbol itself:
|
|
253
|
+
* - class/type/module/function/script/... symbol → symbol (e.g. `SessionReader`)
|
|
254
|
+
* - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
|
|
255
|
+
* - module, no symbol → basename of `file` (e.g. `transcript.ts` → `transcript`) —
|
|
256
|
+
* a common-sense convention for whole-file modules, not a real TS name
|
|
257
|
+
* - otherwise no symbol → fall back to an existing name
|
|
258
|
+
*/
|
|
259
|
+
export function deriveNameFromSymbol(
|
|
260
|
+
symbol: string | undefined,
|
|
261
|
+
construct: SubsystemComponentConstruct,
|
|
262
|
+
existingName?: string,
|
|
263
|
+
file?: string,
|
|
264
|
+
stereotype?: string,
|
|
265
|
+
): string {
|
|
266
|
+
let name: string | undefined;
|
|
267
|
+
if (symbol && symbol.trim()) {
|
|
268
|
+
name = symbol;
|
|
269
|
+
} else if (construct === 'module' && file) {
|
|
270
|
+
const base = file.split('/').pop() ?? '';
|
|
271
|
+
const clean = base.replace(/\.[^.]+$/, ''); // strip extension
|
|
272
|
+
if (clean) name = clean;
|
|
273
|
+
}
|
|
274
|
+
if (!name) name = existingName ?? 'untitled';
|
|
275
|
+
|
|
276
|
+
// Decorations = what the drill-down shows. Framework stereotypes can override
|
|
277
|
+
// the language decoration (a React component wears `<>` instead of `()`).
|
|
278
|
+
// Executable constructs wear `()`; brace-bodied constructs (interface,
|
|
279
|
+
// type_alias, enum) wear ` {}`. Classes render bare — the construct badge
|
|
280
|
+
// already says "class".
|
|
281
|
+
// Everything else (class, store, module, external) renders bare.
|
|
282
|
+
if (stereotype === 'component' && !name.startsWith('<')) {
|
|
283
|
+
return `<${name}>`;
|
|
284
|
+
}
|
|
285
|
+
if ((construct === 'function' || construct === 'method') && !name.endsWith('()')) {
|
|
286
|
+
name = `${name}()`;
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
(construct === 'interface' ||
|
|
290
|
+
construct === 'type_alias' ||
|
|
291
|
+
construct === 'enum') &&
|
|
292
|
+
!name.endsWith('{}')
|
|
293
|
+
) {
|
|
294
|
+
name = `${name} {}`;
|
|
295
|
+
}
|
|
296
|
+
return name;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Human-readable purl identity — drops the `pkg:<type>/` scheme wrapper and
|
|
301
|
+
* trailing version, keeping the package / owner-repo identity:
|
|
302
|
+
*
|
|
303
|
+
* pkg:npm/@principal-ai/core → `@principal-ai/core`
|
|
304
|
+
* pkg:github/principal-ai/my-repo → `principal-ai/my-repo`
|
|
305
|
+
* pkg:npm/left-pad@1.3.0 → `left-pad`
|
|
306
|
+
* pkg:generic/local--Users-me-my-app → `Users-me-my-app (local)`
|
|
307
|
+
*
|
|
308
|
+
* Local purls encode the absolute path with dashes for slashes, which is not
|
|
309
|
+
* losslessly decodable — shown as-is with a `(local)` marker.
|
|
310
|
+
*/
|
|
311
|
+
export function formatPurl(purl: string): string {
|
|
312
|
+
const match = /^pkg:[^/#?]+\/(.+)$/.exec(purl);
|
|
313
|
+
if (!match) return purl;
|
|
314
|
+
let identity = match[1].split(/[?#]/)[0];
|
|
315
|
+
const at = identity.indexOf('@');
|
|
316
|
+
if (at > 0) identity = identity.slice(0, at); // trailing @version
|
|
317
|
+
if (identity.startsWith('local--')) {
|
|
318
|
+
return `${identity.slice('local--'.length)} (local)`;
|
|
319
|
+
}
|
|
320
|
+
return identity;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface SubsystemModelDocument {
|
|
324
|
+
components: SubsystemComponent[];
|
|
325
|
+
edges: SubsystemComponentEdge[];
|
|
326
|
+
/** Ordered execution stories over the graph's edges (one per flow). */
|
|
327
|
+
throughlines?: SubsystemThroughline[];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// React Flow conversion
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
export type SubsystemGraphNodeType = 'subsystem-component' | 'subsystem-group';
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* One process boundary region — all components sharing a `process` value.
|
|
338
|
+
* Nodes without a `process` sit outside every boundary (no region).
|
|
339
|
+
*/
|
|
340
|
+
export interface SubsystemProcessRegion {
|
|
341
|
+
/** The `process` value (e.g. `principal-studio/host`). */
|
|
342
|
+
key: string;
|
|
343
|
+
/** Display label for the boundary frame. */
|
|
344
|
+
label: string;
|
|
345
|
+
/** Component ids that are members of this region. */
|
|
346
|
+
memberIds: string[];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** React Flow id for a process boundary group node. */
|
|
350
|
+
export function processGroupNodeId(processKey: string): string {
|
|
351
|
+
return `process:${processKey}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Derive boundary regions from a document — one per distinct non-empty
|
|
356
|
+
* `process` value, in first-appearance order.
|
|
357
|
+
*/
|
|
358
|
+
export function getSubsystemRegions(
|
|
359
|
+
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
360
|
+
): SubsystemProcessRegion[] {
|
|
361
|
+
const byProcess = new Map<string, string[]>();
|
|
362
|
+
for (const c of doc.components) {
|
|
363
|
+
const p = c.process?.trim();
|
|
364
|
+
if (!p) continue;
|
|
365
|
+
const list = byProcess.get(p) ?? [];
|
|
366
|
+
list.push(c.id);
|
|
367
|
+
byProcess.set(p, list);
|
|
368
|
+
}
|
|
369
|
+
return [...byProcess.entries()].map(([key, memberIds]) => ({
|
|
370
|
+
key,
|
|
371
|
+
label: key,
|
|
372
|
+
memberIds,
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
377
|
+
component: SubsystemComponent;
|
|
378
|
+
/** Set while a file is open in the drawer: true if this node's component
|
|
379
|
+
* lives in that file (spotlighted), false otherwise (dimmed). Absent when
|
|
380
|
+
* no file is open — render neutrally. */
|
|
381
|
+
fileMatch?: boolean;
|
|
382
|
+
/** True while this node is on an opened-but-unselected flow. */
|
|
383
|
+
dimmed?: boolean;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export interface SubsystemGroupNodeData extends Record<string, unknown> {
|
|
387
|
+
region: SubsystemProcessRegion;
|
|
388
|
+
/** True while the region's members are dimmed by flow focus. */
|
|
389
|
+
dimmed?: boolean;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export type SubsystemGraphNode =
|
|
393
|
+
| Node<SubsystemGraphNodeData, 'subsystem-component'>
|
|
394
|
+
| Node<SubsystemGroupNodeData, 'subsystem-group'>;
|
|
395
|
+
|
|
396
|
+
export interface SubsystemGraphEdgeData extends Record<string, unknown> {
|
|
397
|
+
mechanism: SubsystemEdgeMechanism;
|
|
398
|
+
refs?: string[];
|
|
399
|
+
/** True while another edge is selected — render this edge (and its label)
|
|
400
|
+
* dimmed to focus the selected relationship. */
|
|
401
|
+
dimmed?: boolean;
|
|
402
|
+
/** ELK-computed label midpoint (from the actual edge path, not node centers). */
|
|
403
|
+
labelX?: number;
|
|
404
|
+
labelY?: number;
|
|
405
|
+
/** Polyline length in flow-space units (for capping screen-space label size). */
|
|
406
|
+
pathLength?: number;
|
|
407
|
+
/** ELK-computed SVG edge path (overrides React Flow's default path). */
|
|
408
|
+
elkPath?: string;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export type SubsystemGraphEdge = Edge<SubsystemGraphEdgeData>;
|
|
412
|
+
|
|
413
|
+
export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
414
|
+
imports: '#0893d2', // blue
|
|
415
|
+
imports_from: '#5aa9e6', // light blue
|
|
416
|
+
re_exports: '#3aa5c9', // cyan-blue
|
|
417
|
+
defines: '#2e86ab', // steel blue
|
|
418
|
+
calls: '#4ec9b0', // teal
|
|
419
|
+
extends: '#b48ead', // purple
|
|
420
|
+
inherits: '#9b6fd0', // purple
|
|
421
|
+
implements: '#c586c0', // magenta
|
|
422
|
+
mixes_in: '#d474a8', // pink-magenta
|
|
423
|
+
uses: '#e3b341', // gold
|
|
424
|
+
method: '#c586c0', // magenta
|
|
425
|
+
references: '#e07a5f', // terracotta
|
|
426
|
+
contains: '#6c5ce7', // indigo
|
|
427
|
+
feeds: '#22c55e', // green — data-flow into a processor
|
|
428
|
+
produces: '#e07a5f', // terracotta — emits an output type
|
|
429
|
+
writes: '#2f9e44', // deep green — mutates retained state
|
|
430
|
+
reads: '#0ea5e9', // sky — pulls from retained state
|
|
431
|
+
watches: '#9ca3af', // gray — observes, owns nothing
|
|
432
|
+
'registers-into': '#ff6b35', // orange
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed' | 'dotted'> = {
|
|
436
|
+
imports: 'solid',
|
|
437
|
+
imports_from: 'solid',
|
|
438
|
+
re_exports: 'solid',
|
|
439
|
+
defines: 'solid',
|
|
440
|
+
calls: 'solid',
|
|
441
|
+
extends: 'dashed',
|
|
442
|
+
inherits: 'dashed',
|
|
443
|
+
implements: 'dashed',
|
|
444
|
+
mixes_in: 'dashed',
|
|
445
|
+
uses: 'solid',
|
|
446
|
+
method: 'solid',
|
|
447
|
+
references: 'dotted',
|
|
448
|
+
contains: 'solid',
|
|
449
|
+
feeds: 'solid',
|
|
450
|
+
produces: 'solid',
|
|
451
|
+
writes: 'solid',
|
|
452
|
+
reads: 'solid',
|
|
453
|
+
watches: 'dashed',
|
|
454
|
+
'registers-into': 'dashed',
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/** Mechanism → [description, verifiable-with-graphify]. Drives the "not
|
|
458
|
+
* directly verifiable" styling of edge labels. */
|
|
459
|
+
export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][] = [
|
|
460
|
+
['imports', 'import statement (code-level dependency)', true],
|
|
461
|
+
['imports_from', 'imported by (reverse dependency)', true],
|
|
462
|
+
['re_exports', 're-exports symbols from', true],
|
|
463
|
+
['defines', 'defines / declares symbol', true],
|
|
464
|
+
['calls', 'function/method call (call graph edge)', true],
|
|
465
|
+
['extends', 'class inheritance', true],
|
|
466
|
+
['inherits', 'class inheritance', true],
|
|
467
|
+
['implements', 'implements interface / protocol', true],
|
|
468
|
+
['mixes_in', 'applies mixin', true],
|
|
469
|
+
['uses', 'general dependency (import, call, or reference)', false],
|
|
470
|
+
['method', 'structural: has method / member', true],
|
|
471
|
+
['references', 'type / symbol reference (not a call)', true],
|
|
472
|
+
['contains', 'structural: contains / encapsulates', true],
|
|
473
|
+
['feeds', 'data flow: output feeds into input', false],
|
|
474
|
+
['produces', 'data flow: produces / outputs', false],
|
|
475
|
+
['writes', 'state access: mutates retained state', true],
|
|
476
|
+
['reads', 'state access: reads retained state', true],
|
|
477
|
+
['watches', 'observes retained state without owning it', false],
|
|
478
|
+
['registers-into', 'registration pattern', false],
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
/** Package color palette (derived deterministically from the package name). */
|
|
482
|
+
export function packageColor(name: string): string {
|
|
483
|
+
const palette = [
|
|
484
|
+
'#0893d2',
|
|
485
|
+
'#4ec9b0',
|
|
486
|
+
'#ff6b35',
|
|
487
|
+
'#b48ead',
|
|
488
|
+
'#e3b341',
|
|
489
|
+
'#5aa9e6',
|
|
490
|
+
];
|
|
491
|
+
let h = 0;
|
|
492
|
+
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
|
|
493
|
+
return palette[h % palette.length];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Color per semantic role — applied ONLY to the role badge (top-right tab on
|
|
497
|
+
* nodes that carry a role). The node border stays construct-colored. Note:
|
|
498
|
+
* `service` currently equals the old class hex; harmless while role badges
|
|
499
|
+
* are the only surface using it, but pick a distinct value if roles ever
|
|
500
|
+
* take over node borders. */
|
|
501
|
+
export const ROLE_COLOR: Record<SubsystemComponentRole, string> = {
|
|
502
|
+
entry: '#ff6b35', // orange — boundary element
|
|
503
|
+
service: '#0893d2', // blue — external system
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
507
|
+
entry: 'entry',
|
|
508
|
+
service: 'service',
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Primary badge text for a node: prefer framework stereotype over the
|
|
513
|
+
* language construct so a React UI unit reads as "component" / "hook"
|
|
514
|
+
* rather than "function". When both framework and stereotype are set,
|
|
515
|
+
* show `framework · stereotype` (e.g. `react · component`).
|
|
516
|
+
*/
|
|
517
|
+
export function constructBadgeLabel(component: {
|
|
518
|
+
construct: SubsystemComponentConstruct;
|
|
519
|
+
framework?: string;
|
|
520
|
+
stereotype?: string;
|
|
521
|
+
}): string {
|
|
522
|
+
const constructLabel =
|
|
523
|
+
component.construct === 'type_alias' ? 'type alias' : component.construct;
|
|
524
|
+
if (component.stereotype && component.framework) {
|
|
525
|
+
return `${component.framework} · ${component.stereotype}`;
|
|
526
|
+
}
|
|
527
|
+
if (component.stereotype) return component.stereotype;
|
|
528
|
+
return constructLabel ?? '';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Default CSS floor for component nodes (padding aside). */
|
|
532
|
+
export const NODE_CSS_MIN_WIDTH = 150;
|
|
533
|
+
/** Inset of each top badge from the node edge (`left` / `right` style). */
|
|
534
|
+
export const BADGE_EDGE_INSET = 5;
|
|
535
|
+
/** Minimum gap between left construct badge and right role badge. */
|
|
536
|
+
const BADGE_PAIR_GAP = 8;
|
|
537
|
+
/** Badge box chrome: padding 5+5 + border 1+1. */
|
|
538
|
+
const BADGE_BOX_CHROME = 12;
|
|
539
|
+
/** Approx monospace uppercase width incl. letter-spacing (~0.5px). */
|
|
540
|
+
const BADGE_CHAR_WIDTH = 8;
|
|
541
|
+
|
|
542
|
+
/** Estimated rendered width of a top tab badge label. */
|
|
543
|
+
export function estimateBadgeLabelWidth(label: string): number {
|
|
544
|
+
return (label?.length ?? 0) * BADGE_CHAR_WIDTH + BADGE_BOX_CHROME;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Minimum node width so top badges stay on one line and (when both are
|
|
549
|
+
* present) don't overlap — badges are absolutely positioned, so they don't
|
|
550
|
+
* contribute to layout unless we widen the node explicitly.
|
|
551
|
+
*/
|
|
552
|
+
export function nodeMinWidthForBadges(component: {
|
|
553
|
+
construct: SubsystemComponentConstruct;
|
|
554
|
+
framework?: string;
|
|
555
|
+
stereotype?: string;
|
|
556
|
+
role?: SubsystemComponentRole;
|
|
557
|
+
}): number {
|
|
558
|
+
const left = estimateBadgeLabelWidth(constructBadgeLabel(component));
|
|
559
|
+
if (component.role == null) {
|
|
560
|
+
return Math.max(NODE_CSS_MIN_WIDTH, BADGE_EDGE_INSET + left + BADGE_EDGE_INSET);
|
|
561
|
+
}
|
|
562
|
+
const right = estimateBadgeLabelWidth(ROLE_LABEL[component.role]);
|
|
563
|
+
return Math.max(
|
|
564
|
+
NODE_CSS_MIN_WIDTH,
|
|
565
|
+
BADGE_EDGE_INSET + left + BADGE_PAIR_GAP + right + BADGE_EDGE_INSET,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Convert a subsystem graph document into React Flow nodes. Components that
|
|
571
|
+
* carry a `process` get a `parentId` pointing at their boundary group node
|
|
572
|
+
* (`process:<process>`); nodes without one stay top-level (outside every
|
|
573
|
+
* boundary). The initial grid groups by `process ?? purl` so the pre-ELK
|
|
574
|
+
* positions are already clustered; ELK then refines with compound layout.
|
|
575
|
+
*/
|
|
576
|
+
export function convertSubsystemToNodes(
|
|
577
|
+
doc: SubsystemModelDocument,
|
|
578
|
+
opts: { maxNodeWidth?: number } = {},
|
|
579
|
+
): SubsystemGraphNode[] {
|
|
580
|
+
const { maxNodeWidth } = opts;
|
|
581
|
+
// Group components into boundary regions by process when authored, else by
|
|
582
|
+
// package. Process is runtime membership (drawn as one boundary region);
|
|
583
|
+
// purl is code identity — nodes without a process (external actors,
|
|
584
|
+
// services, libraries) fall back to purl and sit outside every boundary.
|
|
585
|
+
const byPkg = new Map<string, SubsystemComponent[]>();
|
|
586
|
+
for (const c of doc.components) {
|
|
587
|
+
const regionKey = c.process ?? c.purl;
|
|
588
|
+
const list = byPkg.get(regionKey) ?? [];
|
|
589
|
+
list.push(c);
|
|
590
|
+
byPkg.set(regionKey, list);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const nodes: SubsystemGraphNode[] = [];
|
|
594
|
+
const COLS = 2;
|
|
595
|
+
const COL_W = 240;
|
|
596
|
+
const ROW_H = 150;
|
|
597
|
+
const PAD = 30;
|
|
598
|
+
const GROUP_GAP = 60;
|
|
599
|
+
|
|
600
|
+
let cursorY = PAD;
|
|
601
|
+
for (const comps of byPkg.values()) {
|
|
602
|
+
const rows = Math.ceil(comps.length / COLS);
|
|
603
|
+
const heightPx = ROW_H * rows;
|
|
604
|
+
|
|
605
|
+
comps.forEach((c, i) => {
|
|
606
|
+
const col = i % COLS;
|
|
607
|
+
const row = Math.floor(i / COLS);
|
|
608
|
+
// Estimate rendered node width from the longest text line so ELK reserves
|
|
609
|
+
// the right space (names can wrap, so we cap at the configurable max).
|
|
610
|
+
const text = [c.symbol, c.name, c.file?.split('/').pop() ?? '']
|
|
611
|
+
.filter((t): t is string => !!t)
|
|
612
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
613
|
+
const cap = maxNodeWidth ?? 300;
|
|
614
|
+
const textWidth = Math.min(cap, Math.max(60, (text?.length ?? 10) * 8));
|
|
615
|
+
// Account for CSS minWidth (incl. top badges) and padding/border so ELK's
|
|
616
|
+
// port positions match the actual rendered node boundaries.
|
|
617
|
+
const cssMinWidth = nodeMinWidthForBadges(c);
|
|
618
|
+
const cssPadding = 20; // horizontal padding (left + right)
|
|
619
|
+
const cssBorder = 4; // 2px border each side
|
|
620
|
+
const rawWidth = Math.max(cssMinWidth, textWidth + cssPadding + cssBorder);
|
|
621
|
+
const nodeWidth = Math.max(cssMinWidth, Math.min(cap, rawWidth));
|
|
622
|
+
const processKey = c.process?.trim();
|
|
623
|
+
nodes.push({
|
|
624
|
+
id: c.id,
|
|
625
|
+
type: 'subsystem-component',
|
|
626
|
+
...(processKey ? { parentId: processGroupNodeId(processKey) } : {}),
|
|
627
|
+
position: { x: PAD + col * COL_W, y: cursorY + row * ROW_H },
|
|
628
|
+
width: nodeWidth,
|
|
629
|
+
height: 84,
|
|
630
|
+
data: { component: c },
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
cursorY += heightPx + PAD * 2 + GROUP_GAP;
|
|
634
|
+
}
|
|
635
|
+
return nodes;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Convert boundary regions into React Flow parent (group) nodes. One per
|
|
640
|
+
* distinct `process` value; member components point at these via `parentId`.
|
|
641
|
+
* Positions/sizes are placeholders — ELK compound layout overwrites them.
|
|
642
|
+
*/
|
|
643
|
+
export function convertSubsystemToGroups(
|
|
644
|
+
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
645
|
+
): SubsystemGraphNode[] {
|
|
646
|
+
return getSubsystemRegions(doc).map((region) => ({
|
|
647
|
+
id: processGroupNodeId(region.key),
|
|
648
|
+
type: 'subsystem-group',
|
|
649
|
+
position: { x: 0, y: 0 },
|
|
650
|
+
width: 400,
|
|
651
|
+
height: 300,
|
|
652
|
+
data: { region },
|
|
653
|
+
}));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Convert a subsystem graph document into React Flow edges. Edges whose target
|
|
658
|
+
* is an external label (not a component id) point at a synthetic stub so the
|
|
659
|
+
* relationship is visible without a member node.
|
|
660
|
+
*/
|
|
661
|
+
export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemGraphEdge[] {
|
|
662
|
+
const compIds = new Set(doc.components.map((c) => c.id));
|
|
663
|
+
const edges: SubsystemGraphEdge[] = [];
|
|
664
|
+
|
|
665
|
+
for (const e of doc.edges) {
|
|
666
|
+
const color = MECHANISM_COLOR[e.mechanism];
|
|
667
|
+
const style = MECHANISM_STYLE[e.mechanism];
|
|
668
|
+
// If `to` is a real component, connect directly; otherwise point at a stub node.
|
|
669
|
+
const isExternal = !compIds.has(e.to);
|
|
670
|
+
const targetId = isExternal ? `external:${e.to}` : e.to;
|
|
671
|
+
|
|
672
|
+
edges.push({
|
|
673
|
+
id: e.id,
|
|
674
|
+
source: e.from,
|
|
675
|
+
target: targetId,
|
|
676
|
+
data: { mechanism: e.mechanism, refs: e.refs },
|
|
677
|
+
type: 'subsystem-edge',
|
|
678
|
+
markerEnd: { type: MarkerType.ArrowClosed, color, width: 32, height: 32 },
|
|
679
|
+
style: { color, stroke: color, strokeDasharray: style === 'dashed' ? '6 4' : undefined },
|
|
680
|
+
// `label` feeds ELK's label-space reservation only; the visible label is
|
|
681
|
+
// rendered by the custom SubsystemEdge as an HTML overlay.
|
|
682
|
+
label: e.mechanism,
|
|
683
|
+
// Mechanism label rendered via the custom SubsystemEdge (an HTML overlay
|
|
684
|
+
// above the SVG edges, so it can't be hidden behind other edge lines).
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return edges;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
|
|
691
|
+
export function subsystemGraphLayoutKey(
|
|
692
|
+
doc: Pick<SubsystemModelDocument, 'components' | 'edges'>,
|
|
693
|
+
): string {
|
|
694
|
+
const components = doc.components
|
|
695
|
+
.map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
|
|
696
|
+
[id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
|
|
697
|
+
.sort()
|
|
698
|
+
.join('\n');
|
|
699
|
+
const edgeKey = doc.edges
|
|
700
|
+
.map(({ id, from, to, mechanism }) => [id, from, to, mechanism].join('\0'))
|
|
701
|
+
.sort()
|
|
702
|
+
.join('\n');
|
|
703
|
+
return `${components}|${edgeKey}`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Build the full React Flow graph (nodes + edges) using **ELK auto-layout** to
|
|
708
|
+
* position every node (including external stub targets), so the graph is
|
|
709
|
+
* layered with minimized crossings.
|
|
710
|
+
*/
|
|
711
|
+
export async function buildSubsystemGraph(
|
|
712
|
+
doc: SubsystemModelDocument,
|
|
713
|
+
opts: { maxNodeWidth?: number; showEdgeLabels?: boolean; measuredWidths?: Map<string, number>; measuredHeights?: Map<string, number> } = {},
|
|
714
|
+
): Promise<{
|
|
715
|
+
nodes: SubsystemGraphNode[];
|
|
716
|
+
edges: SubsystemGraphEdge[];
|
|
717
|
+
regions: SubsystemProcessRegion[];
|
|
718
|
+
}> {
|
|
719
|
+
const { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights } = opts;
|
|
720
|
+
const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
|
|
721
|
+
const edges = convertSubsystemToEdges(doc);
|
|
722
|
+
// Boundary regions: one per multi-member process. Singletons get no frame —
|
|
723
|
+
// strip the parentId convertSubsystemToNodes stamped so React Flow never
|
|
724
|
+
// points at a non-existent parent.
|
|
725
|
+
const regions = getSubsystemRegions(doc).filter((r) => r.memberIds.length >= 2);
|
|
726
|
+
const regionKeys = new Set(regions.map((r) => r.key));
|
|
727
|
+
for (const n of nodes) {
|
|
728
|
+
if (n.type !== 'subsystem-component') continue;
|
|
729
|
+
const proc = (n.data as SubsystemGraphNodeData).component?.process?.trim();
|
|
730
|
+
if (proc && !regionKeys.has(proc)) {
|
|
731
|
+
delete (n as { parentId?: string }).parentId;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// External edge targets that aren't real components → create stub nodes so
|
|
736
|
+
// cross-package edges have something to land on.
|
|
737
|
+
const realIds = new Set(doc.components.map((c) => c.id));
|
|
738
|
+
const externalIds: string[] = [];
|
|
739
|
+
for (const e of doc.edges) {
|
|
740
|
+
if (!realIds.has(e.to)) {
|
|
741
|
+
const extId = `external:${e.to}`;
|
|
742
|
+
if (!externalIds.includes(extId)) externalIds.push(extId);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
for (const extId of externalIds) {
|
|
746
|
+
const label = extId.replace(/^external:/, '');
|
|
747
|
+
const extTextWidth = Math.min(300, Math.max(150, label.length * 8));
|
|
748
|
+
nodes.push({
|
|
749
|
+
id: extId,
|
|
750
|
+
type: 'subsystem-component',
|
|
751
|
+
position: { x: 0, y: 0 },
|
|
752
|
+
width: Math.max(150, extTextWidth + 24),
|
|
753
|
+
height: 60,
|
|
754
|
+
data: {
|
|
755
|
+
component: {
|
|
756
|
+
id: extId,
|
|
757
|
+
name: label,
|
|
758
|
+
construct: 'external',
|
|
759
|
+
purl: 'external',
|
|
760
|
+
file: '',
|
|
761
|
+
purpose: 'cross-package integration target (not a member node)',
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
draggable: false,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Apply measured dimensions (second pass) so ELK gets the real node sizes.
|
|
769
|
+
if (measuredWidths && measuredWidths.size > 0) {
|
|
770
|
+
for (const n of nodes) {
|
|
771
|
+
const mw = measuredWidths.get(n.id);
|
|
772
|
+
if (mw) n.width = mw;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (measuredHeights && measuredHeights.size > 0) {
|
|
776
|
+
for (const n of nodes) {
|
|
777
|
+
const mh = measuredHeights.get(n.id);
|
|
778
|
+
if (mh) n.height = mh;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ELK auto-layout: position nodes (layered, minimized crossings) with
|
|
783
|
+
// process partitions as compound parents so boundaries shape the layout.
|
|
784
|
+
let placedNodes = nodes;
|
|
785
|
+
let labelPositions = new Map<string, { x: number; y: number }>();
|
|
786
|
+
let elkPathStrings = new Map<string, string>();
|
|
787
|
+
let elkPathPoints = new Map<string, { x: number; y: number }[]>();
|
|
788
|
+
if (nodes.length > 0) {
|
|
789
|
+
try {
|
|
790
|
+
const result = await computeElkLayout(nodes, edges, {
|
|
791
|
+
routingStyle: 'orthogonal',
|
|
792
|
+
direction: 'RIGHT',
|
|
793
|
+
nodeSpacing: 60,
|
|
794
|
+
edgeSpacing: 30,
|
|
795
|
+
edgeNodeSpacing: 60,
|
|
796
|
+
interLayerSpacing: 120,
|
|
797
|
+
preserveNodePositions: false,
|
|
798
|
+
edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
|
|
799
|
+
groups: regions.map((r) => ({
|
|
800
|
+
id: processGroupNodeId(r.key),
|
|
801
|
+
memberIds: r.memberIds,
|
|
802
|
+
})),
|
|
803
|
+
});
|
|
804
|
+
const groupNodes: SubsystemGraphNode[] = regions.flatMap((region) => {
|
|
805
|
+
const bounds = result.groupBounds.get(processGroupNodeId(region.key));
|
|
806
|
+
if (!bounds) return [];
|
|
807
|
+
const group: SubsystemGraphNode = {
|
|
808
|
+
id: processGroupNodeId(region.key),
|
|
809
|
+
type: 'subsystem-group',
|
|
810
|
+
position: { x: bounds.x, y: bounds.y },
|
|
811
|
+
width: Math.max(200, bounds.width),
|
|
812
|
+
height: Math.max(160, bounds.height),
|
|
813
|
+
data: { region },
|
|
814
|
+
};
|
|
815
|
+
return [group];
|
|
816
|
+
});
|
|
817
|
+
// Parents first — React Flow resolves children via parentId.
|
|
818
|
+
placedNodes = [...groupNodes, ...(result.nodes as SubsystemGraphNode[])];
|
|
819
|
+
labelPositions = result.edgeLabelPositions;
|
|
820
|
+
elkPathStrings = result.edgePaths;
|
|
821
|
+
elkPathPoints = result.edgePathPoints;
|
|
822
|
+
} catch (err) {
|
|
823
|
+
// Fall back to the (unpositioned) grid if ELK is unavailable.
|
|
824
|
+
console.warn('[subsystem-graph] ELK layout failed, using manual positions:', err);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Stamp ELK-computed label midpoints onto edge data so the overlay can
|
|
829
|
+
// position labels at the actual path midpoint (not the node-center midpoint).
|
|
830
|
+
for (const e of edges) {
|
|
831
|
+
const pos = labelPositions.get(e.id);
|
|
832
|
+
if (pos) {
|
|
833
|
+
const d = (e as SubsystemGraphEdge).data as SubsystemGraphEdgeData;
|
|
834
|
+
d.labelX = pos.x;
|
|
835
|
+
d.labelY = pos.y;
|
|
836
|
+
}
|
|
837
|
+
const elkP = elkPathStrings.get(e.id);
|
|
838
|
+
if (elkP) {
|
|
839
|
+
const d = (e as SubsystemGraphEdge).data as SubsystemGraphEdgeData;
|
|
840
|
+
d.elkPath = elkP;
|
|
841
|
+
}
|
|
842
|
+
const pts = elkPathPoints.get(e.id);
|
|
843
|
+
if (pts && pts.length > 1) {
|
|
844
|
+
const d = (e as SubsystemGraphEdge).data as SubsystemGraphEdgeData;
|
|
845
|
+
d.pathLength = calculatePathLength(pts);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return { nodes: placedNodes, edges, regions };
|
|
850
|
+
}
|