@principal-ai/subsystems-react 0.51.0 → 0.51.2
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 +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +44 -21
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts +493 -0
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts.map +1 -0
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js +329 -0
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js.map +1 -0
- package/dist/subsystem/C4Graph.d.ts +2 -2
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +25 -16
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +85 -35
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +112 -83
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +20 -21
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +12 -3
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +85 -7
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/c4.d.ts +273 -0
- package/dist/subsystem/c4.d.ts.map +1 -0
- package/dist/subsystem/c4.js +322 -0
- package/dist/subsystem/c4.js.map +1 -0
- package/dist/subsystem/c4Evidence.d.ts +143 -0
- package/dist/subsystem/c4Evidence.d.ts.map +1 -0
- package/dist/subsystem/c4Evidence.js +240 -0
- package/dist/subsystem/c4Evidence.js.map +1 -0
- package/dist/subsystem/model.d.ts +2 -6
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/package.json +5 -4
- package/src/index.ts +26 -28
- package/src/pierre/PierreTrailCodeView.tsx +49 -21
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +171 -80
- package/src/stories/Subsystem/C4Graph/ProposedElements.stories.tsx +293 -0
- package/src/stories/Subsystem/C4Graph/c4ProposedElements.fixture.ts +366 -0
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +169 -3
- package/src/subsystem/C4Graph.tsx +39 -24
- package/src/subsystem/C4NodeCard.test.ts +204 -62
- package/src/subsystem/C4NodeCard.tsx +146 -101
- package/src/subsystem/FileDrawer.tsx +32 -34
- package/src/subsystem/IssueList.tsx +13 -2
- package/src/subsystem/SubsystemComponentGraph.tsx +98 -7
- package/src/subsystem/c4.test.ts +573 -0
- package/src/subsystem/c4.ts +559 -0
- package/src/subsystem/c4Evidence.test.ts +386 -0
- package/src/subsystem/c4Evidence.ts +329 -0
- package/src/subsystem/model.ts +2 -6
- package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts +0 -95
- package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js +0 -340
- package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js.map +0 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts +0 -7
- package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js +0 -7335
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +0 -1
- package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts +0 -2
- package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/C4Graph/generateFixture.js +0 -145
- package/dist/stories/Subsystem/C4Graph/generateFixture.js.map +0 -1
- package/dist/subsystem/c4Associations.d.ts +0 -186
- package/dist/subsystem/c4Associations.d.ts.map +0 -1
- package/dist/subsystem/c4Associations.js +0 -284
- package/dist/subsystem/c4Associations.js.map +0 -1
- package/dist/subsystem/toC4.d.ts +0 -214
- package/dist/subsystem/toC4.d.ts.map +0 -1
- package/dist/subsystem/toC4.js +0 -282
- package/dist/subsystem/toC4.js.map +0 -1
- package/src/stories/Subsystem/C4Graph/C4Associations.stories.tsx +0 -273
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +0 -101
- package/src/stories/Subsystem/C4Graph/c4Associations.fixture.ts +0 -344
- package/src/stories/Subsystem/C4Graph/c4Fixture.ts +0 -7339
- package/src/stories/Subsystem/C4Graph/generateFixture.ts +0 -161
- package/src/subsystem/c4Associations.test.ts +0 -484
- package/src/subsystem/c4Associations.ts +0 -398
- package/src/subsystem/toC4.test.ts +0 -114
- package/src/subsystem/toC4.ts +0 -478
package/src/subsystem/toC4.ts
DELETED
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* toC4 — project a subsystem-model (or a composition of them) onto C4 levels.
|
|
3
|
-
*
|
|
4
|
-
* The stored document is flat: `components[]` carry `process` (runtime unit),
|
|
5
|
-
* `purl` (repo/package), `construct`, `role`, `module`, plus `trails[]`
|
|
6
|
-
* (dynamic). C4 wants a hierarchy above the
|
|
7
|
-
* component — system → container → component — with externals and actors
|
|
8
|
-
* outside it. This module derives that hierarchy without inventing data:
|
|
9
|
-
*
|
|
10
|
-
* system ← the repo key (purlRepoKey of the first grounded component)
|
|
11
|
-
* container ← component.process (runtime/deployment boundary)
|
|
12
|
-
* component ← a grounded component
|
|
13
|
-
* external ← construct: 'external'
|
|
14
|
-
* actor ← construct: 'custom_entity' (their authored actor/entity)
|
|
15
|
-
*
|
|
16
|
-
* Edges are rolled up to the owner of each endpoint at the chosen view, so the
|
|
17
|
-
* container view shows container↔container collaborations and the component
|
|
18
|
-
* view shows component↔component ones. Intra-owner edges (from === to) drop.
|
|
19
|
-
*
|
|
20
|
-
* Pure over the document so it is unit-testable and reusable by any renderer.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { SubsystemComponent, SubsystemModelDocument } from './model';
|
|
24
|
-
|
|
25
|
-
/** C4 levels plus the two "outside the system" concepts. */
|
|
26
|
-
export type C4Kind = 'system' | 'container' | 'component' | 'external' | 'actor';
|
|
27
|
-
|
|
28
|
-
/** Which projection to build: containers (level 2) or components (level 3). */
|
|
29
|
-
export type C4View = 'container' | 'component';
|
|
30
|
-
|
|
31
|
-
/** A C4 element the renderer can draw as a box. */
|
|
32
|
-
export interface C4Node {
|
|
33
|
-
id: string;
|
|
34
|
-
kind: C4Kind;
|
|
35
|
-
label: string;
|
|
36
|
-
/** Compound parent (container → system, component → container). */
|
|
37
|
-
parentId?: string;
|
|
38
|
-
/** Grouping key the node was derived from (process key, purl, alias). */
|
|
39
|
-
key?: string;
|
|
40
|
-
/**
|
|
41
|
-
* Every derived key folded into this node, when one association claims more
|
|
42
|
-
* than one. Present only on a merged box — a node built from a single key
|
|
43
|
-
* leaves it undefined and `key` tells the whole story.
|
|
44
|
-
*/
|
|
45
|
-
sourceKeys?: string[];
|
|
46
|
-
/** Source component aliases rolled into this node. */
|
|
47
|
-
members: string[];
|
|
48
|
-
/** Distinct constructs among the members (for a quick breakdown). */
|
|
49
|
-
constructs: string[];
|
|
50
|
-
/** True when any member is a `store` construct (a data store in C4). */
|
|
51
|
-
isStore: boolean;
|
|
52
|
-
/** Present only in the component view — the backing component. */
|
|
53
|
-
component?: SubsystemComponent;
|
|
54
|
-
/** Source model ids that contributed members, when attribution is supplied. */
|
|
55
|
-
models?: string[];
|
|
56
|
-
/**
|
|
57
|
-
* Confirmed C4 attributes, supplied by a decoration. Absent for the raw
|
|
58
|
-
* derivation — a bare `toC4(doc)` proposes nothing and confirms nothing.
|
|
59
|
-
*/
|
|
60
|
-
decoration?: C4Decoration;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The C4 attributes a reviewer (or an agent they approve) attaches to one
|
|
65
|
-
* derived box. This is the seam between "the models said X" and "a person
|
|
66
|
-
* agreed X is a container with technology Y".
|
|
67
|
-
*/
|
|
68
|
-
export interface C4Decoration {
|
|
69
|
-
/** Identity of the confirmed element. Replaces the derived node id, which is
|
|
70
|
-
* what makes two derived keys able to collapse into one box. */
|
|
71
|
-
id: string;
|
|
72
|
-
/** Display name. Defaults to the grouping key. */
|
|
73
|
-
label?: string;
|
|
74
|
-
/** C4 element type — required by the notation, absent from the document. */
|
|
75
|
-
type?: C4ElementType;
|
|
76
|
-
/** C4 "technology" — required on every container by the notation. */
|
|
77
|
-
technology?: string;
|
|
78
|
-
/** One-line responsibility. Derivable from member `purpose`. */
|
|
79
|
-
description?: string;
|
|
80
|
-
/** Has a person looked at this yet? */
|
|
81
|
-
state?: C4AssociationState;
|
|
82
|
-
/** Override the nesting parent (container id) for a component-level element. */
|
|
83
|
-
parentId?: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* C4 element type — the shape of the box.
|
|
88
|
-
*
|
|
89
|
-
* The document has no field for this: `construct` says what a *declaration* is
|
|
90
|
-
* (function / store / external), not what an *element* is. c4model.com requires
|
|
91
|
-
* a type on every element, so this is something the projection has to supply
|
|
92
|
-
* or the reviewer has to confirm.
|
|
93
|
-
*/
|
|
94
|
-
export type C4ElementType =
|
|
95
|
-
/** a deployable unit — web app, server, CLI, desktop app */
|
|
96
|
-
| 'application'
|
|
97
|
-
/** a database, file store, or blob store */
|
|
98
|
-
| 'data-store'
|
|
99
|
-
/** a queue or topic */
|
|
100
|
-
| 'queue'
|
|
101
|
-
/** a JAR / assembly / npm package: organised *within* a container, not one */
|
|
102
|
-
| 'library'
|
|
103
|
-
/** another software system, outside our boundary */
|
|
104
|
-
| 'software-system'
|
|
105
|
-
/** a human */
|
|
106
|
-
| 'person';
|
|
107
|
-
|
|
108
|
-
/** A compound frame (the system, or a container holding components). */
|
|
109
|
-
export interface C4Group {
|
|
110
|
-
id: string;
|
|
111
|
-
kind: 'system' | 'container';
|
|
112
|
-
label: string;
|
|
113
|
-
parentId?: string;
|
|
114
|
-
/** Member C4 node ids (or nested group ids for the system). */
|
|
115
|
-
memberIds: string[];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** A rolled-up collaboration — a trail flow between owners. */
|
|
119
|
-
export interface C4Edge {
|
|
120
|
-
id: string;
|
|
121
|
-
source: string;
|
|
122
|
-
target: string;
|
|
123
|
-
kind: 'flow';
|
|
124
|
-
label: string;
|
|
125
|
-
mechanisms: string[];
|
|
126
|
-
count: number;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface C4Model {
|
|
130
|
-
view: C4View;
|
|
131
|
-
system: C4Node;
|
|
132
|
-
nodes: C4Node[];
|
|
133
|
-
groups: C4Group[];
|
|
134
|
-
edges: C4Edge[];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Has anyone signed off on this element yet?
|
|
139
|
-
*
|
|
140
|
-
* `proposed` is what an agent writes and a human has not looked at. `accepted`
|
|
141
|
-
* is a human decision. `rejected` means the derivation was wrong and the box
|
|
142
|
-
* should not be drawn at all — the one state that removes a node.
|
|
143
|
-
*/
|
|
144
|
-
export type C4AssociationState = 'proposed' | 'accepted' | 'rejected';
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* One confirmed C4 element: a container or component a person (or an agent a
|
|
148
|
-
* person approved) has said is a real element of the architecture.
|
|
149
|
-
*
|
|
150
|
-
* This lives OUTSIDE the subsystem model document on purpose. The document
|
|
151
|
-
* describes code — what a symbol is and where it runs. Whether two of those
|
|
152
|
-
* runs are one deployable unit is a different, revisable claim about the
|
|
153
|
-
* architecture, and writing it back would mean the next audit had to defend it.
|
|
154
|
-
*/
|
|
155
|
-
export interface C4Association {
|
|
156
|
-
/** Stable identity for this element: `container:<slug>`. Survives renames. */
|
|
157
|
-
id: string;
|
|
158
|
-
/** What C4 level this is. Determines where it is drawn. */
|
|
159
|
-
level: 'container' | 'component';
|
|
160
|
-
/**
|
|
161
|
-
* The derived keys this element absorbs. One key in the common case; more
|
|
162
|
-
* than one when several `process` values are really the same deployable.
|
|
163
|
-
* This is the whole dedup mechanism: merging is claiming several keys under
|
|
164
|
-
* one id, so the rollup below collapses them into a single box for free.
|
|
165
|
-
*/
|
|
166
|
-
sourceKeys: string[];
|
|
167
|
-
label: string;
|
|
168
|
-
/** C4 requires a type on every element. */
|
|
169
|
-
type: C4ElementType;
|
|
170
|
-
/** C4 requires technology on every container. */
|
|
171
|
-
technology?: string;
|
|
172
|
-
/** C4 requires a description on every element. */
|
|
173
|
-
description?: string;
|
|
174
|
-
state: C4AssociationState;
|
|
175
|
-
/** Why the association says what it says — shown in the confirm UI. */
|
|
176
|
-
rationale?: string;
|
|
177
|
-
/** Who or what authored this association. */
|
|
178
|
-
author?: string;
|
|
179
|
-
/** When the state last changed (ISO). */
|
|
180
|
-
decidedAt?: string;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export interface ToC4Options {
|
|
184
|
-
view?: C4View;
|
|
185
|
-
/** Override the system title; defaults to the repo key's owner/name. */
|
|
186
|
-
systemLabel?: string;
|
|
187
|
-
/** Override the repo key used for the system id. */
|
|
188
|
-
repoKey?: string;
|
|
189
|
-
/** Model-id attribution per component alias (e.g. from a merge sidecar). */
|
|
190
|
-
modelsByAlias?: Record<string, string[]>;
|
|
191
|
-
/**
|
|
192
|
-
* Confirmed elements. When supplied, a derived key claimed by an
|
|
193
|
-
* `accepted` association is drawn as that association's box instead of as a
|
|
194
|
-
* raw key, and a `rejected` one is not drawn at all.
|
|
195
|
-
*/
|
|
196
|
-
associations?: readonly C4Association[];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const EXTERNAL_PURL = 'external';
|
|
200
|
-
const UNASSIGNED = '(unassigned)';
|
|
201
|
-
|
|
202
|
-
/** A code component (not an external, not an actor/entity). */
|
|
203
|
-
export function isGroundedComponent(c: SubsystemComponent): boolean {
|
|
204
|
-
return c.construct !== 'external' && c.construct !== 'custom_entity';
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Index confirmed associations by the derived keys they claim.
|
|
209
|
-
*
|
|
210
|
-
* First writer wins on a contested key so the result does not depend on array
|
|
211
|
-
* order, and the loser is reported so the conflict is visible rather than
|
|
212
|
-
* silently dropped.
|
|
213
|
-
*/
|
|
214
|
-
export function indexAssociations(
|
|
215
|
-
associations: readonly C4Association[] | undefined,
|
|
216
|
-
): {
|
|
217
|
-
byKey: Map<string, C4Association>;
|
|
218
|
-
contested: Array<{ key: string; kept: string; dropped: string }>;
|
|
219
|
-
} {
|
|
220
|
-
const byKey = new Map<string, C4Association>();
|
|
221
|
-
const contested: Array<{ key: string; kept: string; dropped: string }> = [];
|
|
222
|
-
if (!associations) return { byKey, contested };
|
|
223
|
-
|
|
224
|
-
// Stable order: accepted before proposed, so a confirmed decision outranks a
|
|
225
|
-
// speculative one for the same key.
|
|
226
|
-
const ordered = [...associations].sort((a, b) => {
|
|
227
|
-
const rank = (s: C4AssociationState) => (s === 'accepted' ? 0 : s === 'proposed' ? 1 : 2);
|
|
228
|
-
return rank(a.state) - rank(b.state) || a.id.localeCompare(b.id);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
for (const a of ordered) {
|
|
232
|
-
for (const key of a.sourceKeys) {
|
|
233
|
-
const existing = byKey.get(key);
|
|
234
|
-
if (existing && existing.id !== a.id) {
|
|
235
|
-
contested.push({ key, kept: existing.id, dropped: a.id });
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
byKey.set(key, a);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return { byKey, contested };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** True when a resolved association means this box should not be drawn. */
|
|
245
|
-
function isSuppressed(a: C4Association | undefined): boolean {
|
|
246
|
-
return a?.state === 'rejected';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** `owner/name` from a purl, trimmed of scheme and fragment. */
|
|
250
|
-
export function labelFromPurl(purl: string): string {
|
|
251
|
-
const base = purl.replace(/^external:/, '').split('#')[0]?.trim() ?? '';
|
|
252
|
-
const parts = base.split('/').filter(Boolean);
|
|
253
|
-
return parts.slice(-2).join('/') || base || purl;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** The repo key the system is derived from: first grounded component's purl. */
|
|
257
|
-
export function deriveRepoKey(doc: SubsystemModelDocument): string | undefined {
|
|
258
|
-
for (const c of doc.components) {
|
|
259
|
-
const p = (c.purl ?? '').split('#')[0]?.trim();
|
|
260
|
-
if (p && p !== EXTERNAL_PURL && !p.startsWith('external:')) return p;
|
|
261
|
-
}
|
|
262
|
-
return undefined;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** Stable identity for an external: its real purl, else the model-local alias. */
|
|
266
|
-
function externalKey(c: SubsystemComponent): string {
|
|
267
|
-
const purl = (c.purl ?? '').trim();
|
|
268
|
-
if (purl && purl !== EXTERNAL_PURL && purl !== 'external:proposed') return purl;
|
|
269
|
-
return `alias:${c.alias}`;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function containerKey(c: SubsystemComponent): string {
|
|
273
|
-
return c.process?.trim() || UNASSIGNED;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Derive the C4 projection of a (possibly composed) subsystem document.
|
|
278
|
-
*
|
|
279
|
-
* @param doc - a portable document, or a composed one with canonical aliases.
|
|
280
|
-
* @param options - view + optional system naming + attribution.
|
|
281
|
-
*/
|
|
282
|
-
export function toC4(doc: SubsystemModelDocument, options: ToC4Options = {}): C4Model {
|
|
283
|
-
const view: C4View = options.view ?? 'container';
|
|
284
|
-
const repoKey = options.repoKey ?? deriveRepoKey(doc) ?? 'system';
|
|
285
|
-
const systemId = `system:${repoKey}`;
|
|
286
|
-
const { byKey: assocByKey } = indexAssociations(options.associations);
|
|
287
|
-
// Id -> association, so a container frame can be labelled from its
|
|
288
|
-
// association even when no node exists for it (component view).
|
|
289
|
-
const assocById = new Map<string, C4Association>();
|
|
290
|
-
for (const a of options.associations ?? []) assocById.set(a.id, a);
|
|
291
|
-
|
|
292
|
-
const system: C4Node = {
|
|
293
|
-
id: systemId,
|
|
294
|
-
kind: 'system',
|
|
295
|
-
label: options.systemLabel ?? labelFromPurl(repoKey),
|
|
296
|
-
key: repoKey,
|
|
297
|
-
members: [],
|
|
298
|
-
constructs: [],
|
|
299
|
-
isStore: false,
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const nodes = new Map<string, C4Node>();
|
|
303
|
-
const ownerOf = new Map<string, string>();
|
|
304
|
-
|
|
305
|
-
const containerNodeId = (key: string) => `container:${key}`;
|
|
306
|
-
const componentNodeId = (alias: string) => `component:${alias}`;
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Resolve one derived container key into the box that should carry it.
|
|
310
|
-
*
|
|
311
|
-
* An accepted association wins: its id becomes the node id, so every key it
|
|
312
|
-
* claims rolls into one node (that is the dedup), and its label / type /
|
|
313
|
-
* technology / description become the node's presentation. A rejected one
|
|
314
|
-
* means the box is not drawn at all. Unclaimed keys keep the raw derived
|
|
315
|
-
* behaviour, so the projection is still useful with zero associations.
|
|
316
|
-
*/
|
|
317
|
-
const resolveContainer = (key: string): { id: string; label: string; assoc: C4Association | undefined } => {
|
|
318
|
-
const assoc = assocByKey.get(key);
|
|
319
|
-
if (assoc && assoc.level === 'container') {
|
|
320
|
-
return { id: assoc.id, label: assoc.label, assoc };
|
|
321
|
-
}
|
|
322
|
-
if (view === 'component') {
|
|
323
|
-
return { id: containerNodeId(key), label: key === UNASSIGNED ? 'Unassigned' : key, assoc: undefined };
|
|
324
|
-
}
|
|
325
|
-
return { id: containerNodeId(key), label: key === UNASSIGNED ? 'Unassigned' : key, assoc: undefined };
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
for (const c of doc.components) {
|
|
329
|
-
let id: string;
|
|
330
|
-
let kind: C4Kind;
|
|
331
|
-
let label: string;
|
|
332
|
-
let parentId: string | undefined;
|
|
333
|
-
let key: string | undefined;
|
|
334
|
-
let assoc: C4Association | undefined;
|
|
335
|
-
|
|
336
|
-
if (c.construct === 'external') {
|
|
337
|
-
key = externalKey(c);
|
|
338
|
-
id = `external:${key}`;
|
|
339
|
-
kind = 'external';
|
|
340
|
-
label = key.startsWith('alias:') ? c.name : labelFromPurl(key);
|
|
341
|
-
} else if (c.construct === 'custom_entity') {
|
|
342
|
-
key = c.alias;
|
|
343
|
-
id = `actor:${c.alias}`;
|
|
344
|
-
kind = 'actor';
|
|
345
|
-
label = c.name;
|
|
346
|
-
} else if (view === 'component') {
|
|
347
|
-
key = containerKey(c);
|
|
348
|
-
const resolved = resolveContainer(key);
|
|
349
|
-
if (isSuppressed(resolved.assoc)) continue;
|
|
350
|
-
id = componentNodeId(c.alias);
|
|
351
|
-
kind = 'component';
|
|
352
|
-
label = c.name;
|
|
353
|
-
parentId = resolved.id;
|
|
354
|
-
assoc = undefined; // the component itself has no association
|
|
355
|
-
} else {
|
|
356
|
-
key = containerKey(c);
|
|
357
|
-
const resolved = resolveContainer(key);
|
|
358
|
-
if (isSuppressed(resolved.assoc)) continue;
|
|
359
|
-
id = resolved.id;
|
|
360
|
-
kind = 'container';
|
|
361
|
-
label = resolved.label;
|
|
362
|
-
parentId = systemId;
|
|
363
|
-
assoc = resolved.assoc;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
ownerOf.set(c.alias, id);
|
|
367
|
-
|
|
368
|
-
let node = nodes.get(id);
|
|
369
|
-
if (!node) {
|
|
370
|
-
node = {
|
|
371
|
-
id,
|
|
372
|
-
kind,
|
|
373
|
-
label,
|
|
374
|
-
parentId,
|
|
375
|
-
key,
|
|
376
|
-
members: [],
|
|
377
|
-
constructs: [],
|
|
378
|
-
isStore: false,
|
|
379
|
-
// A merged box is built from several derived keys. Track them as a
|
|
380
|
-
// set so the confirm UI can show what was folded together — without
|
|
381
|
-
// this, the accumulation would run once per member, not per key.
|
|
382
|
-
...(assoc && assoc.sourceKeys.length > 1 ? { sourceKeys: [...assoc.sourceKeys] } : {}),
|
|
383
|
-
...(kind === 'component' ? { component: c } : {}),
|
|
384
|
-
...(assoc ? { decoration: toDecoration(assoc) } : {}),
|
|
385
|
-
};
|
|
386
|
-
nodes.set(id, node);
|
|
387
|
-
}
|
|
388
|
-
node.members.push(c.alias);
|
|
389
|
-
if (!node.constructs.includes(c.construct)) node.constructs.push(c.construct);
|
|
390
|
-
if (c.construct === 'store') node.isStore = true;
|
|
391
|
-
const models = options.modelsByAlias?.[c.alias];
|
|
392
|
-
if (models && models.length > 0) {
|
|
393
|
-
node.models = [...new Set([...(node.models ?? []), ...models])];
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// --- Groups (compound frames) ------------------------------------------
|
|
398
|
-
const groups: C4Group[] = [];
|
|
399
|
-
const labelForContainerId = (id: string): string =>
|
|
400
|
-
nodes.get(id)?.label ??
|
|
401
|
-
assocById.get(id)?.label ??
|
|
402
|
-
(id.startsWith('container:') ? id.slice('container:'.length) : id);
|
|
403
|
-
if (view === 'container') {
|
|
404
|
-
const containerIds = [...nodes.values()].filter((n) => n.kind === 'container').map((n) => n.id);
|
|
405
|
-
if (containerIds.length > 0) {
|
|
406
|
-
groups.push({ id: systemId, kind: 'system', label: system.label, memberIds: containerIds });
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
const byContainer = new Map<string, string[]>();
|
|
410
|
-
for (const n of nodes.values()) {
|
|
411
|
-
if (n.kind !== 'component' || !n.parentId) continue;
|
|
412
|
-
const list = byContainer.get(n.parentId) ?? [];
|
|
413
|
-
list.push(n.id);
|
|
414
|
-
byContainer.set(n.parentId, list);
|
|
415
|
-
}
|
|
416
|
-
const containerIds: string[] = [];
|
|
417
|
-
for (const [id, memberIds] of byContainer) {
|
|
418
|
-
groups.push({ id, kind: 'container', label: labelForContainerId(id), parentId: systemId, memberIds });
|
|
419
|
-
containerIds.push(id);
|
|
420
|
-
}
|
|
421
|
-
if (containerIds.length > 0) {
|
|
422
|
-
groups.push({ id: systemId, kind: 'system', label: system.label, memberIds: containerIds });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// --- Edges: roll each endpoint up to its owner at this view -------------
|
|
427
|
-
const flowEdges = new Map<string, C4Edge>();
|
|
428
|
-
|
|
429
|
-
const addEdge = (
|
|
430
|
-
bucket: Map<string, C4Edge>,
|
|
431
|
-
source: string | undefined,
|
|
432
|
-
target: string | undefined,
|
|
433
|
-
kind: C4Edge['kind'],
|
|
434
|
-
mechanism: string,
|
|
435
|
-
) => {
|
|
436
|
-
if (!source || !target || source === target) return;
|
|
437
|
-
// An endpoint whose box was rejected has no owner; the edge cannot be
|
|
438
|
-
// drawn, so drop it rather than inventing a dangling arrow.
|
|
439
|
-
if (!nodes.has(source) || !nodes.has(target)) return;
|
|
440
|
-
const key = `${source}\0${target}`;
|
|
441
|
-
let edge = bucket.get(key);
|
|
442
|
-
if (!edge) {
|
|
443
|
-
edge = { id: `${kind}:${key}`, source, target, kind, label: '', mechanisms: [], count: 0 };
|
|
444
|
-
bucket.set(key, edge);
|
|
445
|
-
}
|
|
446
|
-
edge.count += 1;
|
|
447
|
-
if (!edge.mechanisms.includes(mechanism)) edge.mechanisms.push(mechanism);
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
for (const w of doc.trails ?? []) {
|
|
451
|
-
for (const s of w.steps ?? []) {
|
|
452
|
-
addEdge(flowEdges, ownerOf.get(s.from), ownerOf.get(s.to), 'flow', s.mechanism);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const edges = [...flowEdges.values()];
|
|
457
|
-
for (const e of edges) e.label = [...e.mechanisms].sort().join(' + ');
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
view,
|
|
461
|
-
system,
|
|
462
|
-
nodes: [...nodes.values()].filter((n) => n.kind !== 'system'),
|
|
463
|
-
groups,
|
|
464
|
-
edges,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/** Project an association down to the attributes a node carries. */
|
|
469
|
-
function toDecoration(a: C4Association): C4Decoration {
|
|
470
|
-
return {
|
|
471
|
-
id: a.id,
|
|
472
|
-
label: a.label,
|
|
473
|
-
type: a.type,
|
|
474
|
-
technology: a.technology,
|
|
475
|
-
description: a.description,
|
|
476
|
-
state: a.state,
|
|
477
|
-
};
|
|
478
|
-
}
|