@principal-ai/subsystems-react 0.21.1 → 0.23.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/dist/graphify/consolidated.d.ts +5 -24
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/index.d.ts +1 -1
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreFileView.d.ts +3 -1
- package/dist/pierre/PierreFileView.d.ts.map +1 -1
- package/dist/pierre/PierreFileView.js +13 -3
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
- package/dist/pierre/PierreSnippetView.js +5 -26
- package/dist/pierre/PierreSnippetView.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +3 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +65 -8
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/pierre/constructColors.d.ts.map +1 -1
- package/dist/pierre/constructColors.js +0 -5
- package/dist/pierre/constructColors.js.map +1 -1
- package/dist/pierre/index.d.ts +1 -1
- package/dist/pierre/index.d.ts.map +1 -1
- package/dist/pierre/index.js +1 -1
- package/dist/pierre/index.js.map +1 -1
- package/dist/pierre/scrollAnchor.d.ts +2 -0
- package/dist/pierre/scrollAnchor.d.ts.map +1 -1
- package/dist/pierre/scrollAnchor.js +7 -0
- package/dist/pierre/scrollAnchor.js.map +1 -1
- package/dist/pierre/sliceSnippet.d.ts +6 -0
- package/dist/pierre/sliceSnippet.d.ts.map +1 -1
- package/dist/pierre/sliceSnippet.js +25 -0
- package/dist/pierre/sliceSnippet.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -4
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts +7 -2
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +13 -9
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +97 -8
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/declarationRef.d.ts +6 -1
- package/dist/subsystem/declarationRef.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +0 -15
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +61 -22
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +185 -65
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +3 -4
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +5 -6
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +6 -4
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +144 -54
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/graphify/consolidated.ts +4 -26
- package/src/graphify/index.ts +0 -2
- package/src/index.ts +0 -2
- package/src/pierre/PierreFileView.tsx +25 -2
- package/src/pierre/PierreSnippetView.tsx +5 -31
- package/src/pierre/PierreWalkthroughCodeView.tsx +110 -5
- package/src/pierre/constructColors.test.ts +1 -2
- package/src/pierre/constructColors.ts +0 -5
- package/src/pierre/index.ts +1 -1
- package/src/pierre/scrollAnchor.ts +13 -0
- package/src/pierre/sliceSnippet.test.ts +44 -1
- package/src/pierre/sliceSnippet.ts +28 -0
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +0 -31
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -46
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -24
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +2 -1
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +287 -0
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -4
- package/src/subsystem/FileDrawer.tsx +19 -7
- package/src/subsystem/SubsystemComponentGraph.tsx +153 -7
- package/src/subsystem/declarationRef.ts +6 -1
- package/src/subsystem/formatDeclaration.ts +0 -19
- package/src/subsystem/model.test.ts +190 -9
- package/src/subsystem/model.ts +219 -77
- package/src/subsystem/nodes.tsx +6 -6
- package/src/utils/elkLayout.ts +148 -58
package/src/subsystem/model.ts
CHANGED
|
@@ -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,16 +91,12 @@ 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'
|
|
101
97
|
| 'mixes_in'
|
|
102
98
|
| 'method'
|
|
103
|
-
| 'references'
|
|
104
|
-
| 'contains';
|
|
99
|
+
| 'references';
|
|
105
100
|
|
|
106
101
|
/**
|
|
107
102
|
* Walkthrough hop mechanism — runtime seams with a `file:line` site.
|
|
@@ -190,11 +185,21 @@ export interface SubsystemComponent {
|
|
|
190
185
|
/**
|
|
191
186
|
* Runtime process membership — which deployment unit this node is a
|
|
192
187
|
* member of (e.g. `principal-studio/host`, `principal-studio/renderer`). Nodes
|
|
193
|
-
* sharing a `process` are drawn inside one boundary region
|
|
194
|
-
*
|
|
195
|
-
*
|
|
188
|
+
* sharing a `process` are drawn inside one boundary region; nodes without
|
|
189
|
+
* one sit outside every process boundary (external actors, services,
|
|
190
|
+
* libraries). Orthogonal to `module` (source-file frame).
|
|
196
191
|
*/
|
|
197
192
|
process?: string;
|
|
193
|
+
/**
|
|
194
|
+
* Source-module membership — which file/module this export belongs to
|
|
195
|
+
* (e.g. `src/session/transcript.ts`). Nodes sharing a `module` are drawn
|
|
196
|
+
* inside one boundary frame. Prefer this over inventing a module construct:
|
|
197
|
+
* each export keeps its real construct (`function` / `class` / …) and the
|
|
198
|
+
* file reads as a frame. Orthogonal to `process` (runtime deployment).
|
|
199
|
+
* When both are set, the module frame is the component's parent (finer
|
|
200
|
+
* grain); process framing still groups siblings that share a process.
|
|
201
|
+
*/
|
|
202
|
+
module?: string;
|
|
198
203
|
/** A symbol this component exposes / is (the node's identity). */
|
|
199
204
|
symbol?: string;
|
|
200
205
|
/**
|
|
@@ -354,26 +359,20 @@ export function deriveGraphEdges(doc: {
|
|
|
354
359
|
*
|
|
355
360
|
* `symbol` is the source of truth (fully-qualified code identity). The name
|
|
356
361
|
* is the symbol itself:
|
|
357
|
-
* - class/type/
|
|
358
|
-
* - method
|
|
359
|
-
* -
|
|
360
|
-
* a common-sense convention for whole-file modules, not a real TS name
|
|
361
|
-
* - otherwise no symbol → fall back to an existing name
|
|
362
|
+
* - class/type/function/... symbol → symbol (e.g. `SessionReader`)
|
|
363
|
+
* - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
|
|
364
|
+
* - otherwise no symbol → fall back to an existing name
|
|
362
365
|
*/
|
|
363
366
|
export function deriveNameFromSymbol(
|
|
364
367
|
symbol: string | undefined,
|
|
365
368
|
construct: SubsystemComponentConstruct,
|
|
366
369
|
existingName?: string,
|
|
367
|
-
|
|
370
|
+
_file?: string,
|
|
368
371
|
stereotype?: string,
|
|
369
372
|
): string {
|
|
370
373
|
let name: string | undefined;
|
|
371
374
|
if (symbol && symbol.trim()) {
|
|
372
375
|
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
376
|
}
|
|
378
377
|
if (!name) name = existingName ?? 'untitled';
|
|
379
378
|
|
|
@@ -382,7 +381,7 @@ export function deriveNameFromSymbol(
|
|
|
382
381
|
// Executable constructs wear `()`; brace-bodied constructs (interface,
|
|
383
382
|
// type_alias, enum) wear ` {}`. Classes render bare — the construct badge
|
|
384
383
|
// already says "class".
|
|
385
|
-
// Everything else (class, store,
|
|
384
|
+
// Everything else (class, store, external) renders bare.
|
|
386
385
|
if (stereotype === 'component' && !name.startsWith('<')) {
|
|
387
386
|
return `<${name}>`;
|
|
388
387
|
}
|
|
@@ -431,11 +430,13 @@ export function formatPurl(purl: string): string {
|
|
|
431
430
|
export type SubsystemGraphNodeType = 'subsystem-component' | 'subsystem-group';
|
|
432
431
|
|
|
433
432
|
/**
|
|
434
|
-
* One
|
|
435
|
-
* Nodes without
|
|
433
|
+
* One boundary region — either a process (runtime deployment unit) or a
|
|
434
|
+
* module (source file). Nodes without that field sit outside those frames.
|
|
436
435
|
*/
|
|
437
436
|
export interface SubsystemProcessRegion {
|
|
438
|
-
/**
|
|
437
|
+
/** Discriminator — which field produced this region. */
|
|
438
|
+
kind: 'process' | 'module';
|
|
439
|
+
/** The `process` or `module` value (e.g. `principal-studio/host`, `src/a.ts`). */
|
|
439
440
|
key: string;
|
|
440
441
|
/** Display label for the boundary frame. */
|
|
441
442
|
label: string;
|
|
@@ -448,9 +449,21 @@ export function processGroupNodeId(processKey: string): string {
|
|
|
448
449
|
return `process:${processKey}`;
|
|
449
450
|
}
|
|
450
451
|
|
|
452
|
+
/** React Flow id for a module boundary group node. */
|
|
453
|
+
export function moduleGroupNodeId(moduleKey: string): string {
|
|
454
|
+
return `module:${moduleKey}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** React Flow id for a boundary group of either kind. */
|
|
458
|
+
export function boundaryGroupNodeId(region: Pick<SubsystemProcessRegion, 'kind' | 'key'>): string {
|
|
459
|
+
return region.kind === 'module'
|
|
460
|
+
? moduleGroupNodeId(region.key)
|
|
461
|
+
: processGroupNodeId(region.key);
|
|
462
|
+
}
|
|
463
|
+
|
|
451
464
|
/**
|
|
452
|
-
* Derive boundary regions
|
|
453
|
-
*
|
|
465
|
+
* Derive process boundary regions — one per distinct non-empty `process`
|
|
466
|
+
* value, in first-appearance order.
|
|
454
467
|
*/
|
|
455
468
|
export function getSubsystemRegions(
|
|
456
469
|
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
@@ -464,12 +477,100 @@ export function getSubsystemRegions(
|
|
|
464
477
|
byProcess.set(p, list);
|
|
465
478
|
}
|
|
466
479
|
return [...byProcess.entries()].map(([key, memberIds]) => ({
|
|
480
|
+
kind: 'process' as const,
|
|
467
481
|
key,
|
|
468
482
|
label: key,
|
|
469
483
|
memberIds,
|
|
470
484
|
}));
|
|
471
485
|
}
|
|
472
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Derive module boundary regions — one per distinct non-empty `module`
|
|
489
|
+
* value, in first-appearance order. Label is the module path (file).
|
|
490
|
+
*/
|
|
491
|
+
export function getSubsystemModuleRegions(
|
|
492
|
+
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
493
|
+
): SubsystemProcessRegion[] {
|
|
494
|
+
const byModule = new Map<string, string[]>();
|
|
495
|
+
for (const c of doc.components) {
|
|
496
|
+
const m = c.module?.trim();
|
|
497
|
+
if (!m) continue;
|
|
498
|
+
const list = byModule.get(m) ?? [];
|
|
499
|
+
list.push(c.id);
|
|
500
|
+
byModule.set(m, list);
|
|
501
|
+
}
|
|
502
|
+
return [...byModule.entries()].map(([key, memberIds]) => ({
|
|
503
|
+
kind: 'module' as const,
|
|
504
|
+
key,
|
|
505
|
+
label: key,
|
|
506
|
+
memberIds,
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* One compound frame for ELK / React Flow — module frames may nest under a
|
|
512
|
+
* process frame via `parentId`.
|
|
513
|
+
*/
|
|
514
|
+
export interface BoundaryLayoutGroup {
|
|
515
|
+
id: string;
|
|
516
|
+
memberIds: string[];
|
|
517
|
+
/** When set, this group is a child of another boundary group (process). */
|
|
518
|
+
parentId?: string;
|
|
519
|
+
region: SubsystemProcessRegion;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Build the process → module → leaf group tree for layout.
|
|
524
|
+
* Multi-member modules nest under a process when every member shares that
|
|
525
|
+
* process; process ELK children are nested module group ids plus any
|
|
526
|
+
* process members that are not inside a kept module frame.
|
|
527
|
+
*/
|
|
528
|
+
export function buildBoundaryLayoutGroups(
|
|
529
|
+
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
530
|
+
): BoundaryLayoutGroup[] {
|
|
531
|
+
const byId = new Map(doc.components.map((c) => [c.id, c]));
|
|
532
|
+
const modules = getSubsystemModuleRegions(doc).filter((r) => r.memberIds.length >= 2);
|
|
533
|
+
const processes = getSubsystemRegions(doc).filter((r) => r.memberIds.length >= 2);
|
|
534
|
+
const keptProcessKeys = new Set(processes.map((r) => r.key));
|
|
535
|
+
|
|
536
|
+
const moduleGroups: BoundaryLayoutGroup[] = modules.map((r) => {
|
|
537
|
+
const processesOfMembers = new Set<string>();
|
|
538
|
+
for (const id of r.memberIds) {
|
|
539
|
+
const p = byId.get(id)?.process?.trim();
|
|
540
|
+
if (p) processesOfMembers.add(p);
|
|
541
|
+
}
|
|
542
|
+
let parentId: string | undefined;
|
|
543
|
+
if (processesOfMembers.size === 1) {
|
|
544
|
+
const p = [...processesOfMembers][0]!;
|
|
545
|
+
if (keptProcessKeys.has(p)) parentId = processGroupNodeId(p);
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
id: moduleGroupNodeId(r.key),
|
|
549
|
+
memberIds: [...r.memberIds],
|
|
550
|
+
parentId,
|
|
551
|
+
region: r,
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const processGroups: BoundaryLayoutGroup[] = processes.map((r) => {
|
|
556
|
+
const processId = processGroupNodeId(r.key);
|
|
557
|
+
const nestedModules = moduleGroups.filter((m) => m.parentId === processId);
|
|
558
|
+
const nestedModuleKeys = new Set(nestedModules.map((m) => m.region.key));
|
|
559
|
+
const directLeaves = r.memberIds.filter((id) => {
|
|
560
|
+
const mod = byId.get(id)?.module?.trim();
|
|
561
|
+
if (!mod) return true;
|
|
562
|
+
return !nestedModuleKeys.has(mod);
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
id: processId,
|
|
566
|
+
memberIds: [...nestedModules.map((m) => m.id), ...directLeaves],
|
|
567
|
+
region: r,
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
return [...moduleGroups, ...processGroups];
|
|
572
|
+
}
|
|
573
|
+
|
|
473
574
|
export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
474
575
|
component: SubsystemComponent;
|
|
475
576
|
/** Set while a file is open in the drawer: true if this node's component
|
|
@@ -509,9 +610,6 @@ export type SubsystemGraphEdge = Edge<SubsystemGraphEdgeData>;
|
|
|
509
610
|
|
|
510
611
|
export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
511
612
|
imports: '#0893d2', // blue
|
|
512
|
-
imports_from: '#5aa9e6', // light blue
|
|
513
|
-
re_exports: '#3aa5c9', // cyan-blue
|
|
514
|
-
defines: '#2e86ab', // steel blue
|
|
515
613
|
calls: '#4ec9b0', // teal
|
|
516
614
|
extends: '#b48ead', // purple
|
|
517
615
|
inherits: '#9b6fd0', // purple
|
|
@@ -520,7 +618,6 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
520
618
|
uses: '#e3b341', // gold
|
|
521
619
|
method: '#c586c0', // magenta
|
|
522
620
|
references: '#e07a5f', // terracotta
|
|
523
|
-
contains: '#6c5ce7', // indigo
|
|
524
621
|
feeds: '#22c55e', // green — data-flow into a processor
|
|
525
622
|
produces: '#e07a5f', // terracotta — emits an output type
|
|
526
623
|
writes: '#2f9e44', // deep green — mutates retained state
|
|
@@ -531,9 +628,6 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
531
628
|
|
|
532
629
|
export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed' | 'dotted'> = {
|
|
533
630
|
imports: 'solid',
|
|
534
|
-
imports_from: 'solid',
|
|
535
|
-
re_exports: 'solid',
|
|
536
|
-
defines: 'solid',
|
|
537
631
|
calls: 'solid',
|
|
538
632
|
extends: 'dashed',
|
|
539
633
|
inherits: 'dashed',
|
|
@@ -542,7 +636,6 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
542
636
|
uses: 'solid',
|
|
543
637
|
method: 'solid',
|
|
544
638
|
references: 'dotted',
|
|
545
|
-
contains: 'solid',
|
|
546
639
|
feeds: 'solid',
|
|
547
640
|
produces: 'solid',
|
|
548
641
|
writes: 'solid',
|
|
@@ -555,9 +648,6 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
555
648
|
* directly verifiable" styling of edge labels. */
|
|
556
649
|
export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][] = [
|
|
557
650
|
['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
651
|
['calls', 'function/method call (call graph edge)', true],
|
|
562
652
|
['extends', 'class inheritance', true],
|
|
563
653
|
['inherits', 'class inheritance', true],
|
|
@@ -566,7 +656,6 @@ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][]
|
|
|
566
656
|
['uses', 'general dependency (import, call, or reference)', false],
|
|
567
657
|
['method', 'structural: has method / member', true],
|
|
568
658
|
['references', 'type / symbol reference (not a call)', true],
|
|
569
|
-
['contains', 'structural: contains / encapsulates', true],
|
|
570
659
|
['feeds', 'data flow: output feeds into input', false],
|
|
571
660
|
['produces', 'data flow: produces / outputs', false],
|
|
572
661
|
['writes', 'state access: mutates retained state', true],
|
|
@@ -750,23 +839,23 @@ export function nodeMinWidthForBadges(component: {
|
|
|
750
839
|
|
|
751
840
|
/**
|
|
752
841
|
* Convert a subsystem graph document into React Flow nodes. Components that
|
|
753
|
-
* carry a `
|
|
754
|
-
* (`
|
|
755
|
-
*
|
|
756
|
-
*
|
|
842
|
+
* carry a `module` get a `parentId` pointing at their module frame
|
|
843
|
+
* (`module:<path>`); otherwise a `process` stamps `process:<process>`. Module
|
|
844
|
+
* frames may themselves nest under a process frame (stamped on the group node
|
|
845
|
+
* in `buildSubsystemGraph`). Nodes without either stay top-level. The initial
|
|
846
|
+
* grid clusters by `module ?? process ?? purl`; ELK then refines with compound
|
|
847
|
+
* layout.
|
|
757
848
|
*/
|
|
758
849
|
export function convertSubsystemToNodes(
|
|
759
850
|
doc: SubsystemModelDocument,
|
|
760
851
|
opts: { maxNodeWidth?: number } = {},
|
|
761
852
|
): SubsystemGraphNode[] {
|
|
762
853
|
const { maxNodeWidth } = opts;
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
// purl is code identity — nodes without a process (external actors,
|
|
766
|
-
// services, libraries) fall back to purl and sit outside every boundary.
|
|
854
|
+
// Cluster for the pre-ELK grid: module (source file) → process (runtime)
|
|
855
|
+
// → purl (package identity).
|
|
767
856
|
const byPkg = new Map<string, SubsystemComponent[]>();
|
|
768
857
|
for (const c of doc.components) {
|
|
769
|
-
const regionKey = c.process ?? c.purl;
|
|
858
|
+
const regionKey = c.module ?? c.process ?? c.purl;
|
|
770
859
|
const list = byPkg.get(regionKey) ?? [];
|
|
771
860
|
list.push(c);
|
|
772
861
|
byPkg.set(regionKey, list);
|
|
@@ -801,11 +890,17 @@ export function convertSubsystemToNodes(
|
|
|
801
890
|
const cssBorder = 4; // 2px border each side
|
|
802
891
|
const rawWidth = Math.max(cssMinWidth, textWidth + cssPadding + cssBorder);
|
|
803
892
|
const nodeWidth = Math.max(cssMinWidth, Math.min(cap, rawWidth));
|
|
893
|
+
const moduleKey = c.module?.trim();
|
|
804
894
|
const processKey = c.process?.trim();
|
|
895
|
+
const parentId = moduleKey
|
|
896
|
+
? moduleGroupNodeId(moduleKey)
|
|
897
|
+
: processKey
|
|
898
|
+
? processGroupNodeId(processKey)
|
|
899
|
+
: undefined;
|
|
805
900
|
nodes.push({
|
|
806
901
|
id: c.id,
|
|
807
902
|
type: 'subsystem-component',
|
|
808
|
-
...(
|
|
903
|
+
...(parentId ? { parentId } : {}),
|
|
809
904
|
position: { x: PAD + col * COL_W, y: cursorY + row * ROW_H },
|
|
810
905
|
width: nodeWidth,
|
|
811
906
|
height: 84,
|
|
@@ -818,20 +913,21 @@ export function convertSubsystemToNodes(
|
|
|
818
913
|
}
|
|
819
914
|
|
|
820
915
|
/**
|
|
821
|
-
* Convert boundary regions into React Flow parent (group) nodes.
|
|
822
|
-
*
|
|
916
|
+
* Convert boundary regions into React Flow parent (group) nodes. Module
|
|
917
|
+
* frames that share a process nest under that process via `parentId`.
|
|
823
918
|
* Positions/sizes are placeholders — ELK compound layout overwrites them.
|
|
824
919
|
*/
|
|
825
920
|
export function convertSubsystemToGroups(
|
|
826
921
|
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
827
922
|
): SubsystemGraphNode[] {
|
|
828
|
-
return
|
|
829
|
-
id:
|
|
830
|
-
type: 'subsystem-group',
|
|
923
|
+
return buildBoundaryLayoutGroups(doc).map((g) => ({
|
|
924
|
+
id: g.id,
|
|
925
|
+
type: 'subsystem-group' as const,
|
|
831
926
|
position: { x: 0, y: 0 },
|
|
832
927
|
width: 400,
|
|
833
928
|
height: 300,
|
|
834
|
-
|
|
929
|
+
...(g.parentId ? { parentId: g.parentId } : {}),
|
|
930
|
+
data: { region: g.region },
|
|
835
931
|
}));
|
|
836
932
|
}
|
|
837
933
|
|
|
@@ -874,8 +970,8 @@ export function subsystemGraphLayoutKey(
|
|
|
874
970
|
doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'>,
|
|
875
971
|
): string {
|
|
876
972
|
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'))
|
|
973
|
+
.map(({ id, purl, name, symbol, construct, file, purpose, process, module }) =>
|
|
974
|
+
[id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? '', module ?? ''].join('\0'))
|
|
879
975
|
.sort()
|
|
880
976
|
.join('\n');
|
|
881
977
|
const edgeKey = deriveGraphEdges(doc)
|
|
@@ -901,15 +997,25 @@ export async function buildSubsystemGraph(
|
|
|
901
997
|
const { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights } = opts;
|
|
902
998
|
const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
|
|
903
999
|
const edges = convertSubsystemToEdges(doc);
|
|
904
|
-
//
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
const
|
|
1000
|
+
// Nested boundary tree: process → module → leaves (when both fields set).
|
|
1001
|
+
const layoutGroups = buildBoundaryLayoutGroups(doc);
|
|
1002
|
+
const regions = layoutGroups.map((g) => g.region);
|
|
1003
|
+
const regionIds = new Set(layoutGroups.map((g) => g.id));
|
|
1004
|
+
const byId = new Map(doc.components.map((c) => [c.id, c]));
|
|
1005
|
+
|
|
1006
|
+
// Drop leaf parentIds that point at singleton / missing frames; fall back
|
|
1007
|
+
// from a dropped module frame to a kept process frame when possible.
|
|
909
1008
|
for (const n of nodes) {
|
|
910
1009
|
if (n.type !== 'subsystem-component') continue;
|
|
911
|
-
const
|
|
912
|
-
if (
|
|
1010
|
+
const parentId = (n as { parentId?: string }).parentId;
|
|
1011
|
+
if (!parentId) continue;
|
|
1012
|
+
if (regionIds.has(parentId)) continue;
|
|
1013
|
+
const comp = (n.data as SubsystemGraphNodeData).component;
|
|
1014
|
+
const processKey = comp?.process?.trim() || byId.get(n.id)?.process?.trim();
|
|
1015
|
+
const processId = processKey ? processGroupNodeId(processKey) : undefined;
|
|
1016
|
+
if (processId && regionIds.has(processId)) {
|
|
1017
|
+
(n as { parentId?: string }).parentId = processId;
|
|
1018
|
+
} else {
|
|
913
1019
|
delete (n as { parentId?: string }).parentId;
|
|
914
1020
|
}
|
|
915
1021
|
}
|
|
@@ -961,8 +1067,7 @@ export async function buildSubsystemGraph(
|
|
|
961
1067
|
}
|
|
962
1068
|
}
|
|
963
1069
|
|
|
964
|
-
// ELK auto-layout:
|
|
965
|
-
// process partitions as compound parents so boundaries shape the layout.
|
|
1070
|
+
// ELK auto-layout: nested compound parents (process → module → leaves).
|
|
966
1071
|
let placedNodes = nodes;
|
|
967
1072
|
let labelPositions = new Map<string, { x: number; y: number }>();
|
|
968
1073
|
let elkPathStrings = new Map<string, string>();
|
|
@@ -978,32 +1083,69 @@ export async function buildSubsystemGraph(
|
|
|
978
1083
|
interLayerSpacing: 120,
|
|
979
1084
|
preserveNodePositions: false,
|
|
980
1085
|
edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
|
|
981
|
-
groups:
|
|
982
|
-
id:
|
|
983
|
-
memberIds:
|
|
1086
|
+
groups: layoutGroups.map((g) => ({
|
|
1087
|
+
id: g.id,
|
|
1088
|
+
memberIds: g.memberIds,
|
|
1089
|
+
parentId: g.parentId,
|
|
984
1090
|
})),
|
|
985
1091
|
});
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
1092
|
+
const builtGroupIds = new Set(result.groupBounds.keys());
|
|
1093
|
+
// Parents before children — process frames, then nested module frames.
|
|
1094
|
+
const processGroupNodes: SubsystemGraphNode[] = [];
|
|
1095
|
+
const moduleGroupNodes: SubsystemGraphNode[] = [];
|
|
1096
|
+
for (const g of layoutGroups) {
|
|
1097
|
+
const bounds = result.groupBounds.get(g.id);
|
|
1098
|
+
if (!bounds) continue;
|
|
1099
|
+
const parentId =
|
|
1100
|
+
g.parentId && builtGroupIds.has(g.parentId) ? g.parentId : undefined;
|
|
989
1101
|
const group: SubsystemGraphNode = {
|
|
990
|
-
id:
|
|
1102
|
+
id: g.id,
|
|
991
1103
|
type: 'subsystem-group',
|
|
992
1104
|
position: { x: bounds.x, y: bounds.y },
|
|
993
1105
|
width: Math.max(200, bounds.width),
|
|
994
1106
|
height: Math.max(160, bounds.height),
|
|
995
|
-
|
|
1107
|
+
...(parentId ? { parentId } : {}),
|
|
1108
|
+
data: { region: g.region },
|
|
996
1109
|
};
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1110
|
+
if (g.region.kind === 'process') processGroupNodes.push(group);
|
|
1111
|
+
else moduleGroupNodes.push(group);
|
|
1112
|
+
}
|
|
1113
|
+
// Clear leaf parentIds that point at groups ELK dropped.
|
|
1114
|
+
for (const n of result.nodes as SubsystemGraphNode[]) {
|
|
1115
|
+
if (n.type !== 'subsystem-component') continue;
|
|
1116
|
+
const parentId = (n as { parentId?: string }).parentId;
|
|
1117
|
+
if (parentId && !builtGroupIds.has(parentId)) {
|
|
1118
|
+
delete (n as { parentId?: string }).parentId;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
placedNodes = [
|
|
1122
|
+
...processGroupNodes,
|
|
1123
|
+
...moduleGroupNodes,
|
|
1124
|
+
...(result.nodes as SubsystemGraphNode[]),
|
|
1125
|
+
];
|
|
1001
1126
|
labelPositions = result.edgeLabelPositions;
|
|
1002
1127
|
elkPathStrings = result.edgePaths;
|
|
1003
1128
|
elkPathPoints = result.edgePathPoints;
|
|
1004
1129
|
} catch (err) {
|
|
1005
|
-
// Fall back to the (unpositioned) grid if ELK is unavailable
|
|
1130
|
+
// Fall back to the (unpositioned) grid if ELK is unavailable — still
|
|
1131
|
+
// emit multi-member frames so parentId targets exist.
|
|
1006
1132
|
console.warn('[subsystem-graph] ELK layout failed, using manual positions:', err);
|
|
1133
|
+
const processGroupNodes: SubsystemGraphNode[] = [];
|
|
1134
|
+
const moduleGroupNodes: SubsystemGraphNode[] = [];
|
|
1135
|
+
for (const g of layoutGroups) {
|
|
1136
|
+
const group: SubsystemGraphNode = {
|
|
1137
|
+
id: g.id,
|
|
1138
|
+
type: 'subsystem-group',
|
|
1139
|
+
position: { x: 0, y: 0 },
|
|
1140
|
+
width: 400,
|
|
1141
|
+
height: 300,
|
|
1142
|
+
...(g.parentId ? { parentId: g.parentId } : {}),
|
|
1143
|
+
data: { region: g.region },
|
|
1144
|
+
};
|
|
1145
|
+
if (g.region.kind === 'process') processGroupNodes.push(group);
|
|
1146
|
+
else moduleGroupNodes.push(group);
|
|
1147
|
+
}
|
|
1148
|
+
placedNodes = [...processGroupNodes, ...moduleGroupNodes, ...nodes];
|
|
1007
1149
|
}
|
|
1008
1150
|
}
|
|
1009
1151
|
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -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
|
-
*
|
|
330
|
-
* via `parentId`; this draws the labeled container only
|
|
331
|
-
* selection).
|
|
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
|
-
{
|
|
381
|
+
{label}
|
|
382
382
|
</div>
|
|
383
383
|
</div>
|
|
384
384
|
);
|