@principal-ai/subsystems-react 0.51.4 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +3 -7
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +159 -45
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +26 -26
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +56 -30
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +20 -1
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +42 -19
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/c4.d.ts +48 -49
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js +50 -0
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/c4Evidence.d.ts +88 -84
- package/dist/subsystem/c4Evidence.d.ts.map +1 -1
- package/dist/subsystem/c4Evidence.js +113 -166
- package/dist/subsystem/c4Evidence.js.map +1 -1
- package/dist/subsystem/nodes.js +5 -5
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/techIcons/index.d.ts +5 -0
- package/dist/subsystem/techIcons/index.d.ts.map +1 -1
- package/dist/subsystem/techIcons/index.js +3 -0
- package/dist/subsystem/techIcons/index.js.map +1 -1
- package/dist/utils/edgeLabel.d.ts +15 -1
- package/dist/utils/edgeLabel.d.ts.map +1 -1
- package/dist/utils/edgeLabel.js +25 -1
- package/dist/utils/edgeLabel.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +47 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +71 -7
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -6
- package/src/stories/Subsystem/C4Graph/C4Component.stories.tsx +198 -0
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +42 -6
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +9 -7
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +686 -2
- package/src/subsystem/C4Graph.tsx +242 -82
- package/src/subsystem/C4NodeCard.test.ts +32 -22
- package/src/subsystem/C4NodeCard.tsx +67 -39
- package/src/subsystem/IssueList.tsx +23 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +38 -16
- package/src/subsystem/c4.protocolColor.test.ts +26 -0
- package/src/subsystem/c4.ts +76 -52
- package/src/subsystem/c4Evidence.test.ts +177 -237
- package/src/subsystem/c4Evidence.ts +160 -235
- package/src/subsystem/nodes.group.test.tsx +15 -0
- package/src/subsystem/nodes.tsx +7 -7
- package/src/subsystem/techIcons/index.tsx +8 -0
- package/src/utils/edgeLabel.test.ts +21 -2
- package/src/utils/edgeLabel.ts +29 -1
- package/src/utils/elkLayout.test.ts +16 -0
- package/src/utils/elkLayout.ts +119 -7
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +0 -602
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
import {
|
|
3
|
-
deriveConcerns,
|
|
4
|
-
suggestMerges,
|
|
5
3
|
findUnassigned,
|
|
6
4
|
isRuntimeConstruct,
|
|
7
5
|
suggestContainerKind,
|
|
8
6
|
proposeElements,
|
|
9
|
-
|
|
7
|
+
verificationIssues,
|
|
8
|
+
verifyProcessBoundaries,
|
|
10
9
|
} from './c4Evidence';
|
|
11
10
|
import type { C4MemberView } from './c4Evidence';
|
|
12
11
|
import type { C4Container, C4Element } from './c4';
|
|
@@ -31,136 +30,6 @@ describe('isRuntimeConstruct', () => {
|
|
|
31
30
|
});
|
|
32
31
|
});
|
|
33
32
|
|
|
34
|
-
describe('deriveConcerns — questions, not answers', () => {
|
|
35
|
-
test('flags a lone member as a singleton', () => {
|
|
36
|
-
const out = deriveConcerns('graphify/extract', [member({ alias: 'x' })]);
|
|
37
|
-
expect(out.some((c) => c.kind === 'singleton')).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('flags a group with no runtime member as library-shaped', () => {
|
|
41
|
-
const out = deriveConcerns(
|
|
42
|
-
'subsystems-core',
|
|
43
|
-
[
|
|
44
|
-
member({ alias: 'a', construct: 'interface' }),
|
|
45
|
-
member({ alias: 'b', construct: 'type_alias' }),
|
|
46
|
-
],
|
|
47
|
-
{ frameworks: [] },
|
|
48
|
-
);
|
|
49
|
-
expect(out.some((c) => c.kind === 'library_shaped')).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('a single non-runtime member is also library-shaped', () => {
|
|
53
|
-
const out = deriveConcerns('shared', [member({ alias: 'a', construct: 'type_alias' })]);
|
|
54
|
-
expect(out.some((c) => c.kind === 'library_shaped')).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('flags a runtime container with no technology signal', () => {
|
|
58
|
-
const out = deriveConcerns('p2', [member({ alias: 'c' })], { frameworks: [] });
|
|
59
|
-
expect(out.some((c) => c.kind === 'missing_technology')).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('does not demand technology once a framework is present', () => {
|
|
63
|
-
const out = deriveConcerns(
|
|
64
|
-
'p1',
|
|
65
|
-
[member({ alias: 'a' }), member({ alias: 'b' })],
|
|
66
|
-
{ frameworks: ['react'] },
|
|
67
|
-
);
|
|
68
|
-
expect(out.some((c) => c.kind === 'missing_technology')).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('flags members spanning two repo keys', () => {
|
|
72
|
-
const out = deriveConcerns('p1', [
|
|
73
|
-
member({ alias: 'a' }),
|
|
74
|
-
member({ alias: 'z', purl: 'pkg:github/other/thing#z.ts' }),
|
|
75
|
-
]);
|
|
76
|
-
expect(out.some((c) => c.kind === 'spans_repos')).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('flags a store with no storage kind', () => {
|
|
80
|
-
const out = deriveConcerns('p1', [
|
|
81
|
-
member({ alias: 'a' }),
|
|
82
|
-
member({ alias: 's', construct: 'store', declaration: { kind: 'store', properties: [] } }),
|
|
83
|
-
]);
|
|
84
|
-
expect(out.some((c) => c.kind === 'store_unclassified')).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('does not flag a store that declared its storage', () => {
|
|
88
|
-
const out = deriveConcerns('p1', [
|
|
89
|
-
member({ alias: 'a' }),
|
|
90
|
-
member({ alias: 's', construct: 'store', declaration: { kind: 'store', storage: 'memory' } }),
|
|
91
|
-
]);
|
|
92
|
-
expect(out.some((c) => c.kind === 'store_unclassified')).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('flags an external with no resolvable identity', () => {
|
|
96
|
-
const out = deriveConcerns('p1', [
|
|
97
|
-
member({ alias: 'a' }),
|
|
98
|
-
member({ alias: 'x', construct: 'external', purl: 'external' }),
|
|
99
|
-
]);
|
|
100
|
-
expect(out.some((c) => c.kind === 'external_unidentified')).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('does not flag an external that has a real purl', () => {
|
|
104
|
-
const out = deriveConcerns('p1', [
|
|
105
|
-
member({ alias: 'a' }),
|
|
106
|
-
member({ alias: 'x', construct: 'external', purl: 'pkg:npm/some-pkg' }),
|
|
107
|
-
]);
|
|
108
|
-
expect(out.some((c) => c.kind === 'external_unidentified')).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('a healthy container raises nothing', () => {
|
|
112
|
-
const out = deriveConcerns(
|
|
113
|
-
'subsystems-studio/host',
|
|
114
|
-
[
|
|
115
|
-
member({ alias: 'a' }),
|
|
116
|
-
member({ alias: 'b' }),
|
|
117
|
-
member({ alias: 's', construct: 'store', declaration: { kind: 'store', storage: 'memory' } }),
|
|
118
|
-
],
|
|
119
|
-
{ frameworks: ['bun'] },
|
|
120
|
-
);
|
|
121
|
-
expect(out).toHaveLength(0);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('never returns a decision — only concerns', () => {
|
|
125
|
-
const out = deriveConcerns('graphify/extract', [member({ alias: 'x' })]);
|
|
126
|
-
// Every field describes something to look at, never a verdict.
|
|
127
|
-
for (const c of out) {
|
|
128
|
-
expect(typeof c.kind).toBe('string');
|
|
129
|
-
expect(c.message.length).toBeGreaterThan(0);
|
|
130
|
-
expect(c.message).not.toMatch(/^\s*(is|use)\b/i);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('suggestMerges', () => {
|
|
136
|
-
test('groups keys that share a trailing role', () => {
|
|
137
|
-
const out = suggestMerges([
|
|
138
|
-
{ key: 'subsystems-studio/host', memberCount: 9 },
|
|
139
|
-
{ key: 'principal-studio/host', memberCount: 1 },
|
|
140
|
-
{ key: 'subsystems-studio/renderer', memberCount: 5 },
|
|
141
|
-
]);
|
|
142
|
-
expect(out).toHaveLength(1);
|
|
143
|
-
expect(out[0]!.role).toBe('host');
|
|
144
|
-
expect(out[0]!.keys).toEqual(['principal-studio/host', 'subsystems-studio/host']);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('ignores a bare key with no app/role shape', () => {
|
|
148
|
-
const out = suggestMerges([
|
|
149
|
-
{ key: 'subsystems-react', memberCount: 2 },
|
|
150
|
-
{ key: 'subsystems-core', memberCount: 1 },
|
|
151
|
-
]);
|
|
152
|
-
expect(out).toHaveLength(0);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('frames the finding as a question', () => {
|
|
156
|
-
const out = suggestMerges([
|
|
157
|
-
{ key: 'a/host', memberCount: 1 },
|
|
158
|
-
{ key: 'b/host', memberCount: 1 },
|
|
159
|
-
]);
|
|
160
|
-
expect(out[0]!.reason).toContain('or two units?');
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
33
|
describe('findUnassigned', () => {
|
|
165
34
|
test('returns runtime components that state no process', () => {
|
|
166
35
|
const out = findUnassigned([
|
|
@@ -197,11 +66,9 @@ describe('suggestContainerKind', () => {
|
|
|
197
66
|
|
|
198
67
|
test('there is no library answer, even for a library-shaped group', () => {
|
|
199
68
|
// The kinds are application | data-store. A package of interfaces still
|
|
200
|
-
// comes back `application
|
|
201
|
-
//
|
|
202
|
-
// concept into the vocabulary.
|
|
69
|
+
// comes back `application`; the library verdict is the propose gate's
|
|
70
|
+
// business (no proposal at all), not this function's.
|
|
203
71
|
const members = [member({ alias: 'a', construct: 'interface' })];
|
|
204
|
-
expect(deriveConcerns('x', members).some((c) => c.kind === 'library_shaped')).toBe(true);
|
|
205
72
|
expect(suggestContainerKind(members)).toBe('application');
|
|
206
73
|
});
|
|
207
74
|
});
|
|
@@ -223,164 +90,237 @@ describe('proposeElements', () => {
|
|
|
223
90
|
},
|
|
224
91
|
];
|
|
225
92
|
|
|
226
|
-
test('
|
|
93
|
+
test('a library-shaped boundary gets no proposal — it cannot be discerned into a container', () => {
|
|
94
|
+
// Question 1: code that never runs is not a deployable unit. The concern
|
|
95
|
+
// stands for review; no box is proposed for a person to reject.
|
|
227
96
|
const set = proposeElements({ repoKey: REPO, rollup });
|
|
228
|
-
expect(set.elements).
|
|
229
|
-
expect(set.elements.every((e) => e.state === 'proposed')).toBe(true);
|
|
97
|
+
expect(set.elements.map((e) => e.id)).toEqual(['container:subsystems-studio/host']);
|
|
230
98
|
});
|
|
231
99
|
|
|
232
|
-
test('every proposal is a container
|
|
100
|
+
test('every proposal is a container claiming its key via `process`, never accepted', () => {
|
|
233
101
|
const set = proposeElements({ repoKey: REPO, rollup });
|
|
234
102
|
expect(set.elements.every((e) => e.kind === 'container')).toBe(true);
|
|
103
|
+
expect(set.elements.every((e) => e.state === 'proposed')).toBe(true);
|
|
104
|
+
expect(set.elements.map((e) => (e as C4Container).process)).toEqual(['subsystems-studio/host']);
|
|
235
105
|
});
|
|
236
106
|
|
|
237
|
-
test('
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
107
|
+
test('a key a container already claims is skipped — no duplicates', () => {
|
|
108
|
+
// Question 2: the container exists; proposing another would be the
|
|
109
|
+
// duplicate this module exists to prevent.
|
|
110
|
+
const existing: C4Element[] = [
|
|
111
|
+
{
|
|
112
|
+
kind: 'container',
|
|
113
|
+
containerKind: 'application',
|
|
114
|
+
technology: 'Bun',
|
|
115
|
+
id: 'container:subsystems-studio/host',
|
|
116
|
+
process: 'subsystems-studio/host',
|
|
117
|
+
label: 'studio host',
|
|
118
|
+
state: 'accepted',
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
const set = proposeElements({ repoKey: REPO, rollup, elements: existing });
|
|
122
|
+
expect(set.elements).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('the scaffold fabricates no rationale — that is the proposing agent\'s to write', () => {
|
|
126
|
+
const set = proposeElements({
|
|
127
|
+
repoKey: REPO,
|
|
128
|
+
rollup: [{ key: 'app/worker', frameworks: ['bun'], members: [member({ alias: 'w' })] }],
|
|
129
|
+
});
|
|
130
|
+
expect(set.elements[0]!.rationale).toBeUndefined();
|
|
241
131
|
});
|
|
242
132
|
|
|
243
|
-
test('
|
|
133
|
+
test('proposes strictly one container per key — folding lives in the model, not here', () => {
|
|
134
|
+
// Two keys that are one deployable unit are a model discrepancy: a
|
|
135
|
+
// consolidation proposal rewrites the components' `process` fields first,
|
|
136
|
+
// and proposeElements then naturally proposes one container. The read
|
|
137
|
+
// side never folds.
|
|
244
138
|
const set = proposeElements({
|
|
245
139
|
repoKey: REPO,
|
|
246
140
|
rollup: [
|
|
141
|
+
rollup[0]!,
|
|
247
142
|
{
|
|
248
|
-
key: '
|
|
249
|
-
frameworks: ['
|
|
250
|
-
members: [member({ alias: '
|
|
143
|
+
key: 'principal-studio/host',
|
|
144
|
+
frameworks: ['electrobun'],
|
|
145
|
+
members: [member({ alias: 'z' })],
|
|
251
146
|
},
|
|
252
147
|
],
|
|
253
148
|
});
|
|
254
|
-
expect(set.elements
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test('members are named, not inferred from the key', () => {
|
|
258
|
-
const set = proposeElements({ repoKey: REPO, rollup });
|
|
259
|
-
const host = set.elements.find((e) => e.derivedFrom?.includes('subsystems-studio/host'))!;
|
|
260
|
-
expect(host.members).toEqual(['a', 's']);
|
|
149
|
+
expect(set.elements).toHaveLength(2);
|
|
261
150
|
});
|
|
262
151
|
|
|
263
152
|
test('a key with no framework signal gets an empty technology, not a guess', () => {
|
|
264
|
-
// `technology` is required by the type, and an empty one is reported
|
|
153
|
+
// `technology` is required by the type, and an empty one is reported as a
|
|
265
154
|
// gap. Writing "TypeScript" here would be the derivation this module does
|
|
266
155
|
// not do.
|
|
267
|
-
const set = proposeElements({
|
|
268
|
-
|
|
269
|
-
|
|
156
|
+
const set = proposeElements({
|
|
157
|
+
repoKey: REPO,
|
|
158
|
+
rollup: [{ key: 'app/host', frameworks: [], members: [member({ alias: 'a' })] }],
|
|
159
|
+
});
|
|
160
|
+
expect((set.elements[0]! as C4Container).technology).toBe('');
|
|
270
161
|
});
|
|
271
162
|
|
|
272
163
|
test('the set is sorted by id, so a review list is stable', () => {
|
|
273
|
-
const set = proposeElements({
|
|
274
|
-
|
|
164
|
+
const set = proposeElements({
|
|
165
|
+
repoKey: REPO,
|
|
166
|
+
rollup: [
|
|
167
|
+
{ key: 'b/host', frameworks: ['bun'], members: [member({ alias: 'a' })] },
|
|
168
|
+
{ key: 'a/host', frameworks: ['bun'], members: [member({ alias: 'b' })] },
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
expect(set.elements.map((e) => e.id)).toEqual(['container:a/host', 'container:b/host']);
|
|
275
172
|
});
|
|
276
173
|
});
|
|
277
174
|
|
|
278
|
-
describe('
|
|
279
|
-
function container(over: Partial<C4Container> & { id: string }): C4Container {
|
|
175
|
+
describe('verifyProcessBoundaries', () => {
|
|
176
|
+
function container(over: Partial<C4Container> & { id: string; process: string }): C4Container {
|
|
280
177
|
return {
|
|
281
178
|
kind: 'container',
|
|
282
179
|
containerKind: 'application',
|
|
283
180
|
technology: 'Bun',
|
|
284
|
-
members: [],
|
|
285
181
|
label: over.id,
|
|
286
182
|
state: 'proposed',
|
|
287
183
|
...over,
|
|
288
184
|
};
|
|
289
185
|
}
|
|
290
186
|
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
187
|
+
const rollup = [
|
|
188
|
+
{ key: 'app/host', memberAliases: ['a', 'b'] },
|
|
189
|
+
{ key: 'app/store', memberAliases: ['s'] },
|
|
190
|
+
{ key: 'app/orphan' },
|
|
295
191
|
];
|
|
296
192
|
|
|
297
|
-
test('
|
|
298
|
-
const out =
|
|
299
|
-
expect(out).
|
|
300
|
-
|
|
301
|
-
expect(host.members.sort()).toEqual(['a', 'b', 'c', 'z']);
|
|
302
|
-
expect(host.derivedFrom!.sort()).toEqual(['principal-studio/host', 'subsystems-studio/host']);
|
|
193
|
+
test('a key no container claims is unassigned', () => {
|
|
194
|
+
const out = verifyProcessBoundaries({ rollup: [rollup[0]!], elements: [] });
|
|
195
|
+
expect(out[0]).toMatchObject({ key: 'app/host', status: 'unassigned' });
|
|
196
|
+
expect(out[0]!.element).toBeUndefined();
|
|
303
197
|
});
|
|
304
198
|
|
|
305
|
-
test('
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
expect(out
|
|
309
|
-
expect(out.some((e) => e.id === 'container:small')).toBe(false);
|
|
199
|
+
test('memberAliases pass through for the confirm UI', () => {
|
|
200
|
+
const out = verifyProcessBoundaries({ rollup, elements: [] });
|
|
201
|
+
expect(out[0]!.memberAliases).toEqual(['a', 'b']);
|
|
202
|
+
expect(out[2]!.memberAliases).toEqual([]);
|
|
310
203
|
});
|
|
311
204
|
|
|
312
|
-
test('a
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
[
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
205
|
+
test('a proposal assigns the boundary and reads as proposed', () => {
|
|
206
|
+
const set = proposeElements({
|
|
207
|
+
repoKey: REPO,
|
|
208
|
+
rollup: [
|
|
209
|
+
{ key: 'app/host', frameworks: ['bun'], members: [member({ alias: 'a' }), member({ alias: 'b' })] },
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
const out = verifyProcessBoundaries({
|
|
213
|
+
rollup: [{ key: 'app/host', memberAliases: ['a', 'b'] }],
|
|
214
|
+
elements: set.elements,
|
|
215
|
+
});
|
|
216
|
+
expect(out[0]).toMatchObject({ key: 'app/host', status: 'proposed' });
|
|
217
|
+
expect(out[0]!.element!.id).toBe('container:app/host');
|
|
319
218
|
});
|
|
320
219
|
|
|
321
|
-
test('
|
|
322
|
-
const out =
|
|
323
|
-
|
|
220
|
+
test('an accepted container verifies the boundary', () => {
|
|
221
|
+
const out = verifyProcessBoundaries({
|
|
222
|
+
rollup: [rollup[0]!],
|
|
223
|
+
elements: [container({ id: 'container:app/host', process: 'app/host', state: 'accepted' })],
|
|
224
|
+
});
|
|
225
|
+
expect(out[0]).toMatchObject({ key: 'app/host', status: 'verified' });
|
|
324
226
|
});
|
|
325
227
|
|
|
326
|
-
test('a
|
|
327
|
-
|
|
228
|
+
test('a rejected container marks the boundary rejected', () => {
|
|
229
|
+
const out = verifyProcessBoundaries({
|
|
230
|
+
rollup: [rollup[0]!],
|
|
231
|
+
elements: [container({ id: 'container:app/host', process: 'app/host', state: 'rejected' })],
|
|
232
|
+
});
|
|
233
|
+
expect(out[0]).toMatchObject({ key: 'app/host', status: 'rejected' });
|
|
328
234
|
});
|
|
329
235
|
|
|
330
|
-
test('
|
|
331
|
-
|
|
236
|
+
test('a boundary matches its container by key, nothing else', () => {
|
|
237
|
+
// A near-miss key is not a claimant: no redirect, no fold — if the model
|
|
238
|
+
// says two keys are one unit, the model gets rewritten, not the lookup.
|
|
239
|
+
const out = verifyProcessBoundaries({
|
|
240
|
+
rollup: [{ key: 'legacy/host', memberAliases: ['z'] }],
|
|
241
|
+
elements: [container({ id: 'container:app/host', process: 'app/host', state: 'accepted' })],
|
|
242
|
+
});
|
|
243
|
+
expect(out[0]).toMatchObject({ key: 'legacy/host', status: 'unassigned' });
|
|
332
244
|
});
|
|
333
245
|
|
|
334
|
-
test('
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
members: ['b'],
|
|
344
|
-
label: 'component:b',
|
|
345
|
-
state: 'proposed',
|
|
346
|
-
derivedFrom: ['y/host'],
|
|
347
|
-
},
|
|
348
|
-
];
|
|
349
|
-
expect(mergeElements(withComponent, ['x/host', 'y/host'])).toHaveLength(2);
|
|
246
|
+
test('accepted outranks rejected when two containers claim one key', () => {
|
|
247
|
+
const out = verifyProcessBoundaries({
|
|
248
|
+
rollup: [rollup[0]!],
|
|
249
|
+
elements: [
|
|
250
|
+
container({ id: 'container:a', process: 'app/host', state: 'rejected' }),
|
|
251
|
+
container({ id: 'container:b', process: 'app/host', state: 'accepted' }),
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
expect(out[0]).toMatchObject({ status: 'verified', element: { id: 'container:b' } });
|
|
350
255
|
});
|
|
351
256
|
|
|
352
|
-
test('
|
|
353
|
-
|
|
354
|
-
|
|
257
|
+
test('a non-container never claims a boundary', () => {
|
|
258
|
+
// Only containers carry a `process` claim; components point at their
|
|
259
|
+
// parent and cannot verify anything.
|
|
260
|
+
const component: C4Element = {
|
|
261
|
+
kind: 'component',
|
|
262
|
+
container: 'container:app/host',
|
|
263
|
+
technology: 'React',
|
|
264
|
+
label: 'component:a',
|
|
265
|
+
state: 'accepted',
|
|
266
|
+
};
|
|
267
|
+
const out = verifyProcessBoundaries({ rollup: [rollup[0]!], elements: [component] });
|
|
268
|
+
expect(out[0]).toMatchObject({ status: 'unassigned' });
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('verificationIssues — statuses as dynamic-topology findings', () => {
|
|
273
|
+
test('a verified boundary reports nothing — absence only', () => {
|
|
274
|
+
const out = verificationIssues([
|
|
275
|
+
{ key: 'app/host', memberAliases: ['a'], status: 'verified' },
|
|
276
|
+
]);
|
|
277
|
+
expect(out).toHaveLength(0);
|
|
355
278
|
});
|
|
356
279
|
|
|
357
|
-
test('is
|
|
358
|
-
const
|
|
359
|
-
|
|
280
|
+
test('an unclaimed boundary is a finding whose fix proposes a container', () => {
|
|
281
|
+
const out = verificationIssues([
|
|
282
|
+
{ key: 'app/worker', memberAliases: ['w'], status: 'unassigned' },
|
|
283
|
+
]);
|
|
284
|
+
expect(out).toHaveLength(1);
|
|
285
|
+
expect(out[0]).toMatchObject({
|
|
286
|
+
id: 'process-verification:app/worker',
|
|
287
|
+
severity: 'info',
|
|
288
|
+
kind: 'boundary_process_unassigned',
|
|
289
|
+
target: { kind: 'process', id: 'app/worker' },
|
|
290
|
+
fix: { label: 'Propose container' },
|
|
291
|
+
});
|
|
360
292
|
});
|
|
361
293
|
|
|
362
|
-
test('
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
294
|
+
test('a proposed boundary awaits the decision; its fix accepts the container', () => {
|
|
295
|
+
const out = verificationIssues([
|
|
296
|
+
{
|
|
297
|
+
key: 'app/renderer',
|
|
298
|
+
memberAliases: ['v'],
|
|
299
|
+
status: 'proposed',
|
|
300
|
+
element: { id: 'container:app/renderer', label: 'Studio renderer' } as C4Element,
|
|
301
|
+
},
|
|
302
|
+
]);
|
|
303
|
+
expect(out[0]).toMatchObject({
|
|
304
|
+
kind: 'boundary_process_proposed',
|
|
305
|
+
fix: { label: 'Accept container' },
|
|
370
306
|
});
|
|
307
|
+
expect(out[0]!.message).toContain('Studio renderer');
|
|
371
308
|
});
|
|
372
309
|
|
|
373
|
-
test('
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
310
|
+
test('a rejected boundary records the declined container and offers no fix', () => {
|
|
311
|
+
const out = verificationIssues([
|
|
312
|
+
{
|
|
313
|
+
key: 'app/worker',
|
|
314
|
+
memberAliases: ['w'],
|
|
315
|
+
status: 'rejected',
|
|
316
|
+
element: { id: 'container:app/worker', label: 'Studio worker' } as C4Element,
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
expect(out[0]).toMatchObject({
|
|
320
|
+
kind: 'boundary_process_rejected',
|
|
321
|
+
target: { kind: 'process', id: 'app/worker' },
|
|
322
|
+
});
|
|
323
|
+
expect(out[0]!.fix).toBeUndefined();
|
|
324
|
+
expect(out[0]!.message).toContain('rejected');
|
|
385
325
|
});
|
|
386
|
-
});
|
|
326
|
+
});
|