@principal-ai/subsystems-react 0.21.0 → 0.22.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 (267) hide show
  1. package/dist/graphify/consolidated.d.ts +5 -24
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/index.d.ts +1 -1
  4. package/dist/graphify/index.d.ts.map +1 -1
  5. package/dist/graphify/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
  10. package/dist/pierre/PierreSnippetView.js +4 -19
  11. package/dist/pierre/PierreSnippetView.js.map +1 -1
  12. package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
  13. package/dist/pierre/PierreWalkthroughCodeView.js +20 -3
  14. package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
  15. package/dist/pierre/constructColors.d.ts.map +1 -1
  16. package/dist/pierre/constructColors.js +0 -5
  17. package/dist/pierre/constructColors.js.map +1 -1
  18. package/dist/pierre/index.d.ts +1 -1
  19. package/dist/pierre/index.d.ts.map +1 -1
  20. package/dist/pierre/index.js +1 -1
  21. package/dist/pierre/index.js.map +1 -1
  22. package/dist/pierre/sliceSnippet.d.ts +6 -0
  23. package/dist/pierre/sliceSnippet.d.ts.map +1 -1
  24. package/dist/pierre/sliceSnippet.js +25 -0
  25. package/dist/pierre/sliceSnippet.js.map +1 -1
  26. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  27. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -3
  28. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  29. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  30. package/dist/subsystem/formatDeclaration.js +0 -15
  31. package/dist/subsystem/formatDeclaration.js.map +1 -1
  32. package/dist/subsystem/model.d.ts +61 -22
  33. package/dist/subsystem/model.d.ts.map +1 -1
  34. package/dist/subsystem/model.js +186 -63
  35. package/dist/subsystem/model.js.map +1 -1
  36. package/dist/subsystem/nodes.d.ts +3 -4
  37. package/dist/subsystem/nodes.d.ts.map +1 -1
  38. package/dist/subsystem/nodes.js +5 -6
  39. package/dist/subsystem/nodes.js.map +1 -1
  40. package/dist/utils/elkLayout.d.ts +6 -4
  41. package/dist/utils/elkLayout.d.ts.map +1 -1
  42. package/dist/utils/elkLayout.js +144 -54
  43. package/dist/utils/elkLayout.js.map +1 -1
  44. package/package.json +3 -3
  45. package/src/graphify/consolidated.ts +4 -26
  46. package/src/graphify/index.ts +0 -2
  47. package/src/index.ts +0 -2
  48. package/src/pierre/PierreSnippetView.tsx +4 -18
  49. package/src/pierre/PierreWalkthroughCodeView.tsx +39 -3
  50. package/src/pierre/constructColors.test.ts +1 -2
  51. package/src/pierre/constructColors.ts +0 -5
  52. package/src/pierre/index.ts +1 -1
  53. package/src/pierre/sliceSnippet.test.ts +44 -1
  54. package/src/pierre/sliceSnippet.ts +28 -0
  55. package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +0 -31
  56. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -46
  57. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -24
  58. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +2 -2
  59. package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +287 -0
  60. package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +1 -1
  61. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
  62. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -3
  63. package/src/subsystem/formatDeclaration.ts +0 -19
  64. package/src/subsystem/model.test.ts +190 -9
  65. package/src/subsystem/model.ts +219 -73
  66. package/src/subsystem/nodes.tsx +6 -6
  67. package/src/utils/elkLayout.ts +148 -58
  68. package/dist/components/ConfigurationSelector.d.ts +0 -35
  69. package/dist/components/ConfigurationSelector.d.ts.map +0 -1
  70. package/dist/components/ConfigurationSelector.js +0 -63
  71. package/dist/components/ConfigurationSelector.js.map +0 -1
  72. package/dist/components/GraphRenderer.d.ts +0 -295
  73. package/dist/components/GraphRenderer.d.ts.map +0 -1
  74. package/dist/components/GraphRenderer.js +0 -2008
  75. package/dist/components/GraphRenderer.js.map +0 -1
  76. package/dist/components/MultiCanvasRenderer.d.ts +0 -75
  77. package/dist/components/MultiCanvasRenderer.d.ts.map +0 -1
  78. package/dist/components/MultiCanvasRenderer.js +0 -203
  79. package/dist/components/MultiCanvasRenderer.js.map +0 -1
  80. package/dist/components/NodeTooltip.d.ts +0 -24
  81. package/dist/components/NodeTooltip.d.ts.map +0 -1
  82. package/dist/components/NodeTooltip.js +0 -160
  83. package/dist/components/NodeTooltip.js.map +0 -1
  84. package/dist/components/SelectionSidebar.d.ts +0 -18
  85. package/dist/components/SelectionSidebar.d.ts.map +0 -1
  86. package/dist/components/SelectionSidebar.js +0 -190
  87. package/dist/components/SelectionSidebar.js.map +0 -1
  88. package/dist/components/SequenceDiagramRenderer.d.ts +0 -77
  89. package/dist/components/SequenceDiagramRenderer.d.ts.map +0 -1
  90. package/dist/components/SequenceDiagramRenderer.js +0 -604
  91. package/dist/components/SequenceDiagramRenderer.js.map +0 -1
  92. package/dist/components/WorkflowSequenceDiagram.d.ts +0 -63
  93. package/dist/components/WorkflowSequenceDiagram.d.ts.map +0 -1
  94. package/dist/components/WorkflowSequenceDiagram.js +0 -147
  95. package/dist/components/WorkflowSequenceDiagram.js.map +0 -1
  96. package/dist/components/dashboard/DashboardRenderer.d.ts +0 -10
  97. package/dist/components/dashboard/DashboardRenderer.d.ts.map +0 -1
  98. package/dist/components/dashboard/DashboardRenderer.js +0 -179
  99. package/dist/components/dashboard/DashboardRenderer.js.map +0 -1
  100. package/dist/components/dashboard/MetricPanel.d.ts +0 -10
  101. package/dist/components/dashboard/MetricPanel.d.ts.map +0 -1
  102. package/dist/components/dashboard/MetricPanel.js +0 -103
  103. package/dist/components/dashboard/MetricPanel.js.map +0 -1
  104. package/dist/components/dashboard/MockDataProvider.d.ts +0 -30
  105. package/dist/components/dashboard/MockDataProvider.d.ts.map +0 -1
  106. package/dist/components/dashboard/MockDataProvider.js +0 -270
  107. package/dist/components/dashboard/MockDataProvider.js.map +0 -1
  108. package/dist/components/dashboard/components/BarChart.d.ts +0 -10
  109. package/dist/components/dashboard/components/BarChart.d.ts.map +0 -1
  110. package/dist/components/dashboard/components/BarChart.js +0 -167
  111. package/dist/components/dashboard/components/BarChart.js.map +0 -1
  112. package/dist/components/dashboard/components/LineChart.d.ts +0 -10
  113. package/dist/components/dashboard/components/LineChart.d.ts.map +0 -1
  114. package/dist/components/dashboard/components/LineChart.js +0 -141
  115. package/dist/components/dashboard/components/LineChart.js.map +0 -1
  116. package/dist/components/dashboard/components/MetricCard.d.ts +0 -9
  117. package/dist/components/dashboard/components/MetricCard.d.ts.map +0 -1
  118. package/dist/components/dashboard/components/MetricCard.js +0 -163
  119. package/dist/components/dashboard/components/MetricCard.js.map +0 -1
  120. package/dist/components/dashboard/components/SourceLink.d.ts +0 -9
  121. package/dist/components/dashboard/components/SourceLink.d.ts.map +0 -1
  122. package/dist/components/dashboard/components/SourceLink.js +0 -39
  123. package/dist/components/dashboard/components/SourceLink.js.map +0 -1
  124. package/dist/components/dashboard/components/TimeRangeSelector.d.ts +0 -9
  125. package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +0 -1
  126. package/dist/components/dashboard/components/TimeRangeSelector.js +0 -167
  127. package/dist/components/dashboard/components/TimeRangeSelector.js.map +0 -1
  128. package/dist/components/dashboard/components/index.d.ts +0 -6
  129. package/dist/components/dashboard/components/index.d.ts.map +0 -1
  130. package/dist/components/dashboard/components/index.js +0 -6
  131. package/dist/components/dashboard/components/index.js.map +0 -1
  132. package/dist/components/dashboard/index.d.ts +0 -6
  133. package/dist/components/dashboard/index.d.ts.map +0 -1
  134. package/dist/components/dashboard/index.js +0 -8
  135. package/dist/components/dashboard/index.js.map +0 -1
  136. package/dist/components/dashboard/types.d.ts +0 -74
  137. package/dist/components/dashboard/types.d.ts.map +0 -1
  138. package/dist/components/dashboard/types.js +0 -8
  139. package/dist/components/dashboard/types.js.map +0 -1
  140. package/dist/components/state-view/PipelineView.d.ts +0 -14
  141. package/dist/components/state-view/PipelineView.d.ts.map +0 -1
  142. package/dist/components/state-view/PipelineView.js +0 -195
  143. package/dist/components/state-view/PipelineView.js.map +0 -1
  144. package/dist/components/state-view/index.d.ts +0 -14
  145. package/dist/components/state-view/index.d.ts.map +0 -1
  146. package/dist/components/state-view/index.js +0 -12
  147. package/dist/components/state-view/index.js.map +0 -1
  148. package/dist/components/state-view/types.d.ts +0 -188
  149. package/dist/components/state-view/types.d.ts.map +0 -1
  150. package/dist/components/state-view/types.js +0 -10
  151. package/dist/components/state-view/types.js.map +0 -1
  152. package/dist/components/state-view/useStateView.d.ts +0 -32
  153. package/dist/components/state-view/useStateView.d.ts.map +0 -1
  154. package/dist/components/state-view/useStateView.js +0 -129
  155. package/dist/components/state-view/useStateView.js.map +0 -1
  156. package/dist/contexts/GraphEditContext.d.ts +0 -20
  157. package/dist/contexts/GraphEditContext.d.ts.map +0 -1
  158. package/dist/contexts/GraphEditContext.js +0 -8
  159. package/dist/contexts/GraphEditContext.js.map +0 -1
  160. package/dist/contexts/TooltipPortalContext.d.ts +0 -8
  161. package/dist/contexts/TooltipPortalContext.d.ts.map +0 -1
  162. package/dist/contexts/TooltipPortalContext.js +0 -8
  163. package/dist/contexts/TooltipPortalContext.js.map +0 -1
  164. package/dist/edges/CustomEdge.d.ts +0 -24
  165. package/dist/edges/CustomEdge.d.ts.map +0 -1
  166. package/dist/edges/CustomEdge.js +0 -222
  167. package/dist/edges/CustomEdge.js.map +0 -1
  168. package/dist/edges/GenericEdge.d.ts +0 -18
  169. package/dist/edges/GenericEdge.d.ts.map +0 -1
  170. package/dist/edges/GenericEdge.js +0 -10
  171. package/dist/edges/GenericEdge.js.map +0 -1
  172. package/dist/graphify/kind.d.ts +0 -23
  173. package/dist/graphify/kind.d.ts.map +0 -1
  174. package/dist/graphify/kind.js +0 -77
  175. package/dist/graphify/kind.js.map +0 -1
  176. package/dist/hooks/usePathBasedEvents.d.ts +0 -42
  177. package/dist/hooks/usePathBasedEvents.d.ts.map +0 -1
  178. package/dist/hooks/usePathBasedEvents.js +0 -119
  179. package/dist/hooks/usePathBasedEvents.js.map +0 -1
  180. package/dist/hooks/useSequenceLayout.d.ts +0 -186
  181. package/dist/hooks/useSequenceLayout.d.ts.map +0 -1
  182. package/dist/hooks/useSequenceLayout.js +0 -327
  183. package/dist/hooks/useSequenceLayout.js.map +0 -1
  184. package/dist/hooks/useUndoRedo.d.ts +0 -80
  185. package/dist/hooks/useUndoRedo.d.ts.map +0 -1
  186. package/dist/hooks/useUndoRedo.js +0 -70
  187. package/dist/hooks/useUndoRedo.js.map +0 -1
  188. package/dist/nodes/CustomNode.d.ts +0 -28
  189. package/dist/nodes/CustomNode.d.ts.map +0 -1
  190. package/dist/nodes/CustomNode.js +0 -820
  191. package/dist/nodes/CustomNode.js.map +0 -1
  192. package/dist/nodes/GenericNode.d.ts +0 -20
  193. package/dist/nodes/GenericNode.d.ts.map +0 -1
  194. package/dist/nodes/GenericNode.js +0 -20
  195. package/dist/nodes/GenericNode.js.map +0 -1
  196. package/dist/nodes/otel/OtelBoundaryNode.d.ts +0 -55
  197. package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +0 -1
  198. package/dist/nodes/otel/OtelBoundaryNode.js +0 -81
  199. package/dist/nodes/otel/OtelBoundaryNode.js.map +0 -1
  200. package/dist/nodes/otel/OtelEventNode.d.ts +0 -59
  201. package/dist/nodes/otel/OtelEventNode.d.ts.map +0 -1
  202. package/dist/nodes/otel/OtelEventNode.js +0 -82
  203. package/dist/nodes/otel/OtelEventNode.js.map +0 -1
  204. package/dist/nodes/otel/OtelResourceNode.d.ts +0 -53
  205. package/dist/nodes/otel/OtelResourceNode.d.ts.map +0 -1
  206. package/dist/nodes/otel/OtelResourceNode.js +0 -105
  207. package/dist/nodes/otel/OtelResourceNode.js.map +0 -1
  208. package/dist/nodes/otel/OtelScopeNode.d.ts +0 -53
  209. package/dist/nodes/otel/OtelScopeNode.d.ts.map +0 -1
  210. package/dist/nodes/otel/OtelScopeNode.js +0 -81
  211. package/dist/nodes/otel/OtelScopeNode.js.map +0 -1
  212. package/dist/nodes/otel/OtelSpanConventionNode.d.ts +0 -61
  213. package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +0 -1
  214. package/dist/nodes/otel/OtelSpanConventionNode.js +0 -131
  215. package/dist/nodes/otel/OtelSpanConventionNode.js.map +0 -1
  216. package/dist/nodes/otel/index.d.ts +0 -17
  217. package/dist/nodes/otel/index.d.ts.map +0 -1
  218. package/dist/nodes/otel/index.js +0 -13
  219. package/dist/nodes/otel/index.js.map +0 -1
  220. package/dist/nodes/otel/shared/NodeBadges.d.ts +0 -10
  221. package/dist/nodes/otel/shared/NodeBadges.d.ts.map +0 -1
  222. package/dist/nodes/otel/shared/NodeBadges.js +0 -178
  223. package/dist/nodes/otel/shared/NodeBadges.js.map +0 -1
  224. package/dist/nodes/otel/shared/NodeContent.d.ts +0 -10
  225. package/dist/nodes/otel/shared/NodeContent.d.ts.map +0 -1
  226. package/dist/nodes/otel/shared/NodeContent.js +0 -96
  227. package/dist/nodes/otel/shared/NodeContent.js.map +0 -1
  228. package/dist/nodes/otel/shared/index.d.ts +0 -8
  229. package/dist/nodes/otel/shared/index.d.ts.map +0 -1
  230. package/dist/nodes/otel/shared/index.js +0 -8
  231. package/dist/nodes/otel/shared/index.js.map +0 -1
  232. package/dist/nodes/otel/shared/types.d.ts +0 -129
  233. package/dist/nodes/otel/shared/types.d.ts.map +0 -1
  234. package/dist/nodes/otel/shared/types.js +0 -5
  235. package/dist/nodes/otel/shared/types.js.map +0 -1
  236. package/dist/nodes/otel/shared/useNodeBehavior.d.ts +0 -42
  237. package/dist/nodes/otel/shared/useNodeBehavior.d.ts.map +0 -1
  238. package/dist/nodes/otel/shared/useNodeBehavior.js +0 -61
  239. package/dist/nodes/otel/shared/useNodeBehavior.js.map +0 -1
  240. package/dist/pierre/PierreThroughlineCodeView.d.ts +0 -22
  241. package/dist/pierre/PierreThroughlineCodeView.d.ts.map +0 -1
  242. package/dist/pierre/PierreThroughlineCodeView.js +0 -206
  243. package/dist/pierre/PierreThroughlineCodeView.js.map +0 -1
  244. package/dist/subsystem/SubsystemCarousel.d.ts +0 -36
  245. package/dist/subsystem/SubsystemCarousel.d.ts.map +0 -1
  246. package/dist/subsystem/SubsystemCarousel.js +0 -163
  247. package/dist/subsystem/SubsystemCarousel.js.map +0 -1
  248. package/dist/utils/animationMapping.d.ts +0 -53
  249. package/dist/utils/animationMapping.d.ts.map +0 -1
  250. package/dist/utils/animationMapping.js +0 -126
  251. package/dist/utils/animationMapping.js.map +0 -1
  252. package/dist/utils/canvasBounds.d.ts +0 -50
  253. package/dist/utils/canvasBounds.d.ts.map +0 -1
  254. package/dist/utils/canvasBounds.js +0 -88
  255. package/dist/utils/canvasBounds.js.map +0 -1
  256. package/dist/utils/graphConverter.d.ts +0 -18
  257. package/dist/utils/graphConverter.d.ts.map +0 -1
  258. package/dist/utils/graphConverter.js +0 -114
  259. package/dist/utils/graphConverter.js.map +0 -1
  260. package/dist/utils/iconResolver.d.ts +0 -29
  261. package/dist/utils/iconResolver.d.ts.map +0 -1
  262. package/dist/utils/iconResolver.js +0 -30
  263. package/dist/utils/iconResolver.js.map +0 -1
  264. package/dist/utils/orientationUtils.d.ts +0 -19
  265. package/dist/utils/orientationUtils.d.ts.map +0 -1
  266. package/dist/utils/orientationUtils.js +0 -62
  267. package/dist/utils/orientationUtils.js.map +0 -1
