@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/src/subsystem/model.ts
CHANGED
|
@@ -108,17 +108,6 @@ export interface SubsystemDeclToken {
|
|
|
108
108
|
color?: string;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
* Topology relation type — structural / module / type claims.
|
|
113
|
-
* Belongs on `relations[]`, not on walkthrough hops.
|
|
114
|
-
*/
|
|
115
|
-
export type SubsystemRelationType =
|
|
116
|
-
| 'extends'
|
|
117
|
-
| 'inherits'
|
|
118
|
-
| 'implements'
|
|
119
|
-
| 'mixes_in'
|
|
120
|
-
| 'method';
|
|
121
|
-
|
|
122
111
|
/**
|
|
123
112
|
* Walkthrough hop mechanism — runtime seams with a `file:line` site.
|
|
124
113
|
*/
|
|
@@ -132,24 +121,21 @@ export type SubsystemWalkthroughMechanism =
|
|
|
132
121
|
| 'watches'
|
|
133
122
|
| 'registers-into';
|
|
134
123
|
|
|
135
|
-
/** Union for derived graph-edge styling
|
|
136
|
-
export type SubsystemEdgeMechanism =
|
|
137
|
-
| SubsystemRelationType
|
|
138
|
-
| SubsystemWalkthroughMechanism;
|
|
124
|
+
/** Union for derived graph-edge styling — the walkthrough hop mechanism. */
|
|
125
|
+
export type SubsystemEdgeMechanism = SubsystemWalkthroughMechanism;
|
|
139
126
|
|
|
140
127
|
/**
|
|
141
|
-
* Which edge
|
|
142
|
-
*
|
|
143
|
-
* - `
|
|
128
|
+
* Which edge source the canvas draws. The two sources are disjoint; a view
|
|
129
|
+
* shows only edges (and their labels) from the selected source.
|
|
130
|
+
* - `graphify`: only graphify-native static edges (`imports`, `contains`, …)
|
|
144
131
|
* - `walkthroughs`: only walkthrough hop edges (`calls`, `feeds`, …)
|
|
145
132
|
*/
|
|
146
|
-
export type SubsystemEdgeView = '
|
|
133
|
+
export type SubsystemEdgeView = 'graphify' | 'walkthroughs';
|
|
147
134
|
|
|
148
135
|
/**
|
|
149
136
|
* Where a display edge came from.
|
|
150
137
|
*
|
|
151
|
-
* - `subsystem` (default): a
|
|
152
|
-
* (`SubsystemRelationType` / `SubsystemWalkthroughMechanism`), colored from
|
|
138
|
+
* - `subsystem` (default): a walkthrough hop mechanism verb, colored from
|
|
153
139
|
* `MECHANISM_COLOR`.
|
|
154
140
|
* - `graphify`: a raw relation read off graphify's static symbol graph
|
|
155
141
|
* (`imports`, `contains`, `re_exports`, …). These are DERIVED, never authored,
|
|
@@ -162,10 +148,8 @@ export type SubsystemEdgeProvenance = 'subsystem' | 'graphify';
|
|
|
162
148
|
* A graphify-native topology edge — a raw graphify relation that has no
|
|
163
149
|
* subsystem mechanism equivalent.
|
|
164
150
|
*
|
|
165
|
-
*
|
|
166
|
-
* a
|
|
167
|
-
* derived from a graphify run and carry graphify's own (open) verb set. They are
|
|
168
|
-
* a display input only — never written back into a `SubsystemModelDocument`.
|
|
151
|
+
* Derived from a graphify run and carry graphify's own (open) verb set. They
|
|
152
|
+
* are a display input only — never written back into a `SubsystemModelDocument`.
|
|
169
153
|
*/
|
|
170
154
|
export interface SubsystemGraphifyRelation {
|
|
171
155
|
id: string;
|
|
@@ -192,8 +176,8 @@ export interface SubsystemGraphifyRelation {
|
|
|
192
176
|
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
193
177
|
export interface SubsystemComponent {
|
|
194
178
|
/**
|
|
195
|
-
* Model-local stable alias, unique per model. Referenced by
|
|
196
|
-
*
|
|
179
|
+
* Model-local stable alias, unique per model. Referenced by walkthrough
|
|
180
|
+
* `from` / `to`; edges point at the alias, not the location.
|
|
197
181
|
* Code identity (for composed multi-model views) lives on
|
|
198
182
|
* `purl` + `file` + `symbol`, not here.
|
|
199
183
|
*/
|
|
@@ -330,19 +314,9 @@ export interface SubsystemComponent {
|
|
|
330
314
|
declarationRef?: SubsystemDeclarationRef;
|
|
331
315
|
}
|
|
332
316
|
|
|
333
|
-
/** A topology relation between components (structural / module / type). */
|
|
334
|
-
export interface SubsystemRelation {
|
|
335
|
-
id: string;
|
|
336
|
-
from: string;
|
|
337
|
-
to: string;
|
|
338
|
-
relationType: SubsystemRelationType;
|
|
339
|
-
/** Concrete file/symbol refs backing the relation. */
|
|
340
|
-
refs?: string[];
|
|
341
|
-
}
|
|
342
|
-
|
|
343
317
|
/**
|
|
344
|
-
* Derived / display graph edge used by renderers. Built from
|
|
345
|
-
* and
|
|
318
|
+
* Derived / display graph edge used by renderers. Built from walkthrough hops
|
|
319
|
+
* (and graphify-native relations) — not authored as its own document field.
|
|
346
320
|
*/
|
|
347
321
|
export interface SubsystemComponentEdge {
|
|
348
322
|
id: string;
|
|
@@ -352,8 +326,8 @@ export interface SubsystemComponentEdge {
|
|
|
352
326
|
* The edge verb. For `provenance: 'subsystem'` (the default) this is a
|
|
353
327
|
* `SubsystemEdgeMechanism`; for `provenance: 'graphify'` it is the raw
|
|
354
328
|
* graphify relation. Typed as `string` because the display edge is a derived
|
|
355
|
-
* structure and graphify's verb set is open — the
|
|
356
|
-
* (`
|
|
329
|
+
* structure and graphify's verb set is open — the walkthrough vocabulary
|
|
330
|
+
* (`SubsystemWalkthroughMechanism`) stays closed.
|
|
357
331
|
*/
|
|
358
332
|
mechanism: string;
|
|
359
333
|
/** Origin of the edge; absent means `'subsystem'`. */
|
|
@@ -414,13 +388,11 @@ export interface SubsystemWalkthrough {
|
|
|
414
388
|
|
|
415
389
|
export interface SubsystemModelDocument {
|
|
416
390
|
components: SubsystemComponent[];
|
|
417
|
-
/** Topology relations (structural / module / type). May be empty. */
|
|
418
|
-
relations: SubsystemRelation[];
|
|
419
391
|
/** Ordered runtime walkthroughs (one per named behavior). */
|
|
420
392
|
walkthroughs?: SubsystemWalkthrough[];
|
|
421
393
|
}
|
|
422
394
|
|
|
423
|
-
/** Stable id for a derived graph edge from a
|
|
395
|
+
/** Stable id for a derived graph edge from a walkthrough hop. */
|
|
424
396
|
export function derivedGraphEdgeId(
|
|
425
397
|
from: string,
|
|
426
398
|
to: string,
|
|
@@ -429,10 +401,6 @@ export function derivedGraphEdgeId(
|
|
|
429
401
|
return `${from}--${mechanism}-->${to}`;
|
|
430
402
|
}
|
|
431
403
|
|
|
432
|
-
/**
|
|
433
|
-
* Build display edges for the graph canvas from topology relations and
|
|
434
|
-
* walkthrough hops (deduped by from/to/mechanism).
|
|
435
|
-
*/
|
|
436
404
|
/** React Flow / canvas edge id for a walkthrough hop. */
|
|
437
405
|
export function walkthroughStepGraphEdgeId(
|
|
438
406
|
step: Pick<SubsystemWalkthroughStep, 'from' | 'to' | 'mechanism'>,
|
|
@@ -472,23 +440,10 @@ export function reorderTargetIndex(boundary: number, from: number): number {
|
|
|
472
440
|
}
|
|
473
441
|
|
|
474
442
|
export function deriveGraphEdges(doc: {
|
|
475
|
-
relations?: readonly SubsystemRelation[];
|
|
476
443
|
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
477
444
|
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
478
445
|
}): SubsystemComponentEdge[] {
|
|
479
446
|
const byId = new Map<string, SubsystemComponentEdge>();
|
|
480
|
-
for (const r of doc.relations ?? []) {
|
|
481
|
-
const id = r.id || derivedGraphEdgeId(r.from, r.to, r.relationType);
|
|
482
|
-
if (!byId.has(id)) {
|
|
483
|
-
byId.set(id, {
|
|
484
|
-
id,
|
|
485
|
-
from: r.from,
|
|
486
|
-
to: r.to,
|
|
487
|
-
mechanism: r.relationType,
|
|
488
|
-
refs: r.refs,
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
447
|
for (const w of doc.walkthroughs ?? []) {
|
|
493
448
|
for (const step of w.steps) {
|
|
494
449
|
const id = derivedGraphEdgeId(step.from, step.to, step.mechanism);
|
|
@@ -522,18 +477,24 @@ export function deriveGraphEdges(doc: {
|
|
|
522
477
|
}
|
|
523
478
|
|
|
524
479
|
/**
|
|
525
|
-
* True when the model has components but no
|
|
526
|
-
*
|
|
480
|
+
* True when the model has components but no graph structure at all: no
|
|
481
|
+
* walkthrough/graphify edges AND no boundary containment (`module` / `process`)
|
|
482
|
+
* to frame. Those snapshots are a catalog of declarations, not a graph.
|
|
483
|
+
*
|
|
484
|
+
* A model that carries `module` / `process` membership still draws frames even
|
|
485
|
+
* with zero edges — that is static topology, so it is not constructs-only.
|
|
527
486
|
*/
|
|
528
487
|
export function isConstructsOnlyModel(doc: {
|
|
529
|
-
components: readonly { alias: string }[];
|
|
530
|
-
relations?: readonly SubsystemRelation[];
|
|
488
|
+
components: readonly { alias: string; module?: string; process?: string }[];
|
|
531
489
|
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
532
490
|
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
533
491
|
}): boolean {
|
|
534
492
|
if (doc.components.length === 0) return false;
|
|
493
|
+
const hasBoundary = doc.components.some(
|
|
494
|
+
(c) => (c.module?.trim() ?? '') !== '' || (c.process?.trim() ?? '') !== '',
|
|
495
|
+
);
|
|
496
|
+
if (hasBoundary) return false;
|
|
535
497
|
return deriveGraphEdges({
|
|
536
|
-
relations: doc.relations,
|
|
537
498
|
walkthroughs: doc.walkthroughs,
|
|
538
499
|
graphifyRelations: doc.graphifyRelations,
|
|
539
500
|
}).length === 0;
|
|
@@ -655,7 +616,7 @@ export type SubsystemGraphNodeType = 'subsystem-component' | 'subsystem-group';
|
|
|
655
616
|
*/
|
|
656
617
|
export interface SubsystemProcessRegion {
|
|
657
618
|
/** Discriminator — which field / identity produced this region. */
|
|
658
|
-
kind: 'process' | 'module' | 'package';
|
|
619
|
+
kind: 'process' | 'module' | 'directory' | 'package';
|
|
659
620
|
/** The `process` / `module` value, or purl repo key for packages. */
|
|
660
621
|
key: string;
|
|
661
622
|
/** Display label for the boundary frame. */
|
|
@@ -678,6 +639,15 @@ export interface BoundaryFrameOptions {
|
|
|
678
639
|
* - `never`: no package frames
|
|
679
640
|
*/
|
|
680
641
|
packageFrames?: 'multi-repo' | 'always' | 'never';
|
|
642
|
+
/**
|
|
643
|
+
* How module frames group.
|
|
644
|
+
* - `exact` (default): one frame per distinct `module` string.
|
|
645
|
+
* - `path`: derive directory frames from the path segments of each
|
|
646
|
+
* component's `module`, and nest module frames under the directories that
|
|
647
|
+
* contain them. Directory frames are interposed between process/package
|
|
648
|
+
* and module frames.
|
|
649
|
+
*/
|
|
650
|
+
moduleNesting?: 'exact' | 'path';
|
|
681
651
|
}
|
|
682
652
|
|
|
683
653
|
/** React Flow id for a process boundary group node. */
|
|
@@ -690,6 +660,23 @@ export function moduleGroupNodeId(moduleKey: string): string {
|
|
|
690
660
|
return `module:${moduleKey}`;
|
|
691
661
|
}
|
|
692
662
|
|
|
663
|
+
/** React Flow id for a directory boundary group node (path-derived nesting). */
|
|
664
|
+
export function directoryGroupNodeId(dirKey: string): string {
|
|
665
|
+
return `directory:${dirKey}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Normalize a module path to forward slashes, no trailing slash or `./`. */
|
|
669
|
+
function normalizeModulePath(p: string): string {
|
|
670
|
+
return p.trim().replace(/\\/g, '/').replace(/\/+$/, '').replace(/^\.\//, '');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Parent directory of a path-like key, or `''` when it has no slash. */
|
|
674
|
+
function parentDirectory(path: string): string {
|
|
675
|
+
const norm = normalizeModulePath(path);
|
|
676
|
+
const idx = norm.lastIndexOf('/');
|
|
677
|
+
return idx <= 0 ? '' : norm.slice(0, idx);
|
|
678
|
+
}
|
|
679
|
+
|
|
693
680
|
export const MODULE_BADGE_INSET = 12;
|
|
694
681
|
const MODULE_BADGE_CHAR_WIDTH = 12.5;
|
|
695
682
|
const MODULE_BADGE_CHROME = 16;
|
|
@@ -765,6 +752,7 @@ export function packageGroupNodeId(packageKey: string): string {
|
|
|
765
752
|
/** React Flow id for a boundary group of any kind. */
|
|
766
753
|
export function boundaryGroupNodeId(region: Pick<SubsystemProcessRegion, 'kind' | 'key'>): string {
|
|
767
754
|
if (region.kind === 'module') return moduleGroupNodeId(region.key);
|
|
755
|
+
if (region.kind === 'directory') return directoryGroupNodeId(region.key);
|
|
768
756
|
if (region.kind === 'package') return packageGroupNodeId(region.key);
|
|
769
757
|
return processGroupNodeId(region.key);
|
|
770
758
|
}
|
|
@@ -887,6 +875,9 @@ export function buildBoundaryLayoutGroups(
|
|
|
887
875
|
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
888
876
|
opts: BoundaryFrameOptions = {},
|
|
889
877
|
): BoundaryLayoutGroup[] {
|
|
878
|
+
if (opts.moduleNesting === 'path') {
|
|
879
|
+
return buildBoundaryLayoutGroupsByPath(doc, opts);
|
|
880
|
+
}
|
|
890
881
|
const showSingletons = opts.showSingletonFrames === true;
|
|
891
882
|
const packageMode = opts.packageFrames ?? 'multi-repo';
|
|
892
883
|
const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
|
|
@@ -1030,6 +1021,261 @@ export function buildBoundaryLayoutGroups(
|
|
|
1030
1021
|
return [...moduleGroups, ...processGroups, ...packageGroups];
|
|
1031
1022
|
}
|
|
1032
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* Path-nesting variant of {@link buildBoundaryLayoutGroups}.
|
|
1026
|
+
*
|
|
1027
|
+
* Adds directory frames derived from each component's `module` path segments
|
|
1028
|
+
* (`src/session/transcript.ts` → `src` → `src/session` → module frame) and
|
|
1029
|
+
* nests module frames under the deepest directory that contains them. Directory
|
|
1030
|
+
* frames are kept when they hold 2+ leaves (or `showSingletonFrames`). Process
|
|
1031
|
+
* and package frames nest above directories exactly as before.
|
|
1032
|
+
*
|
|
1033
|
+
* Module frames are unchanged (one per exact `module` string); leaves still
|
|
1034
|
+
* parent to their module frame in `convertSubsystemToNodes`.
|
|
1035
|
+
*/
|
|
1036
|
+
export function buildBoundaryLayoutGroupsByPath(
|
|
1037
|
+
doc: Pick<SubsystemModelDocument, 'components'>,
|
|
1038
|
+
opts: BoundaryFrameOptions = {},
|
|
1039
|
+
): BoundaryLayoutGroup[] {
|
|
1040
|
+
const showSingletons = opts.showSingletonFrames === true;
|
|
1041
|
+
const packageMode = opts.packageFrames ?? 'multi-repo';
|
|
1042
|
+
const byAlias = new Map(doc.components.map((c) => [c.alias, c]));
|
|
1043
|
+
|
|
1044
|
+
const allPackages = getSubsystemPackageRegions(doc);
|
|
1045
|
+
const packageEligible =
|
|
1046
|
+
packageMode === 'always'
|
|
1047
|
+
? true
|
|
1048
|
+
: packageMode === 'never'
|
|
1049
|
+
? false
|
|
1050
|
+
: allPackages.length >= 2;
|
|
1051
|
+
const packages = packageEligible
|
|
1052
|
+
? allPackages.filter((r) => keepRegion(r, showSingletons))
|
|
1053
|
+
: [];
|
|
1054
|
+
const keptPackageKeys = new Set(packages.map((r) => r.key));
|
|
1055
|
+
|
|
1056
|
+
const processes = getSubsystemRegions(doc).filter((r) =>
|
|
1057
|
+
keepRegion(r, showSingletons),
|
|
1058
|
+
);
|
|
1059
|
+
const keptProcessKeys = new Set(processes.map((r) => r.key));
|
|
1060
|
+
|
|
1061
|
+
const modules = getSubsystemModuleRegions(doc).filter((r) =>
|
|
1062
|
+
keepRegion(r, showSingletons),
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
// Directory regions: every ancestor directory of a component's module path,
|
|
1066
|
+
// with the union of all aliases beneath it.
|
|
1067
|
+
const dirMembers = new Map<string, Set<string>>();
|
|
1068
|
+
for (const c of doc.components) {
|
|
1069
|
+
const m = c.module?.trim();
|
|
1070
|
+
if (!m) continue;
|
|
1071
|
+
let d = parentDirectory(m);
|
|
1072
|
+
while (d) {
|
|
1073
|
+
const set = dirMembers.get(d) ?? new Set<string>();
|
|
1074
|
+
set.add(c.alias);
|
|
1075
|
+
dirMembers.set(d, set);
|
|
1076
|
+
d = parentDirectory(d);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const keptDirKeys = new Set<string>();
|
|
1080
|
+
for (const [d, members] of dirMembers) {
|
|
1081
|
+
if (members.size >= 2 || showSingletons) keptDirKeys.add(d);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Files sitting directly in each directory (drives chain compaction below).
|
|
1085
|
+
const directFileCount = new Map<string, number>();
|
|
1086
|
+
for (const c of doc.components) {
|
|
1087
|
+
const m = c.module?.trim();
|
|
1088
|
+
if (!m) continue;
|
|
1089
|
+
const d = parentDirectory(m);
|
|
1090
|
+
if (d) directFileCount.set(d, (directFileCount.get(d) ?? 0) + 1);
|
|
1091
|
+
}
|
|
1092
|
+
// Compact single-child directory chains, mirroring the file tree: a directory
|
|
1093
|
+
// whose only child is a single subdirectory (and which holds no files
|
|
1094
|
+
// directly) is merged into it. `app` → `app/book` renders one `app/book`
|
|
1095
|
+
// frame, not `app` wrapping `book`.
|
|
1096
|
+
let compacting = true;
|
|
1097
|
+
while (compacting) {
|
|
1098
|
+
compacting = false;
|
|
1099
|
+
const childDirsOf = new Map<string, string[]>();
|
|
1100
|
+
for (const d of keptDirKeys) {
|
|
1101
|
+
let a = parentDirectory(d);
|
|
1102
|
+
while (a && !keptDirKeys.has(a)) a = parentDirectory(a);
|
|
1103
|
+
if (!a) continue;
|
|
1104
|
+
const list = childDirsOf.get(a) ?? [];
|
|
1105
|
+
list.push(d);
|
|
1106
|
+
childDirsOf.set(a, list);
|
|
1107
|
+
}
|
|
1108
|
+
for (const d of [...keptDirKeys]) {
|
|
1109
|
+
const childDirs = childDirsOf.get(d) ?? [];
|
|
1110
|
+
if (childDirs.length === 1 && (directFileCount.get(d) ?? 0) === 0) {
|
|
1111
|
+
keptDirKeys.delete(d);
|
|
1112
|
+
compacting = true;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const nearestKeptDirAncestor = (dirKey: string): string | undefined => {
|
|
1118
|
+
let p = parentDirectory(dirKey);
|
|
1119
|
+
while (p) {
|
|
1120
|
+
if (keptDirKeys.has(p)) return p;
|
|
1121
|
+
p = parentDirectory(p);
|
|
1122
|
+
}
|
|
1123
|
+
return undefined;
|
|
1124
|
+
};
|
|
1125
|
+
const dirRegions: SubsystemProcessRegion[] = [...keptDirKeys]
|
|
1126
|
+
.sort()
|
|
1127
|
+
.map((key) => {
|
|
1128
|
+
// Label is the path relative to the enclosing kept directory (which
|
|
1129
|
+
// already carries the prefix): `book` under `app`, or the full collapsed
|
|
1130
|
+
// path `app/book` when the chain was compacted.
|
|
1131
|
+
const ancestor = nearestKeptDirAncestor(key);
|
|
1132
|
+
return {
|
|
1133
|
+
kind: 'directory' as const,
|
|
1134
|
+
key,
|
|
1135
|
+
label: ancestor ? key.slice(ancestor.length + 1) : key,
|
|
1136
|
+
memberAliases: [...(dirMembers.get(key) ?? [])],
|
|
1137
|
+
};
|
|
1138
|
+
});
|
|
1139
|
+
const moduleDirParent = (moduleKey: string): string | undefined => {
|
|
1140
|
+
let d = parentDirectory(moduleKey);
|
|
1141
|
+
while (d) {
|
|
1142
|
+
if (keptDirKeys.has(d)) return d;
|
|
1143
|
+
d = parentDirectory(d);
|
|
1144
|
+
}
|
|
1145
|
+
return undefined;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
const sharedProcessParent = (
|
|
1149
|
+
memberAliases: readonly string[],
|
|
1150
|
+
): string | undefined => {
|
|
1151
|
+
const seen = new Set<string>();
|
|
1152
|
+
for (const alias of memberAliases) {
|
|
1153
|
+
const p = byAlias.get(alias)?.process?.trim();
|
|
1154
|
+
if (p) seen.add(p);
|
|
1155
|
+
}
|
|
1156
|
+
if (seen.size !== 1) return undefined;
|
|
1157
|
+
const p = [...seen][0]!;
|
|
1158
|
+
return keptProcessKeys.has(p) ? processGroupNodeId(p) : undefined;
|
|
1159
|
+
};
|
|
1160
|
+
const sharedPackageParent = (
|
|
1161
|
+
memberAliases: readonly string[],
|
|
1162
|
+
): string | undefined => {
|
|
1163
|
+
const seen = new Set<string>();
|
|
1164
|
+
for (const alias of memberAliases) {
|
|
1165
|
+
const c = byAlias.get(alias);
|
|
1166
|
+
const pkg = c ? componentPackageKey(c) : undefined;
|
|
1167
|
+
if (pkg) seen.add(pkg);
|
|
1168
|
+
}
|
|
1169
|
+
if (seen.size !== 1) return undefined;
|
|
1170
|
+
const pkg = [...seen][0]!;
|
|
1171
|
+
return keptPackageKeys.has(pkg) ? packageGroupNodeId(pkg) : undefined;
|
|
1172
|
+
};
|
|
1173
|
+
const frameParent = (memberAliases: readonly string[]): string | undefined =>
|
|
1174
|
+
sharedProcessParent(memberAliases) ?? sharedPackageParent(memberAliases);
|
|
1175
|
+
|
|
1176
|
+
const moduleGroups: BoundaryLayoutGroup[] = modules.map((r) => {
|
|
1177
|
+
const dirParent = moduleDirParent(r.key);
|
|
1178
|
+
const parentId = dirParent
|
|
1179
|
+
? directoryGroupNodeId(dirParent)
|
|
1180
|
+
: frameParent(r.memberAliases);
|
|
1181
|
+
// Relative label when nested: `app/book/actions.ts` under `app/book`
|
|
1182
|
+
// reads as `actions.ts`. At the root the full module path stays.
|
|
1183
|
+
const label = dirParent ? r.key.slice(dirParent.length + 1) : r.label;
|
|
1184
|
+
return {
|
|
1185
|
+
id: moduleGroupNodeId(r.key),
|
|
1186
|
+
memberAliases: [...r.memberAliases],
|
|
1187
|
+
parentId,
|
|
1188
|
+
region: { ...r, label },
|
|
1189
|
+
};
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const dirGroupByKey = new Map(dirRegions.map((r) => [r.key, r]));
|
|
1193
|
+
const moduleRegionByKey = new Map(modules.map((r) => [r.key, r]));
|
|
1194
|
+
const dirGroups: BoundaryLayoutGroup[] = dirRegions.map((r) => {
|
|
1195
|
+
const id = directoryGroupNodeId(r.key);
|
|
1196
|
+
const ancestor = nearestKeptDirAncestor(r.key);
|
|
1197
|
+
const parentId = ancestor
|
|
1198
|
+
? directoryGroupNodeId(ancestor)
|
|
1199
|
+
: frameParent(r.memberAliases);
|
|
1200
|
+
// Immediate children: subdirectories and module frames parented here.
|
|
1201
|
+
// Only child *group ids* go in the layout member list — a group's
|
|
1202
|
+
// descendant leaves belong to its child groups alone, or ELK sees the
|
|
1203
|
+
// same leaf in two compound parents and rejects the layout.
|
|
1204
|
+
const childDirKeys = dirRegions
|
|
1205
|
+
.filter((d) => nearestKeptDirAncestor(d.key) === r.key)
|
|
1206
|
+
.map((d) => d.key);
|
|
1207
|
+
const childModuleKeys = modules
|
|
1208
|
+
.filter((m) => moduleDirParent(m.key) === r.key)
|
|
1209
|
+
.map((m) => m.key);
|
|
1210
|
+
const claimed = new Set<string>();
|
|
1211
|
+
for (const dk of childDirKeys) {
|
|
1212
|
+
for (const a of dirGroupByKey.get(dk)?.memberAliases ?? []) claimed.add(a);
|
|
1213
|
+
}
|
|
1214
|
+
for (const mk of childModuleKeys) {
|
|
1215
|
+
for (const a of moduleRegionByKey.get(mk)?.memberAliases ?? []) claimed.add(a);
|
|
1216
|
+
}
|
|
1217
|
+
const directLeaves = r.memberAliases.filter((a) => !claimed.has(a));
|
|
1218
|
+
return {
|
|
1219
|
+
id,
|
|
1220
|
+
memberAliases: [
|
|
1221
|
+
...childDirKeys.map(directoryGroupNodeId),
|
|
1222
|
+
...childModuleKeys.map(moduleGroupNodeId),
|
|
1223
|
+
...directLeaves,
|
|
1224
|
+
],
|
|
1225
|
+
parentId,
|
|
1226
|
+
region: r,
|
|
1227
|
+
};
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
const frameGroups = [...dirGroups, ...moduleGroups];
|
|
1231
|
+
|
|
1232
|
+
// Aliases owned by a module frame (singleton or not) must not become direct
|
|
1233
|
+
// leaves of an enclosing process/package frame.
|
|
1234
|
+
const moduleMemberAliases = new Set<string>();
|
|
1235
|
+
for (const r of modules) for (const a of r.memberAliases) moduleMemberAliases.add(a);
|
|
1236
|
+
|
|
1237
|
+
const processGroups: BoundaryLayoutGroup[] = processes.map((r) => {
|
|
1238
|
+
const processId = processGroupNodeId(r.key);
|
|
1239
|
+
const nested = frameGroups.filter((g) => g.parentId === processId);
|
|
1240
|
+
const claimed = new Set<string>();
|
|
1241
|
+
for (const g of nested) for (const a of g.region.memberAliases) claimed.add(a);
|
|
1242
|
+
const directLeaves = r.memberAliases.filter(
|
|
1243
|
+
(alias) => !claimed.has(alias) && !moduleMemberAliases.has(alias),
|
|
1244
|
+
);
|
|
1245
|
+
return {
|
|
1246
|
+
id: processId,
|
|
1247
|
+
memberAliases: [...nested.map((g) => g.id), ...directLeaves],
|
|
1248
|
+
parentId: sharedPackageParent(r.memberAliases),
|
|
1249
|
+
region: r,
|
|
1250
|
+
};
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const processMemberAliases = new Set<string>();
|
|
1254
|
+
for (const r of processes) for (const a of r.memberAliases) processMemberAliases.add(a);
|
|
1255
|
+
|
|
1256
|
+
const packageGroups: BoundaryLayoutGroup[] = packages.map((r) => {
|
|
1257
|
+
const packageId = packageGroupNodeId(r.key);
|
|
1258
|
+
const nested = [...processGroups, ...frameGroups].filter(
|
|
1259
|
+
(g) => g.parentId === packageId,
|
|
1260
|
+
);
|
|
1261
|
+
const claimed = new Set<string>();
|
|
1262
|
+
for (const g of nested) for (const a of g.region.memberAliases) claimed.add(a);
|
|
1263
|
+
const directLeaves = r.memberAliases.filter(
|
|
1264
|
+
(alias) =>
|
|
1265
|
+
!claimed.has(alias) &&
|
|
1266
|
+
!moduleMemberAliases.has(alias) &&
|
|
1267
|
+
!processMemberAliases.has(alias),
|
|
1268
|
+
);
|
|
1269
|
+
return {
|
|
1270
|
+
id: packageId,
|
|
1271
|
+
memberAliases: [...nested.map((g) => g.id), ...directLeaves],
|
|
1272
|
+
region: r,
|
|
1273
|
+
};
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
return [...frameGroups, ...processGroups, ...packageGroups];
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1033
1279
|
export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
1034
1280
|
component: SubsystemComponent;
|
|
1035
1281
|
/** Set while a file is open in the drawer: true if this node's component
|
|
@@ -1131,19 +1377,6 @@ export interface SubsystemGraphEdgeData extends Record<string, unknown> {
|
|
|
1131
1377
|
|
|
1132
1378
|
export type SubsystemGraphEdge = Edge<SubsystemGraphEdgeData>;
|
|
1133
1379
|
|
|
1134
|
-
/**
|
|
1135
|
-
* Runtime vocabulary of relation types — mirrors `SubsystemRelationType`.
|
|
1136
|
-
* Used to split derived display edges into their relation vs walkthrough
|
|
1137
|
-
* source (the two unions are disjoint).
|
|
1138
|
-
*/
|
|
1139
|
-
export const SUBSYSTEM_RELATION_TYPES = [
|
|
1140
|
-
'extends',
|
|
1141
|
-
'inherits',
|
|
1142
|
-
'implements',
|
|
1143
|
-
'mixes_in',
|
|
1144
|
-
'method',
|
|
1145
|
-
] as const satisfies readonly SubsystemRelationType[];
|
|
1146
|
-
|
|
1147
1380
|
/** Runtime vocabulary of walkthrough hop mechanisms — mirrors `SubsystemWalkthroughMechanism`. */
|
|
1148
1381
|
export const SUBSYSTEM_WALKTHROUGH_MECHANISMS = [
|
|
1149
1382
|
'calls',
|
|
@@ -1156,18 +1389,10 @@ export const SUBSYSTEM_WALKTHROUGH_MECHANISMS = [
|
|
|
1156
1389
|
'registers-into',
|
|
1157
1390
|
] as const satisfies readonly SubsystemWalkthroughMechanism[];
|
|
1158
1391
|
|
|
1159
|
-
const RELATION_TYPE_SET: ReadonlySet<string> = new Set(SUBSYSTEM_RELATION_TYPES);
|
|
1160
1392
|
const WALKTHROUGH_MECHANISM_SET: ReadonlySet<string> = new Set(
|
|
1161
1393
|
SUBSYSTEM_WALKTHROUGH_MECHANISMS,
|
|
1162
1394
|
);
|
|
1163
1395
|
|
|
1164
|
-
/** True when a mechanism belongs to the topology relation vocabulary. */
|
|
1165
|
-
export function isRelationMechanism(
|
|
1166
|
-
mechanism: string,
|
|
1167
|
-
): mechanism is SubsystemRelationType {
|
|
1168
|
-
return RELATION_TYPE_SET.has(mechanism);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
1396
|
/** True when a mechanism belongs to the walkthrough hop vocabulary. */
|
|
1172
1397
|
export function isWalkthroughMechanism(
|
|
1173
1398
|
mechanism: string,
|
|
@@ -1177,12 +1402,7 @@ export function isWalkthroughMechanism(
|
|
|
1177
1402
|
|
|
1178
1403
|
export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
1179
1404
|
calls: '#22c55e', // green
|
|
1180
|
-
extends: '#b48ead', // purple
|
|
1181
|
-
inherits: '#9b6fd0', // purple
|
|
1182
|
-
implements: '#c586c0', // magenta
|
|
1183
|
-
mixes_in: '#d474a8', // pink-magenta
|
|
1184
1405
|
uses: '#e3b341', // gold
|
|
1185
|
-
method: '#c586c0', // magenta
|
|
1186
1406
|
feeds: '#4ec9b0', // teal — data-flow into a processor
|
|
1187
1407
|
produces: '#a78bfa', // violet — emits an output type
|
|
1188
1408
|
writes: '#e8853a', // orange — mutates retained state
|
|
@@ -1193,12 +1413,7 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
1193
1413
|
|
|
1194
1414
|
export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed' | 'dotted'> = {
|
|
1195
1415
|
calls: 'solid',
|
|
1196
|
-
extends: 'dashed',
|
|
1197
|
-
inherits: 'dashed',
|
|
1198
|
-
implements: 'dashed',
|
|
1199
|
-
mixes_in: 'dashed',
|
|
1200
1416
|
uses: 'solid',
|
|
1201
|
-
method: 'solid',
|
|
1202
1417
|
feeds: 'solid',
|
|
1203
1418
|
produces: 'solid',
|
|
1204
1419
|
writes: 'solid',
|
|
@@ -1363,12 +1578,7 @@ export function boundaryFill(color: string, alpha = '1f'): string {
|
|
|
1363
1578
|
* directly verifiable" styling of edge labels. */
|
|
1364
1579
|
export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][] = [
|
|
1365
1580
|
['calls', 'function/method call (call graph edge)', true],
|
|
1366
|
-
['extends', 'class inheritance', true],
|
|
1367
|
-
['inherits', 'class inheritance', true],
|
|
1368
|
-
['implements', 'implements interface / protocol', true],
|
|
1369
|
-
['mixes_in', 'applies mixin', true],
|
|
1370
1581
|
['uses', 'general dependency (import, call, or reference)', false],
|
|
1371
|
-
['method', 'structural: has method / member', true],
|
|
1372
1582
|
['feeds', 'data flow: output feeds into input', false],
|
|
1373
1583
|
['produces', 'data flow: produces / outputs', false],
|
|
1374
1584
|
['writes', 'state access: mutates retained state', true],
|
|
@@ -1377,6 +1587,30 @@ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][]
|
|
|
1377
1587
|
['registers-into', 'registration pattern', false],
|
|
1378
1588
|
];
|
|
1379
1589
|
|
|
1590
|
+
/**
|
|
1591
|
+
* Single border/badge color for directory (folder) frames. Folders read as one
|
|
1592
|
+
* kind of container, so they share a neutral hue rather than hashing per-path
|
|
1593
|
+
* like modules / processes / packages. Host `boundaryColors` still overrides.
|
|
1594
|
+
*/
|
|
1595
|
+
export const FOLDER_FRAME_COLOR = '#7aa2d4';
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* True when a boundary frame names a folder. `directory` frames always are.
|
|
1599
|
+
* A `module` frame is a folder when its key looks like a directory path — the
|
|
1600
|
+
* last segment has no file extension (the model has no explicit file/folder
|
|
1601
|
+
* flag, so this is a heuristic; `Dockerfile`-style extensionless files read as
|
|
1602
|
+
* folders).
|
|
1603
|
+
*/
|
|
1604
|
+
export function isFolderFrame(
|
|
1605
|
+
region: { kind: string; key: string } | undefined,
|
|
1606
|
+
): boolean {
|
|
1607
|
+
if (!region) return false;
|
|
1608
|
+
if (region.kind === 'directory') return true;
|
|
1609
|
+
if (region.kind !== 'module') return false;
|
|
1610
|
+
const last = region.key.split('/').pop() ?? '';
|
|
1611
|
+
return last !== '' && !last.includes('.');
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1380
1614
|
/** Package color palette (derived deterministically from the package name). */
|
|
1381
1615
|
export function packageColor(name: string): string {
|
|
1382
1616
|
const palette = [
|
|
@@ -1733,7 +1967,7 @@ export function convertSubsystemToEdges(
|
|
|
1733
1967
|
|
|
1734
1968
|
/** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
|
|
1735
1969
|
export function subsystemGraphLayoutKey(
|
|
1736
|
-
doc: Pick<SubsystemModelDocument, 'components' | '
|
|
1970
|
+
doc: Pick<SubsystemModelDocument, 'components' | 'walkthroughs'> & {
|
|
1737
1971
|
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
1738
1972
|
},
|
|
1739
1973
|
): string {
|
|
@@ -1792,10 +2026,15 @@ export async function buildSubsystemGraph(
|
|
|
1792
2026
|
measuredHeights,
|
|
1793
2027
|
showSingletonFrames,
|
|
1794
2028
|
packageFrames,
|
|
2029
|
+
moduleNesting,
|
|
1795
2030
|
graphifyRelations,
|
|
1796
2031
|
orderByLine = false,
|
|
1797
2032
|
} = opts;
|
|
1798
|
-
const frameOpts: BoundaryFrameOptions = {
|
|
2033
|
+
const frameOpts: BoundaryFrameOptions = {
|
|
2034
|
+
showSingletonFrames,
|
|
2035
|
+
packageFrames,
|
|
2036
|
+
moduleNesting,
|
|
2037
|
+
};
|
|
1799
2038
|
const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
|
|
1800
2039
|
const edges = convertSubsystemToEdges(doc, graphifyRelations);
|
|
1801
2040
|
// Nested boundary tree: package → process → module → leaves.
|
|
@@ -1887,7 +2126,12 @@ export async function buildSubsystemGraph(
|
|
|
1887
2126
|
try {
|
|
1888
2127
|
const result = await computeElkLayout(nodes, edges, {
|
|
1889
2128
|
routingStyle: 'orthogonal',
|
|
1890
|
-
|
|
2129
|
+
// Edge-driven graphs flow left-to-right. With no edges (pure
|
|
2130
|
+
// containment, e.g. the static-topology layer) a RIGHT layout stacks
|
|
2131
|
+
// disconnected top-level frames in one vertical column; DOWN makes the
|
|
2132
|
+
// layered pass place those sibling frames along the horizontal axis
|
|
2133
|
+
// while each frame's members stack vertically.
|
|
2134
|
+
direction: edges.length > 0 ? 'RIGHT' : 'DOWN',
|
|
1891
2135
|
nodeSpacing: 60,
|
|
1892
2136
|
edgeSpacing: 30,
|
|
1893
2137
|
edgeNodeSpacing: 60,
|
|
@@ -1908,7 +2152,7 @@ export async function buildSubsystemGraph(
|
|
|
1908
2152
|
// room for the collapsed badge. Processes/packages never collapse:
|
|
1909
2153
|
// size the frame to hold the full centered label plus padding.
|
|
1910
2154
|
minWidth:
|
|
1911
|
-
g.region.kind === 'module'
|
|
2155
|
+
g.region.kind === 'module' || g.region.kind === 'directory'
|
|
1912
2156
|
? moduleMinWidthForBadge(g.region.label)
|
|
1913
2157
|
: boundaryMinWidthForBadge(g.region.label),
|
|
1914
2158
|
})),
|
|
@@ -1973,7 +2217,7 @@ export async function buildSubsystemGraph(
|
|
|
1973
2217
|
position: { x: 0, y: 0 },
|
|
1974
2218
|
width: Math.max(
|
|
1975
2219
|
400,
|
|
1976
|
-
g.region.kind === 'module'
|
|
2220
|
+
g.region.kind === 'module' || g.region.kind === 'directory'
|
|
1977
2221
|
? moduleMinWidthForBadge(g.region.label)
|
|
1978
2222
|
: boundaryMinWidthForBadge(g.region.label),
|
|
1979
2223
|
),
|