@principal-ai/subsystems-react 0.42.0 → 0.43.1
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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js +1 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +6 -9
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +15 -50
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +2 -3
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +2 -3
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +5 -5
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +9 -18
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +16 -12
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +99 -113
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts +5 -5
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +13 -49
- package/dist/subsystem/SubsystemFileTree.js.map +1 -1
- package/dist/subsystem/model.d.ts +67 -53
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +305 -55
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +11 -4
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/toC4.d.ts +4 -4
- package/dist/subsystem/toC4.d.ts.map +1 -1
- package/dist/subsystem/toC4.js +3 -7
- package/dist/subsystem/toC4.js.map +1 -1
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +7 -0
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +0 -4
- package/src/stories/Subsystem/AgentsPanel.stories.tsx +3 -5
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +1 -4
- package/src/stories/Subsystem/C4Graph/c4Fixture.ts +1 -1
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -4
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +19 -19
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +0 -2
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +2 -6
- package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +21 -21
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +0 -3
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +30 -32
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +1 -26
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +3 -25
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +7 -8
- package/src/stories/Subsystem/ComponentGraph/ModulePathNesting.stories.tsx +116 -0
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +9 -11
- package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +3 -6
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +8 -6
- package/src/stories/Subsystem/ComponentGraph/ProposedWalkthroughs.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/Reorder.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +26 -28
- package/src/stories/Subsystem/ComponentGraph/WalkthroughAutoplay.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/WorkspacePackageFrames.stories.tsx +2 -3
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +18 -58
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +0 -2
- package/src/subsystem/C4Graph.tsx +2 -3
- package/src/subsystem/IssueList.tsx +12 -22
- package/src/subsystem/SubsystemComponentGraph.tsx +117 -135
- package/src/subsystem/SubsystemFileTree.tsx +17 -50
- package/src/subsystem/model.test.ts +155 -24
- package/src/subsystem/model.ts +352 -108
- package/src/subsystem/nodes.tsx +13 -3
- package/src/subsystem/toC4.test.ts +5 -12
- package/src/subsystem/toC4.ts +5 -9
- package/src/utils/elkLayout.ts +10 -0
package/dist/subsystem/model.js
CHANGED
|
@@ -15,14 +15,10 @@ import { MarkerType, } from '@xyflow/react';
|
|
|
15
15
|
import { computeElkLayout, calculatePathLength } from '../utils/elkLayout';
|
|
16
16
|
import { EDGE_LABEL_SIDE_PADDING, EDGE_ARROW_INSET } from '../utils/edgeLabel';
|
|
17
17
|
import { purlOwnerName, purlRepoKey } from './paths';
|
|
18
|
-
/** Stable id for a derived graph edge from a
|
|
18
|
+
/** Stable id for a derived graph edge from a walkthrough hop. */
|
|
19
19
|
export function derivedGraphEdgeId(from, to, mechanism) {
|
|
20
20
|
return `${from}--${mechanism}-->${to}`;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Build display edges for the graph canvas from topology relations and
|
|
24
|
-
* walkthrough hops (deduped by from/to/mechanism).
|
|
25
|
-
*/
|
|
26
22
|
/** React Flow / canvas edge id for a walkthrough hop. */
|
|
27
23
|
export function walkthroughStepGraphEdgeId(step) {
|
|
28
24
|
return derivedGraphEdgeId(step.from, step.to, step.mechanism);
|
|
@@ -57,18 +53,6 @@ export function reorderTargetIndex(boundary, from) {
|
|
|
57
53
|
}
|
|
58
54
|
export function deriveGraphEdges(doc) {
|
|
59
55
|
const byId = new Map();
|
|
60
|
-
for (const r of doc.relations ?? []) {
|
|
61
|
-
const id = r.id || derivedGraphEdgeId(r.from, r.to, r.relationType);
|
|
62
|
-
if (!byId.has(id)) {
|
|
63
|
-
byId.set(id, {
|
|
64
|
-
id,
|
|
65
|
-
from: r.from,
|
|
66
|
-
to: r.to,
|
|
67
|
-
mechanism: r.relationType,
|
|
68
|
-
refs: r.refs,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
56
|
for (const w of doc.walkthroughs ?? []) {
|
|
73
57
|
for (const step of w.steps) {
|
|
74
58
|
const id = derivedGraphEdgeId(step.from, step.to, step.mechanism);
|
|
@@ -101,14 +85,20 @@ export function deriveGraphEdges(doc) {
|
|
|
101
85
|
return [...byId.values()];
|
|
102
86
|
}
|
|
103
87
|
/**
|
|
104
|
-
* True when the model has components but no
|
|
105
|
-
*
|
|
88
|
+
* True when the model has components but no graph structure at all: no
|
|
89
|
+
* walkthrough/graphify edges AND no boundary containment (`module` / `process`)
|
|
90
|
+
* to frame. Those snapshots are a catalog of declarations, not a graph.
|
|
91
|
+
*
|
|
92
|
+
* A model that carries `module` / `process` membership still draws frames even
|
|
93
|
+
* with zero edges — that is static topology, so it is not constructs-only.
|
|
106
94
|
*/
|
|
107
95
|
export function isConstructsOnlyModel(doc) {
|
|
108
96
|
if (doc.components.length === 0)
|
|
109
97
|
return false;
|
|
98
|
+
const hasBoundary = doc.components.some((c) => (c.module?.trim() ?? '') !== '' || (c.process?.trim() ?? '') !== '');
|
|
99
|
+
if (hasBoundary)
|
|
100
|
+
return false;
|
|
110
101
|
return deriveGraphEdges({
|
|
111
|
-
relations: doc.relations,
|
|
112
102
|
walkthroughs: doc.walkthroughs,
|
|
113
103
|
graphifyRelations: doc.graphifyRelations,
|
|
114
104
|
}).length === 0;
|
|
@@ -216,6 +206,20 @@ export function processGroupNodeId(processKey) {
|
|
|
216
206
|
export function moduleGroupNodeId(moduleKey) {
|
|
217
207
|
return `module:${moduleKey}`;
|
|
218
208
|
}
|
|
209
|
+
/** React Flow id for a directory boundary group node (path-derived nesting). */
|
|
210
|
+
export function directoryGroupNodeId(dirKey) {
|
|
211
|
+
return `directory:${dirKey}`;
|
|
212
|
+
}
|
|
213
|
+
/** Normalize a module path to forward slashes, no trailing slash or `./`. */
|
|
214
|
+
function normalizeModulePath(p) {
|
|
215
|
+
return p.trim().replace(/\\/g, '/').replace(/\/+$/, '').replace(/^\.\//, '');
|
|
216
|
+
}
|
|
217
|
+
/** Parent directory of a path-like key, or `''` when it has no slash. */
|
|
218
|
+
function parentDirectory(path) {
|
|
219
|
+
const norm = normalizeModulePath(path);
|
|
220
|
+
const idx = norm.lastIndexOf('/');
|
|
221
|
+
return idx <= 0 ? '' : norm.slice(0, idx);
|
|
222
|
+
}
|
|
219
223
|
export const MODULE_BADGE_INSET = 12;
|
|
220
224
|
const MODULE_BADGE_CHAR_WIDTH = 12.5;
|
|
221
225
|
const MODULE_BADGE_CHROME = 16;
|
|
@@ -284,6 +288,8 @@ export function packageGroupNodeId(packageKey) {
|
|
|
284
288
|
export function boundaryGroupNodeId(region) {
|
|
285
289
|
if (region.kind === 'module')
|
|
286
290
|
return moduleGroupNodeId(region.key);
|
|
291
|
+
if (region.kind === 'directory')
|
|
292
|
+
return directoryGroupNodeId(region.key);
|
|
287
293
|
if (region.kind === 'package')
|
|
288
294
|
return packageGroupNodeId(region.key);
|
|
289
295
|
return processGroupNodeId(region.key);
|
|
@@ -379,6 +385,9 @@ function keepRegion(r, showSingletons) {
|
|
|
379
385
|
* processes nest under a package the same way.
|
|
380
386
|
*/
|
|
381
387
|
export function buildBoundaryLayoutGroups(doc, opts = {}) {
|
|
388
|
+
if (opts.moduleNesting === 'path') {
|
|
389
|
+
return buildBoundaryLayoutGroupsByPath(doc, opts);
|
|
390
|
+
}
|
|
382
391
|
const showSingletons = opts.showSingletonFrames === true;
|
|
383
392
|
const packageMode = opts.packageFrames ?? 'multi-repo';
|
|
384
393
|
const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
|
|
@@ -517,17 +526,246 @@ export function buildBoundaryLayoutGroups(doc, opts = {}) {
|
|
|
517
526
|
return [...moduleGroups, ...processGroups, ...packageGroups];
|
|
518
527
|
}
|
|
519
528
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
529
|
+
* Path-nesting variant of {@link buildBoundaryLayoutGroups}.
|
|
530
|
+
*
|
|
531
|
+
* Adds directory frames derived from each component's `module` path segments
|
|
532
|
+
* (`src/session/transcript.ts` → `src` → `src/session` → module frame) and
|
|
533
|
+
* nests module frames under the deepest directory that contains them. Directory
|
|
534
|
+
* frames are kept when they hold 2+ leaves (or `showSingletonFrames`). Process
|
|
535
|
+
* and package frames nest above directories exactly as before.
|
|
536
|
+
*
|
|
537
|
+
* Module frames are unchanged (one per exact `module` string); leaves still
|
|
538
|
+
* parent to their module frame in `convertSubsystemToNodes`.
|
|
523
539
|
*/
|
|
524
|
-
export
|
|
525
|
-
|
|
526
|
-
'
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
'
|
|
530
|
-
|
|
540
|
+
export function buildBoundaryLayoutGroupsByPath(doc, opts = {}) {
|
|
541
|
+
const showSingletons = opts.showSingletonFrames === true;
|
|
542
|
+
const packageMode = opts.packageFrames ?? 'multi-repo';
|
|
543
|
+
const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
|
|
544
|
+
const allPackages = getSubsystemPackageRegions(doc);
|
|
545
|
+
const packageEligible = packageMode === 'always'
|
|
546
|
+
? true
|
|
547
|
+
: packageMode === 'never'
|
|
548
|
+
? false
|
|
549
|
+
: allPackages.length >= 2;
|
|
550
|
+
const packages = packageEligible
|
|
551
|
+
? allPackages.filter((r) => keepRegion(r, showSingletons))
|
|
552
|
+
: [];
|
|
553
|
+
const keptPackageKeys = new Set(packages.map((r) => r.key));
|
|
554
|
+
const processes = getSubsystemRegions(doc).filter((r) => keepRegion(r, showSingletons));
|
|
555
|
+
const keptProcessKeys = new Set(processes.map((r) => r.key));
|
|
556
|
+
const modules = getSubsystemModuleRegions(doc).filter((r) => keepRegion(r, showSingletons));
|
|
557
|
+
// Directory regions: every ancestor directory of a component's module path,
|
|
558
|
+
// with the union of all aliases beneath it.
|
|
559
|
+
const dirMembers = new Map();
|
|
560
|
+
for (const c of doc.components) {
|
|
561
|
+
const m = c.module?.trim();
|
|
562
|
+
if (!m)
|
|
563
|
+
continue;
|
|
564
|
+
let d = parentDirectory(m);
|
|
565
|
+
while (d) {
|
|
566
|
+
const set = dirMembers.get(d) ?? new Set();
|
|
567
|
+
set.add(c.alias);
|
|
568
|
+
dirMembers.set(d, set);
|
|
569
|
+
d = parentDirectory(d);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const keptDirKeys = new Set();
|
|
573
|
+
for (const [d, members] of dirMembers) {
|
|
574
|
+
if (members.size >= 2 || showSingletons)
|
|
575
|
+
keptDirKeys.add(d);
|
|
576
|
+
}
|
|
577
|
+
// Files sitting directly in each directory (drives chain compaction below).
|
|
578
|
+
const directFileCount = new Map();
|
|
579
|
+
for (const c of doc.components) {
|
|
580
|
+
const m = c.module?.trim();
|
|
581
|
+
if (!m)
|
|
582
|
+
continue;
|
|
583
|
+
const d = parentDirectory(m);
|
|
584
|
+
if (d)
|
|
585
|
+
directFileCount.set(d, (directFileCount.get(d) ?? 0) + 1);
|
|
586
|
+
}
|
|
587
|
+
// Compact single-child directory chains, mirroring the file tree: a directory
|
|
588
|
+
// whose only child is a single subdirectory (and which holds no files
|
|
589
|
+
// directly) is merged into it. `app` → `app/book` renders one `app/book`
|
|
590
|
+
// frame, not `app` wrapping `book`.
|
|
591
|
+
let compacting = true;
|
|
592
|
+
while (compacting) {
|
|
593
|
+
compacting = false;
|
|
594
|
+
const childDirsOf = new Map();
|
|
595
|
+
for (const d of keptDirKeys) {
|
|
596
|
+
let a = parentDirectory(d);
|
|
597
|
+
while (a && !keptDirKeys.has(a))
|
|
598
|
+
a = parentDirectory(a);
|
|
599
|
+
if (!a)
|
|
600
|
+
continue;
|
|
601
|
+
const list = childDirsOf.get(a) ?? [];
|
|
602
|
+
list.push(d);
|
|
603
|
+
childDirsOf.set(a, list);
|
|
604
|
+
}
|
|
605
|
+
for (const d of [...keptDirKeys]) {
|
|
606
|
+
const childDirs = childDirsOf.get(d) ?? [];
|
|
607
|
+
if (childDirs.length === 1 && (directFileCount.get(d) ?? 0) === 0) {
|
|
608
|
+
keptDirKeys.delete(d);
|
|
609
|
+
compacting = true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const nearestKeptDirAncestor = (dirKey) => {
|
|
614
|
+
let p = parentDirectory(dirKey);
|
|
615
|
+
while (p) {
|
|
616
|
+
if (keptDirKeys.has(p))
|
|
617
|
+
return p;
|
|
618
|
+
p = parentDirectory(p);
|
|
619
|
+
}
|
|
620
|
+
return undefined;
|
|
621
|
+
};
|
|
622
|
+
const dirRegions = [...keptDirKeys]
|
|
623
|
+
.sort()
|
|
624
|
+
.map((key) => {
|
|
625
|
+
// Label is the path relative to the enclosing kept directory (which
|
|
626
|
+
// already carries the prefix): `book` under `app`, or the full collapsed
|
|
627
|
+
// path `app/book` when the chain was compacted.
|
|
628
|
+
const ancestor = nearestKeptDirAncestor(key);
|
|
629
|
+
return {
|
|
630
|
+
kind: 'directory',
|
|
631
|
+
key,
|
|
632
|
+
label: ancestor ? key.slice(ancestor.length + 1) : key,
|
|
633
|
+
memberAliases: [...(dirMembers.get(key) ?? [])],
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
const moduleDirParent = (moduleKey) => {
|
|
637
|
+
let d = parentDirectory(moduleKey);
|
|
638
|
+
while (d) {
|
|
639
|
+
if (keptDirKeys.has(d))
|
|
640
|
+
return d;
|
|
641
|
+
d = parentDirectory(d);
|
|
642
|
+
}
|
|
643
|
+
return undefined;
|
|
644
|
+
};
|
|
645
|
+
const sharedProcessParent = (memberAliases) => {
|
|
646
|
+
const seen = new Set();
|
|
647
|
+
for (const alias of memberAliases) {
|
|
648
|
+
const p = byAlias.get(alias)?.process?.trim();
|
|
649
|
+
if (p)
|
|
650
|
+
seen.add(p);
|
|
651
|
+
}
|
|
652
|
+
if (seen.size !== 1)
|
|
653
|
+
return undefined;
|
|
654
|
+
const p = [...seen][0];
|
|
655
|
+
return keptProcessKeys.has(p) ? processGroupNodeId(p) : undefined;
|
|
656
|
+
};
|
|
657
|
+
const sharedPackageParent = (memberAliases) => {
|
|
658
|
+
const seen = new Set();
|
|
659
|
+
for (const alias of memberAliases) {
|
|
660
|
+
const c = byAlias.get(alias);
|
|
661
|
+
const pkg = c ? componentPackageKey(c) : undefined;
|
|
662
|
+
if (pkg)
|
|
663
|
+
seen.add(pkg);
|
|
664
|
+
}
|
|
665
|
+
if (seen.size !== 1)
|
|
666
|
+
return undefined;
|
|
667
|
+
const pkg = [...seen][0];
|
|
668
|
+
return keptPackageKeys.has(pkg) ? packageGroupNodeId(pkg) : undefined;
|
|
669
|
+
};
|
|
670
|
+
const frameParent = (memberAliases) => sharedProcessParent(memberAliases) ?? sharedPackageParent(memberAliases);
|
|
671
|
+
const moduleGroups = modules.map((r) => {
|
|
672
|
+
const dirParent = moduleDirParent(r.key);
|
|
673
|
+
const parentId = dirParent
|
|
674
|
+
? directoryGroupNodeId(dirParent)
|
|
675
|
+
: frameParent(r.memberAliases);
|
|
676
|
+
// Relative label when nested: `app/book/actions.ts` under `app/book`
|
|
677
|
+
// reads as `actions.ts`. At the root the full module path stays.
|
|
678
|
+
const label = dirParent ? r.key.slice(dirParent.length + 1) : r.label;
|
|
679
|
+
return {
|
|
680
|
+
id: moduleGroupNodeId(r.key),
|
|
681
|
+
memberAliases: [...r.memberAliases],
|
|
682
|
+
parentId,
|
|
683
|
+
region: { ...r, label },
|
|
684
|
+
};
|
|
685
|
+
});
|
|
686
|
+
const dirGroupByKey = new Map(dirRegions.map((r) => [r.key, r]));
|
|
687
|
+
const moduleRegionByKey = new Map(modules.map((r) => [r.key, r]));
|
|
688
|
+
const dirGroups = dirRegions.map((r) => {
|
|
689
|
+
const id = directoryGroupNodeId(r.key);
|
|
690
|
+
const ancestor = nearestKeptDirAncestor(r.key);
|
|
691
|
+
const parentId = ancestor
|
|
692
|
+
? directoryGroupNodeId(ancestor)
|
|
693
|
+
: frameParent(r.memberAliases);
|
|
694
|
+
// Immediate children: subdirectories and module frames parented here.
|
|
695
|
+
// Only child *group ids* go in the layout member list — a group's
|
|
696
|
+
// descendant leaves belong to its child groups alone, or ELK sees the
|
|
697
|
+
// same leaf in two compound parents and rejects the layout.
|
|
698
|
+
const childDirKeys = dirRegions
|
|
699
|
+
.filter((d) => nearestKeptDirAncestor(d.key) === r.key)
|
|
700
|
+
.map((d) => d.key);
|
|
701
|
+
const childModuleKeys = modules
|
|
702
|
+
.filter((m) => moduleDirParent(m.key) === r.key)
|
|
703
|
+
.map((m) => m.key);
|
|
704
|
+
const claimed = new Set();
|
|
705
|
+
for (const dk of childDirKeys) {
|
|
706
|
+
for (const a of dirGroupByKey.get(dk)?.memberAliases ?? [])
|
|
707
|
+
claimed.add(a);
|
|
708
|
+
}
|
|
709
|
+
for (const mk of childModuleKeys) {
|
|
710
|
+
for (const a of moduleRegionByKey.get(mk)?.memberAliases ?? [])
|
|
711
|
+
claimed.add(a);
|
|
712
|
+
}
|
|
713
|
+
const directLeaves = r.memberAliases.filter((a) => !claimed.has(a));
|
|
714
|
+
return {
|
|
715
|
+
id,
|
|
716
|
+
memberAliases: [
|
|
717
|
+
...childDirKeys.map(directoryGroupNodeId),
|
|
718
|
+
...childModuleKeys.map(moduleGroupNodeId),
|
|
719
|
+
...directLeaves,
|
|
720
|
+
],
|
|
721
|
+
parentId,
|
|
722
|
+
region: r,
|
|
723
|
+
};
|
|
724
|
+
});
|
|
725
|
+
const frameGroups = [...dirGroups, ...moduleGroups];
|
|
726
|
+
// Aliases owned by a module frame (singleton or not) must not become direct
|
|
727
|
+
// leaves of an enclosing process/package frame.
|
|
728
|
+
const moduleMemberAliases = new Set();
|
|
729
|
+
for (const r of modules)
|
|
730
|
+
for (const a of r.memberAliases)
|
|
731
|
+
moduleMemberAliases.add(a);
|
|
732
|
+
const processGroups = processes.map((r) => {
|
|
733
|
+
const processId = processGroupNodeId(r.key);
|
|
734
|
+
const nested = frameGroups.filter((g) => g.parentId === processId);
|
|
735
|
+
const claimed = new Set();
|
|
736
|
+
for (const g of nested)
|
|
737
|
+
for (const a of g.region.memberAliases)
|
|
738
|
+
claimed.add(a);
|
|
739
|
+
const directLeaves = r.memberAliases.filter((alias) => !claimed.has(alias) && !moduleMemberAliases.has(alias));
|
|
740
|
+
return {
|
|
741
|
+
id: processId,
|
|
742
|
+
memberAliases: [...nested.map((g) => g.id), ...directLeaves],
|
|
743
|
+
parentId: sharedPackageParent(r.memberAliases),
|
|
744
|
+
region: r,
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
const processMemberAliases = new Set();
|
|
748
|
+
for (const r of processes)
|
|
749
|
+
for (const a of r.memberAliases)
|
|
750
|
+
processMemberAliases.add(a);
|
|
751
|
+
const packageGroups = packages.map((r) => {
|
|
752
|
+
const packageId = packageGroupNodeId(r.key);
|
|
753
|
+
const nested = [...processGroups, ...frameGroups].filter((g) => g.parentId === packageId);
|
|
754
|
+
const claimed = new Set();
|
|
755
|
+
for (const g of nested)
|
|
756
|
+
for (const a of g.region.memberAliases)
|
|
757
|
+
claimed.add(a);
|
|
758
|
+
const directLeaves = r.memberAliases.filter((alias) => !claimed.has(alias) &&
|
|
759
|
+
!moduleMemberAliases.has(alias) &&
|
|
760
|
+
!processMemberAliases.has(alias));
|
|
761
|
+
return {
|
|
762
|
+
id: packageId,
|
|
763
|
+
memberAliases: [...nested.map((g) => g.id), ...directLeaves],
|
|
764
|
+
region: r,
|
|
765
|
+
};
|
|
766
|
+
});
|
|
767
|
+
return [...frameGroups, ...processGroups, ...packageGroups];
|
|
768
|
+
}
|
|
531
769
|
/** Runtime vocabulary of walkthrough hop mechanisms — mirrors `SubsystemWalkthroughMechanism`. */
|
|
532
770
|
export const SUBSYSTEM_WALKTHROUGH_MECHANISMS = [
|
|
533
771
|
'calls',
|
|
@@ -539,24 +777,14 @@ export const SUBSYSTEM_WALKTHROUGH_MECHANISMS = [
|
|
|
539
777
|
'watches',
|
|
540
778
|
'registers-into',
|
|
541
779
|
];
|
|
542
|
-
const RELATION_TYPE_SET = new Set(SUBSYSTEM_RELATION_TYPES);
|
|
543
780
|
const WALKTHROUGH_MECHANISM_SET = new Set(SUBSYSTEM_WALKTHROUGH_MECHANISMS);
|
|
544
|
-
/** True when a mechanism belongs to the topology relation vocabulary. */
|
|
545
|
-
export function isRelationMechanism(mechanism) {
|
|
546
|
-
return RELATION_TYPE_SET.has(mechanism);
|
|
547
|
-
}
|
|
548
781
|
/** True when a mechanism belongs to the walkthrough hop vocabulary. */
|
|
549
782
|
export function isWalkthroughMechanism(mechanism) {
|
|
550
783
|
return WALKTHROUGH_MECHANISM_SET.has(mechanism);
|
|
551
784
|
}
|
|
552
785
|
export const MECHANISM_COLOR = {
|
|
553
786
|
calls: '#22c55e',
|
|
554
|
-
extends: '#b48ead',
|
|
555
|
-
inherits: '#9b6fd0',
|
|
556
|
-
implements: '#c586c0',
|
|
557
|
-
mixes_in: '#d474a8',
|
|
558
787
|
uses: '#e3b341',
|
|
559
|
-
method: '#c586c0',
|
|
560
788
|
feeds: '#4ec9b0',
|
|
561
789
|
produces: '#a78bfa',
|
|
562
790
|
writes: '#e8853a',
|
|
@@ -566,12 +794,7 @@ export const MECHANISM_COLOR = {
|
|
|
566
794
|
};
|
|
567
795
|
export const MECHANISM_STYLE = {
|
|
568
796
|
calls: 'solid',
|
|
569
|
-
extends: 'dashed',
|
|
570
|
-
inherits: 'dashed',
|
|
571
|
-
implements: 'dashed',
|
|
572
|
-
mixes_in: 'dashed',
|
|
573
797
|
uses: 'solid',
|
|
574
|
-
method: 'solid',
|
|
575
798
|
feeds: 'solid',
|
|
576
799
|
produces: 'solid',
|
|
577
800
|
writes: 'solid',
|
|
@@ -718,12 +941,7 @@ export function boundaryFill(color, alpha = '1f') {
|
|
|
718
941
|
* directly verifiable" styling of edge labels. */
|
|
719
942
|
export const MECHANISM_DESCRIPTIONS = [
|
|
720
943
|
['calls', 'function/method call (call graph edge)', true],
|
|
721
|
-
['extends', 'class inheritance', true],
|
|
722
|
-
['inherits', 'class inheritance', true],
|
|
723
|
-
['implements', 'implements interface / protocol', true],
|
|
724
|
-
['mixes_in', 'applies mixin', true],
|
|
725
944
|
['uses', 'general dependency (import, call, or reference)', false],
|
|
726
|
-
['method', 'structural: has method / member', true],
|
|
727
945
|
['feeds', 'data flow: output feeds into input', false],
|
|
728
946
|
['produces', 'data flow: produces / outputs', false],
|
|
729
947
|
['writes', 'state access: mutates retained state', true],
|
|
@@ -731,6 +949,29 @@ export const MECHANISM_DESCRIPTIONS = [
|
|
|
731
949
|
['watches', 'observes retained state without owning it', false],
|
|
732
950
|
['registers-into', 'registration pattern', false],
|
|
733
951
|
];
|
|
952
|
+
/**
|
|
953
|
+
* Single border/badge color for directory (folder) frames. Folders read as one
|
|
954
|
+
* kind of container, so they share a neutral hue rather than hashing per-path
|
|
955
|
+
* like modules / processes / packages. Host `boundaryColors` still overrides.
|
|
956
|
+
*/
|
|
957
|
+
export const FOLDER_FRAME_COLOR = '#7aa2d4';
|
|
958
|
+
/**
|
|
959
|
+
* True when a boundary frame names a folder. `directory` frames always are.
|
|
960
|
+
* A `module` frame is a folder when its key looks like a directory path — the
|
|
961
|
+
* last segment has no file extension (the model has no explicit file/folder
|
|
962
|
+
* flag, so this is a heuristic; `Dockerfile`-style extensionless files read as
|
|
963
|
+
* folders).
|
|
964
|
+
*/
|
|
965
|
+
export function isFolderFrame(region) {
|
|
966
|
+
if (!region)
|
|
967
|
+
return false;
|
|
968
|
+
if (region.kind === 'directory')
|
|
969
|
+
return true;
|
|
970
|
+
if (region.kind !== 'module')
|
|
971
|
+
return false;
|
|
972
|
+
const last = region.key.split('/').pop() ?? '';
|
|
973
|
+
return last !== '' && !last.includes('.');
|
|
974
|
+
}
|
|
734
975
|
/** Package color palette (derived deterministically from the package name). */
|
|
735
976
|
export function packageColor(name) {
|
|
736
977
|
const palette = [
|
|
@@ -1041,8 +1282,12 @@ export function subsystemGraphLayoutKey(doc) {
|
|
|
1041
1282
|
* layered with minimized crossings.
|
|
1042
1283
|
*/
|
|
1043
1284
|
export async function buildSubsystemGraph(doc, opts = {}) {
|
|
1044
|
-
const { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, packageFrames, graphifyRelations, orderByLine = false, } = opts;
|
|
1045
|
-
const frameOpts = {
|
|
1285
|
+
const { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, packageFrames, moduleNesting, graphifyRelations, orderByLine = false, } = opts;
|
|
1286
|
+
const frameOpts = {
|
|
1287
|
+
showSingletonFrames,
|
|
1288
|
+
packageFrames,
|
|
1289
|
+
moduleNesting,
|
|
1290
|
+
};
|
|
1046
1291
|
const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
|
|
1047
1292
|
const edges = convertSubsystemToEdges(doc, graphifyRelations);
|
|
1048
1293
|
// Nested boundary tree: package → process → module → leaves.
|
|
@@ -1133,7 +1378,12 @@ export async function buildSubsystemGraph(doc, opts = {}) {
|
|
|
1133
1378
|
try {
|
|
1134
1379
|
const result = await computeElkLayout(nodes, edges, {
|
|
1135
1380
|
routingStyle: 'orthogonal',
|
|
1136
|
-
|
|
1381
|
+
// Edge-driven graphs flow left-to-right. With no edges (pure
|
|
1382
|
+
// containment, e.g. the static-topology layer) a RIGHT layout stacks
|
|
1383
|
+
// disconnected top-level frames in one vertical column; DOWN makes the
|
|
1384
|
+
// layered pass place those sibling frames along the horizontal axis
|
|
1385
|
+
// while each frame's members stack vertically.
|
|
1386
|
+
direction: edges.length > 0 ? 'RIGHT' : 'DOWN',
|
|
1137
1387
|
nodeSpacing: 60,
|
|
1138
1388
|
edgeSpacing: 30,
|
|
1139
1389
|
edgeNodeSpacing: 60,
|
|
@@ -1153,7 +1403,7 @@ export async function buildSubsystemGraph(doc, opts = {}) {
|
|
|
1153
1403
|
// Modules collapse a long path to `first/…/last`, so they only need
|
|
1154
1404
|
// room for the collapsed badge. Processes/packages never collapse:
|
|
1155
1405
|
// size the frame to hold the full centered label plus padding.
|
|
1156
|
-
minWidth: g.region.kind === 'module'
|
|
1406
|
+
minWidth: g.region.kind === 'module' || g.region.kind === 'directory'
|
|
1157
1407
|
? moduleMinWidthForBadge(g.region.label)
|
|
1158
1408
|
: boundaryMinWidthForBadge(g.region.label),
|
|
1159
1409
|
})),
|
|
@@ -1221,7 +1471,7 @@ export async function buildSubsystemGraph(doc, opts = {}) {
|
|
|
1221
1471
|
id: g.id,
|
|
1222
1472
|
type: 'subsystem-group',
|
|
1223
1473
|
position: { x: 0, y: 0 },
|
|
1224
|
-
width: Math.max(400, g.region.kind === 'module'
|
|
1474
|
+
width: Math.max(400, g.region.kind === 'module' || g.region.kind === 'directory'
|
|
1225
1475
|
? moduleMinWidthForBadge(g.region.label)
|
|
1226
1476
|
: boundaryMinWidthForBadge(g.region.label)),
|
|
1227
1477
|
height: 300,
|