@@ -32,7 +32,6 @@ export type SubsystemComponentConstruct =
32
32
  | 'interface'
33
33
  | 'type_alias'
34
34
  | 'enum'
35
- | 'module'
36
35
  | 'store'
37
36
  | 'external'
38
37
  | 'custom_entity';
@@ -92,9 +91,6 @@ export interface SubsystemDeclToken {
92
91
  */
93
92
  export type SubsystemRelationType =
94
93
  | 'imports'
95
- | 'imports_from'
96
- | 're_exports'
97
- | 'defines'
98
94
  | 'extends'
99
95
  | 'inherits'
100
96
  | 'implements'
@@ -190,11 +186,21 @@ export interface SubsystemComponent {
190
186
  /**
191
187
  * Runtime process membership — which deployment unit this node is a
192
188
  * member of (e.g. `principal-studio/host`, `principal-studio/renderer`). Nodes
193
- * sharing a `process` are drawn inside one boundary region (grouping is
194
- * `process ?? purl`); nodes without one sit outside every boundary
195
- * (external actors, services, libraries).
189
+ * sharing a `process` are drawn inside one boundary region; nodes without
190
+ * one sit outside every process boundary (external actors, services,
191
+ * libraries). Orthogonal to `module` (source-file frame).
196
192
  */
197
193
  process?: string;
194
+ /**
195
+ * Source-module membership — which file/module this export belongs to
196
+ * (e.g. `src/session/transcript.ts`). Nodes sharing a `module` are drawn
197
+ * inside one boundary frame. Prefer this over inventing a module construct:
198
+ * each export keeps its real construct (`function` / `class` / …) and the
199
+ * file reads as a frame. Orthogonal to `process` (runtime deployment).
200
+ * When both are set, the module frame is the component's parent (finer
201
+ * grain); process framing still groups siblings that share a process.
202
+ */
203
+ module?: string;
198
204
  /** A symbol this component exposes / is (the node's identity). */
199
205
  symbol?: string;
200
206
  /**
@@ -354,26 +360,20 @@ export function deriveGraphEdges(doc: {
354
360
  *
355
361
  * `symbol` is the source of truth (fully-qualified code identity). The name
356
362
  * is the symbol itself:
357
- * - class/type/module/function/script/... symbol → symbol (e.g. `SessionReader`)
358
- * - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
359
- * - module, no symbol → basename of `file` (e.g. `transcript.ts` → `transcript`) —
360
- * a common-sense convention for whole-file modules, not a real TS name
361
- * - otherwise no symbol → fall back to an existing name
363
+ * - class/type/function/... symbol → symbol (e.g. `SessionReader`)
364
+ * - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
365
+ * - otherwise no symbol → fall back to an existing name
362
366
  */
363
367
  export function deriveNameFromSymbol(
364
368
  symbol: string | undefined,
365
369
  construct: SubsystemComponentConstruct,
366
370
  existingName?: string,
367
- file?: string,
371
+ _file?: string,
368
372
  stereotype?: string,
369
373
  ): string {
370
374
  let name: string | undefined;
371
375
  if (symbol && symbol.trim()) {
372
376
  name = symbol;
373
- } else if (construct === 'module' && file) {
374
- const base = file.split('/').pop() ?? '';
375
- const clean = base.replace(/\.[^.]+$/, ''); // strip extension
376
- if (clean) name = clean;
377
377
  }
378
378
  if (!name) name = existingName ?? 'untitled';
379
379
 
@@ -382,7 +382,7 @@ export function deriveNameFromSymbol(
382
382
  // Executable constructs wear `()`; brace-bodied constructs (interface,
383
383
  // type_alias, enum) wear ` {}`. Classes render bare — the construct badge
384
384
  // already says "class".
385
- // Everything else (class, store, module, external) renders bare.
385
+ // Everything else (class, store, external) renders bare.
386
386
  if (stereotype === 'component' && !name.startsWith('<')) {
387
387
  return `<${name}>`;
388
388
  }
@@ -431,11 +431,13 @@ export function formatPurl(purl: string): string {
431
431
  export type SubsystemGraphNodeType = 'subsystem-component' | 'subsystem-group';
432
432
 
433
433
  /**
434
- * One process boundary region — all components sharing a `process` value.
435
- * Nodes without a `process` sit outside every boundary (no region).
434
+ * One boundary region — either a process (runtime deployment unit) or a
435
+ * module (source file). Nodes without that field sit outside those frames.
436
436
  */
437
437
  export interface SubsystemProcessRegion {
438
- /** The `process` value (e.g. `principal-studio/host`). */
438
+ /** Discriminator — which field produced this region. */
439
+ kind: 'process' | 'module';
440
+ /** The `process` or `module` value (e.g. `principal-studio/host`, `src/a.ts`). */
439
441
  key: string;
440
442
  /** Display label for the boundary frame. */
441
443
  label: string;
@@ -448,9 +450,21 @@ export function processGroupNodeId(processKey: string): string {
448
450
  return `process:${processKey}`;
449
451
  }
450
452
 
453
+ /** React Flow id for a module boundary group node. */
454
+ export function moduleGroupNodeId(moduleKey: string): string {
455
+ return `module:${moduleKey}`;
456
+ }
457
+
458
+ /** React Flow id for a boundary group of either kind. */
459
+ export function boundaryGroupNodeId(region: Pick<SubsystemProcessRegion, 'kind' | 'key'>): string {
460
+ return region.kind === 'module'
461
+ ? moduleGroupNodeId(region.key)
462
+ : processGroupNodeId(region.key);
463
+ }
464
+
451
465
  /**
452
- * Derive boundary regions from a document — one per distinct non-empty
453
- * `process` value, in first-appearance order.
466
+ * Derive process boundary regions — one per distinct non-empty `process`
467
+ * value, in first-appearance order.
454
468
  */
455
469
  export function getSubsystemRegions(
456
470
  doc: Pick<SubsystemModelDocument, 'components'>,
@@ -464,12 +478,100 @@ export function getSubsystemRegions(
464
478
  byProcess.set(p, list);
465
479
  }
466
480
  return [...byProcess.entries()].map(([key, memberIds]) => ({
481
+ kind: 'process' as const,
467
482
  key,
468
483
  label: key,
469
484
  memberIds,
470
485
  }));
471
486
  }
472
487
 
488
+ /**
489
+ * Derive module boundary regions — one per distinct non-empty `module`
490
+ * value, in first-appearance order. Label is the module path (file).
491
+ */
492
+ export function getSubsystemModuleRegions(
493
+ doc: Pick<SubsystemModelDocument, 'components'>,
494
+ ): SubsystemProcessRegion[] {
495
+ const byModule = new Map<string, string[]>();
496
+ for (const c of doc.components) {
497
+ const m = c.module?.trim();
498
+ if (!m) continue;
499
+ const list = byModule.get(m) ?? [];
500
+ list.push(c.id);
501
+ byModule.set(m, list);
502
+ }
503
+ return [...byModule.entries()].map(([key, memberIds]) => ({
504
+ kind: 'module' as const,
505
+ key,
506
+ label: key,
507
+ memberIds,
508
+ }));
509
+ }
510
+
511
+ /**
512
+ * One compound frame for ELK / React Flow — module frames may nest under a
513
+ * process frame via `parentId`.
514
+ */
515
+ export interface BoundaryLayoutGroup {
516
+ id: string;
517
+ memberIds: string[];
518
+ /** When set, this group is a child of another boundary group (process). */
519
+ parentId?: string;
520
+ region: SubsystemProcessRegion;
521
+ }
522
+
523
+ /**
524
+ * Build the process → module → leaf group tree for layout.
525
+ * Multi-member modules nest under a process when every member shares that
526
+ * process; process ELK children are nested module group ids plus any
527
+ * process members that are not inside a kept module frame.
528
+ */
529
+ export function buildBoundaryLayoutGroups(
530
+ doc: Pick<SubsystemModelDocument, 'components'>,
531
+ ): BoundaryLayoutGroup[] {
532
+ const byId = new Map(doc.components.map((c) => [c.id, c]));
533
+ const modules = getSubsystemModuleRegions(doc).filter((r) => r.memberIds.length >= 2);
534
+ const processes = getSubsystemRegions(doc).filter((r) => r.memberIds.length >= 2);
535
+ const keptProcessKeys = new Set(processes.map((r) => r.key));
536
+
537
+ const moduleGroups: BoundaryLayoutGroup[] = modules.map((r) => {
538
+ const processesOfMembers = new Set<string>();
539
+ for (const id of r.memberIds) {
540
+ const p = byId.get(id)?.process?.trim();
541
+ if (p) processesOfMembers.add(p);
542
+ }
543
+ let parentId: string | undefined;
544
+ if (processesOfMembers.size === 1) {
545
+ const p = [...processesOfMembers][0]!;
546
+ if (keptProcessKeys.has(p)) parentId = processGroupNodeId(p);
547
+ }
548
+ return {
549
+ id: moduleGroupNodeId(r.key),
550
+ memberIds: [...r.memberIds],
551
+ parentId,
552
+ region: r,
553
+ };
554
+ });
555
+
556
+ const processGroups: BoundaryLayoutGroup[] = processes.map((r) => {
557
+ const processId = processGroupNodeId(r.key);
558
+ const nestedModules = moduleGroups.filter((m) => m.parentId === processId);
559
+ const nestedModuleKeys = new Set(nestedModules.map((m) => m.region.key));
560
+ const directLeaves = r.memberIds.filter((id) => {
561
+ const mod = byId.get(id)?.module?.trim();
562
+ if (!mod) return true;
563
+ return !nestedModuleKeys.has(mod);
564
+ });
565
+ return {
566
+ id: processId,
567
+ memberIds: [...nestedModules.map((m) => m.id), ...directLeaves],
568
+ region: r,
569
+ };
570
+ });
571
+
572
+ return [...moduleGroups, ...processGroups];
573
+ }
574
+
473
575
  export interface SubsystemGraphNodeData extends Record<string, unknown> {
474
576
  component: SubsystemComponent;
475
577
  /** Set while a file is open in the drawer: true if this node's component
@@ -509,9 +611,6 @@ export type SubsystemGraphEdge = Edge<SubsystemGraphEdgeData>;
509
611
 
510
612
  export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
511
613
  imports: '#0893d2', // blue
512
- imports_from: '#5aa9e6', // light blue
513
- re_exports: '#3aa5c9', // cyan-blue
514
- defines: '#2e86ab', // steel blue
515
614
  calls: '#4ec9b0', // teal
516
615
  extends: '#b48ead', // purple
517
616
  inherits: '#9b6fd0', // purple
@@ -531,9 +630,6 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
531
630
 
532
631
  export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed' | 'dotted'> = {
533
632
  imports: 'solid',
534
- imports_from: 'solid',
535
- re_exports: 'solid',
536
- defines: 'solid',
537
633
  calls: 'solid',
538
634
  extends: 'dashed',
539
635
  inherits: 'dashed',
@@ -555,9 +651,6 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
555
651
  * directly verifiable" styling of edge labels. */
556
652
  export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][] = [
557
653
  ['imports', 'import statement (code-level dependency)', true],
558
- ['imports_from', 'imported by (reverse dependency)', true],
559
- ['re_exports', 're-exports symbols from', true],
560
- ['defines', 'defines / declares symbol', true],
561
654
  ['calls', 'function/method call (call graph edge)', true],
562
655
  ['extends', 'class inheritance', true],
563
656
  ['inherits', 'class inheritance', true],
@@ -566,7 +659,7 @@ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][]
566
659
  ['uses', 'general dependency (import, call, or reference)', false],
567
660
  ['method', 'structural: has method / member', true],
568
661
  ['references', 'type / symbol reference (not a call)', true],
569
- ['contains', 'structural: contains / encapsulates', true],
662
+ ['contains', 'UI composition: shell/view owns a child surface', false],
570
663
  ['feeds', 'data flow: output feeds into input', false],
571
664
  ['produces', 'data flow: produces / outputs', false],
572
665
  ['writes', 'state access: mutates retained state', true],
@@ -750,23 +843,23 @@ export function nodeMinWidthForBadges(component: {
750
843
 
751
844
  /**
752
845
  * Convert a subsystem graph document into React Flow nodes. Components that
753
- * carry a `process` get a `parentId` pointing at their boundary group node
754
- * (`process:<process>`); nodes without one stay top-level (outside every
755
- * boundary). The initial grid groups by `process ?? purl` so the pre-ELK
756
- * positions are already clustered; ELK then refines with compound layout.
846
+ * carry a `module` get a `parentId` pointing at their module frame
847
+ * (`module:<path>`); otherwise a `process` stamps `process:<process>`. Module
848
+ * frames may themselves nest under a process frame (stamped on the group node
849
+ * in `buildSubsystemGraph`). Nodes without either stay top-level. The initial
850
+ * grid clusters by `module ?? process ?? purl`; ELK then refines with compound
851
+ * layout.
757
852
  */
758
853
  export function convertSubsystemToNodes(
759
854
  doc: SubsystemModelDocument,
760
855
  opts: { maxNodeWidth?: number } = {},
761
856
  ): SubsystemGraphNode[] {
762
857
  const { maxNodeWidth } = opts;
763
- // Group components into boundary regions by process when authored, else by
764
- // package. Process is runtime membership (drawn as one boundary region);
765
- // purl is code identity — nodes without a process (external actors,
766
- // services, libraries) fall back to purl and sit outside every boundary.
858
+ // Cluster for the pre-ELK grid: module (source file) → process (runtime)
859
+ // → purl (package identity).
767
860
  const byPkg = new Map<string, SubsystemComponent[]>();
768
861
  for (const c of doc.components) {
769
- const regionKey = c.process ?? c.purl;
862
+ const regionKey = c.module ?? c.process ?? c.purl;
770
863
  const list = byPkg.get(regionKey) ?? [];
771
864
  list.push(c);
772
865
  byPkg.set(regionKey, list);
@@ -801,11 +894,17 @@ export function convertSubsystemToNodes(
801
894
  const cssBorder = 4; // 2px border each side
802
895
  const rawWidth = Math.max(cssMinWidth, textWidth + cssPadding + cssBorder);
803
896
  const nodeWidth = Math.max(cssMinWidth, Math.min(cap, rawWidth));
897
+ const moduleKey = c.module?.trim();
804
898
  const processKey = c.process?.trim();
899
+ const parentId = moduleKey
900
+ ? moduleGroupNodeId(moduleKey)
901
+ : processKey
902
+ ? processGroupNodeId(processKey)
903
+ : undefined;
805
904
  nodes.push({
806
905
  id: c.id,
807
906
  type: 'subsystem-component',
808
- ...(processKey ? { parentId: processGroupNodeId(processKey) } : {}),
907
+ ...(parentId ? { parentId } : {}),
809
908
  position: { x: PAD + col * COL_W, y: cursorY + row * ROW_H },
810
909
  width: nodeWidth,
811
910
  height: 84,
@@ -818,20 +917,21 @@ export function convertSubsystemToNodes(
818
917
  }
819
918
 
820
919
  /**
821
- * Convert boundary regions into React Flow parent (group) nodes. One per
822
- * distinct `process` value; member components point at these via `parentId`.
920
+ * Convert boundary regions into React Flow parent (group) nodes. Module
921
+ * frames that share a process nest under that process via `parentId`.
823
922
  * Positions/sizes are placeholders — ELK compound layout overwrites them.
824
923
  */
825
924
  export function convertSubsystemToGroups(
826
925
  doc: Pick<SubsystemModelDocument, 'components'>,
827
926
  ): SubsystemGraphNode[] {
828
- return getSubsystemRegions(doc).map((region) => ({
829
- id: processGroupNodeId(region.key),
830
- type: 'subsystem-group',
927
+ return buildBoundaryLayoutGroups(doc).map((g) => ({
928
+ id: g.id,
929
+ type: 'subsystem-group' as const,
831
930
  position: { x: 0, y: 0 },
832
931
  width: 400,
833
932
  height: 300,
834
- data: { region },
933
+ ...(g.parentId ? { parentId: g.parentId } : {}),
934
+ data: { region: g.region },
835
935
  }));
836
936
  }
837
937
 
@@ -874,8 +974,8 @@ export function subsystemGraphLayoutKey(
874
974
  doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'>,
875
975
  ): string {
876
976
  const components = doc.components
877
- .map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
878
- [id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
977
+ .map(({ id, purl, name, symbol, construct, file, purpose, process, module }) =>
978
+ [id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? '', module ?? ''].join('\0'))
879
979
  .sort()
880
980
  .join('\n');
881
981
  const edgeKey = deriveGraphEdges(doc)
@@ -901,15 +1001,25 @@ export async function buildSubsystemGraph(
901
1001
  const { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights } = opts;
902
1002
  const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
903
1003
  const edges = convertSubsystemToEdges(doc);
904
- // Boundary regions: one per multi-member process. Singletons get no frame —
905
- // strip the parentId convertSubsystemToNodes stamped so React Flow never
906
- // points at a non-existent parent.
907
- const regions = getSubsystemRegions(doc).filter((r) => r.memberIds.length >= 2);
908
- const regionKeys = new Set(regions.map((r) => r.key));
1004
+ // Nested boundary tree: process → module → leaves (when both fields set).
1005
+ const layoutGroups = buildBoundaryLayoutGroups(doc);
1006
+ const regions = layoutGroups.map((g) => g.region);
1007
+ const regionIds = new Set(layoutGroups.map((g) => g.id));
1008
+ const byId = new Map(doc.components.map((c) => [c.id, c]));
1009
+
1010
+ // Drop leaf parentIds that point at singleton / missing frames; fall back
1011
+ // from a dropped module frame to a kept process frame when possible.
909
1012
  for (const n of nodes) {
910
1013
  if (n.type !== 'subsystem-component') continue;
911
- const proc = (n.data as SubsystemGraphNodeData).component?.process?.trim();
912
- if (proc && !regionKeys.has(proc)) {
1014
+ const parentId = (n as { parentId?: string }).parentId;
1015
+ if (!parentId) continue;
1016
+ if (regionIds.has(parentId)) continue;
1017
+ const comp = (n.data as SubsystemGraphNodeData).component;
1018
+ const processKey = comp?.process?.trim() || byId.get(n.id)?.process?.trim();
1019
+ const processId = processKey ? processGroupNodeId(processKey) : undefined;
1020
+ if (processId && regionIds.has(processId)) {
1021
+ (n as { parentId?: string }).parentId = processId;
1022
+ } else {
913
1023
  delete (n as { parentId?: string }).parentId;
914
1024
  }
915
1025
  }
@@ -961,8 +1071,7 @@ export async function buildSubsystemGraph(
961
1071
  }
962
1072
  }
963
1073
 
964
- // ELK auto-layout: position nodes (layered, minimized crossings) with
965
- // process partitions as compound parents so boundaries shape the layout.
1074
+ // ELK auto-layout: nested compound parents (process → module → leaves).
966
1075
  let placedNodes = nodes;
967
1076
  let labelPositions = new Map<string, { x: number; y: number }>();
968
1077
  let elkPathStrings = new Map<string, string>();
@@ -978,32 +1087,69 @@ export async function buildSubsystemGraph(
978
1087
  interLayerSpacing: 120,
979
1088
  preserveNodePositions: false,
980
1089
  edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
981
- groups: regions.map((r) => ({
982
- id: processGroupNodeId(r.key),
983
- memberIds: r.memberIds,
1090
+ groups: layoutGroups.map((g) => ({
1091
+ id: g.id,
1092
+ memberIds: g.memberIds,
1093
+ parentId: g.parentId,
984
1094
  })),
985
1095
  });
986
- const groupNodes: SubsystemGraphNode[] = regions.flatMap((region) => {
987
- const bounds = result.groupBounds.get(processGroupNodeId(region.key));
988
- if (!bounds) return [];
1096
+ const builtGroupIds = new Set(result.groupBounds.keys());
1097
+ // Parents before children — process frames, then nested module frames.
1098
+ const processGroupNodes: SubsystemGraphNode[] = [];
1099
+ const moduleGroupNodes: SubsystemGraphNode[] = [];
1100
+ for (const g of layoutGroups) {
1101
+ const bounds = result.groupBounds.get(g.id);
1102
+ if (!bounds) continue;
1103
+ const parentId =
1104
+ g.parentId && builtGroupIds.has(g.parentId) ? g.parentId : undefined;
989
1105
  const group: SubsystemGraphNode = {
990
- id: processGroupNodeId(region.key),
1106
+ id: g.id,
991
1107
  type: 'subsystem-group',
992
1108
  position: { x: bounds.x, y: bounds.y },
993
1109
  width: Math.max(200, bounds.width),
994
1110
  height: Math.max(160, bounds.height),
995
- data: { region },
1111
+ ...(parentId ? { parentId } : {}),
1112
+ data: { region: g.region },
996
1113
  };
997
- return [group];
998
- });
999
- // Parents first — React Flow resolves children via parentId.
1000
- placedNodes = [...groupNodes, ...(result.nodes as SubsystemGraphNode[])];
1114
+ if (g.region.kind === 'process') processGroupNodes.push(group);
1115
+ else moduleGroupNodes.push(group);
1116
+ }
1117
+ // Clear leaf parentIds that point at groups ELK dropped.
1118
+ for (const n of result.nodes as SubsystemGraphNode[]) {
1119
+ if (n.type !== 'subsystem-component') continue;
1120
+ const parentId = (n as { parentId?: string }).parentId;
1121
+ if (parentId && !builtGroupIds.has(parentId)) {
1122
+ delete (n as { parentId?: string }).parentId;
1123
+ }
1124
+ }
1125
+ placedNodes = [
1126
+ ...processGroupNodes,
1127
+ ...moduleGroupNodes,
1128
+ ...(result.nodes as SubsystemGraphNode[]),
1129
+ ];
1001
1130
  labelPositions = result.edgeLabelPositions;
1002
1131
  elkPathStrings = result.edgePaths;
1003
1132
  elkPathPoints = result.edgePathPoints;
1004
1133
  } catch (err) {
1005
- // Fall back to the (unpositioned) grid if ELK is unavailable.
1134
+ // Fall back to the (unpositioned) grid if ELK is unavailable — still
1135
+ // emit multi-member frames so parentId targets exist.
1006
1136
  console.warn('[subsystem-graph] ELK layout failed, using manual positions:', err);
1137
+ const processGroupNodes: SubsystemGraphNode[] = [];
1138
+ const moduleGroupNodes: SubsystemGraphNode[] = [];
1139
+ for (const g of layoutGroups) {
1140
+ const group: SubsystemGraphNode = {
1141
+ id: g.id,
1142
+ type: 'subsystem-group',
1143
+ position: { x: 0, y: 0 },
1144
+ width: 400,
1145
+ height: 300,
1146
+ ...(g.parentId ? { parentId: g.parentId } : {}),
1147
+ data: { region: g.region },
1148
+ };
1149
+ if (g.region.kind === 'process') processGroupNodes.push(group);
1150
+ else moduleGroupNodes.push(group);
1151
+ }
1152
+ placedNodes = [...processGroupNodes, ...moduleGroupNodes, ...nodes];
1007
1153
  }
1008
1154
  }
1009
1155
 
@@ -43,7 +43,6 @@ export const CONSTRUCT_LABEL: Record<string, string> = {
43
43
  interface: 'interface',
44
44
  type_alias: 'type alias',
45
45
  enum: 'enum',
46
- module: 'module',
47
46
  store: 'store',
48
47
  external: 'external',
49
48
  custom_entity: 'entity',
@@ -326,10 +325,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
326
325
  }
327
326
 
328
327
  /**
329
- * Process boundary frame — a React Flow parent node. Members render inside
330
- * via `parentId`; this draws the labeled container only (no handles, no
331
- * selection). The border color derives deterministically from the process key
332
- * so each deployment unit reads as its own region.
328
+ * Boundary frame — a React Flow parent node for a process or module region.
329
+ * Members render inside via `parentId`; this draws the labeled container only
330
+ * (no handles, no selection). Border color derives from the region key.
333
331
  */
334
332
  export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData, 'subsystem-group'>>) {
335
333
  const { theme } = useTheme();
@@ -343,6 +341,8 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
343
341
  const color = packageColor(region?.key ?? 'process');
344
342
  const dimmed = data.dimmed === true;
345
343
  const hidden = (data as { hidden?: boolean }).hidden === true;
344
+ const label =
345
+ region?.kind === 'module' ? `module · ${region.label}` : (region?.label ?? '');
346
346
 
347
347
  if (!region) return null;
348
348
 
@@ -378,7 +378,7 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
378
378
  whiteSpace: 'nowrap',
379
379
  }}
380
380
  >
381
- {region.label}
381
+ {label}
382
382
  </div>
383
383
  </div>
384
384
  );