@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
|
@@ -7,23 +7,43 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The encoding has one job: make confirmation state legible at a glance. A box
|
|
9
9
|
* nobody has looked at must not read the same as a box a person signed off.
|
|
10
|
-
*
|
|
11
|
-
* surfaced in the order a
|
|
10
|
+
* Kind says what the box is — shape, size, and a written tag — and everything
|
|
11
|
+
* else in C4 notation (technology, description) is surfaced in the order a
|
|
12
|
+
* reader wants it.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
16
|
+
import { Cloud, Component, Container, UserRound } from 'lucide-react';
|
|
17
|
+
import type { LucideIcon } from 'lucide-react';
|
|
15
18
|
import { describeConstructBreakdown } from './model';
|
|
16
|
-
import type { C4Node } from './
|
|
19
|
+
import type { C4Node } from './c4';
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
|
-
* Card box
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* what lets the `needs …` marker sit on its own line instead of clipping.
|
|
22
|
+
* Card box for a container: a wide rectangle. A container carries four things
|
|
23
|
+
* at once (tag, label, type + technology, and the notation gap it still has),
|
|
24
|
+
* and a strip would force the last two to truncate.
|
|
23
25
|
*/
|
|
24
26
|
export const NODE_W = 210;
|
|
25
27
|
export const NODE_H = 138;
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* A component is a proper square — equal sides, and visibly smaller than the
|
|
31
|
+
* container it sits inside. Square-vs-rectangle is the clearest signal that
|
|
32
|
+
* these are different C4 levels, and it survives grayscale and colour-blind
|
|
33
|
+
* reading where a border-weight difference alone does not.
|
|
34
|
+
*/
|
|
35
|
+
export const COMPONENT_SIZE = 132;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-level box size. The graph lays out against these, so a square component
|
|
39
|
+
* and a rectangular container do not collide at a shared nominal width.
|
|
40
|
+
*/
|
|
41
|
+
export function nodeSize(node: C4Node): { width: number; height: number } {
|
|
42
|
+
return node.kind === 'component'
|
|
43
|
+
? { width: COMPONENT_SIZE, height: COMPONENT_SIZE }
|
|
44
|
+
: { width: NODE_W, height: NODE_H };
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
/**
|
|
28
48
|
* Fixed slot heights, so rows line up across every card regardless of which
|
|
29
49
|
* optional fields are present. Without these a card missing its notation-gap
|
|
@@ -36,26 +56,34 @@ type Theme = ReturnType<typeof useTheme>['theme'];
|
|
|
36
56
|
|
|
37
57
|
export interface NodeStyle {
|
|
38
58
|
color: string;
|
|
39
|
-
dash:
|
|
59
|
+
dash: 'solid' | 'dashed' | 'dotted';
|
|
40
60
|
width: number;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
/**
|
|
44
|
-
* Border style
|
|
64
|
+
* Border style for one node: colour, dash and weight, from confirmation state
|
|
65
|
+
* and level.
|
|
66
|
+
*
|
|
67
|
+
* One axis per channel, so nothing is said twice and nothing is contradicted:
|
|
68
|
+
*
|
|
69
|
+
* colour + dash → confirmation state (the primary axis)
|
|
70
|
+
* weight → C4 level, as a grayscale redundancy for the square
|
|
71
|
+
* shape + size → C4 kind (`nodeShape`, `nodeSize`)
|
|
45
72
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
73
|
+
* State owning the dash is deliberate. An earlier build gave the dash to kind
|
|
74
|
+
* and drew proposed boxes solid, quietly contradicting the one thing the
|
|
75
|
+
* confirm UI needs a reader to see. Kind now says itself through shape, and
|
|
76
|
+
* the border says who vouched for it.
|
|
49
77
|
*/
|
|
50
78
|
export function nodeStyle(node: C4Node, theme: Theme, selected = false): NodeStyle {
|
|
51
79
|
const muted = theme.colors.border ?? '#555';
|
|
52
80
|
if (selected) return { color: theme.colors.primary ?? '#5aa', dash: 'solid', width: 3 };
|
|
53
81
|
|
|
54
|
-
// Border
|
|
82
|
+
// Border weight carries the level, so a component stays a component even
|
|
55
83
|
// in grayscale or for a reader who cannot separate the hues.
|
|
56
84
|
const weight = node.kind === 'component' ? 1 : 2;
|
|
57
85
|
|
|
58
|
-
switch (node.
|
|
86
|
+
switch (node.state) {
|
|
59
87
|
case 'accepted':
|
|
60
88
|
// Solid and saturated — part of the architecture now.
|
|
61
89
|
return { color: theme.colors.primary ?? '#4ec9b0', dash: 'solid', width: weight };
|
|
@@ -63,21 +91,8 @@ export function nodeStyle(node: C4Node, theme: Theme, selected = false): NodeSty
|
|
|
63
91
|
// Dashed — an agent asked; a human has not answered.
|
|
64
92
|
return { color: theme.colors.warning ?? '#e8a33a', dash: 'dashed', width: weight };
|
|
65
93
|
case 'rejected':
|
|
66
|
-
// Should never be drawn (
|
|
94
|
+
// Should never be drawn (buildC4Model drops it); belt-and-braces.
|
|
67
95
|
return { color: muted, dash: 'dotted', width: 1 };
|
|
68
|
-
default:
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// No association at all — the raw derivation. Muted, so confirmed boxes
|
|
73
|
-
// read as the stronger signal.
|
|
74
|
-
switch (node.kind) {
|
|
75
|
-
case 'external':
|
|
76
|
-
return { color: theme.colors.warning ?? '#a78bfa', dash: 'solid', width: weight };
|
|
77
|
-
case 'actor':
|
|
78
|
-
return { color: theme.colors.accent ?? theme.colors.info ?? '#e3b341', dash: 'solid', width: weight };
|
|
79
|
-
default:
|
|
80
|
-
return { color: muted, dash: 'solid', width: weight };
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
|
|
@@ -86,12 +101,17 @@ function baseName(path: string): string {
|
|
|
86
101
|
return parts[parts.length - 1] ?? path;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* The kind + technology line under the label.
|
|
106
|
+
*
|
|
107
|
+
* The container kind moved up to the icon row (see `nodeTypeLabel`), so it is
|
|
108
|
+
* not repeated here. Technology is the remaining confirmed attribute; without
|
|
109
|
+
* one we fall back to what the document told us.
|
|
110
|
+
*/
|
|
90
111
|
export function nodeSubtitle(node: C4Node): string {
|
|
91
|
-
|
|
92
|
-
if (d?.type || d?.technology) return [d.type, d.technology].filter(Boolean).join(' · ');
|
|
112
|
+
if (node.technology) return node.technology;
|
|
93
113
|
|
|
94
|
-
// No
|
|
114
|
+
// No stated technology: fall back to what the document told us.
|
|
95
115
|
if (node.kind === 'component') {
|
|
96
116
|
return `${node.component?.construct ?? 'code'}${node.component?.file ? ` · ${baseName(node.component.file)}` : ''}`;
|
|
97
117
|
}
|
|
@@ -100,68 +120,96 @@ export function nodeSubtitle(node: C4Node): string {
|
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* reviewed it; collapsing the two into one label is what made the two
|
|
108
|
-
* indistinguishable. Confirmation is a separate axis (`nodeStateTag`).
|
|
123
|
+
* What the box IS — its C4 kind. An accepted container is still a container;
|
|
124
|
+
* collapsing the two is what once made containers and components unreadable.
|
|
125
|
+
* Confirmation state is a separate axis carried by the border (see
|
|
126
|
+
* `nodeStyle`), not by a word on the card.
|
|
109
127
|
*/
|
|
110
128
|
export function nodeTag(node: C4Node): string {
|
|
111
129
|
return node.kind;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
133
|
+
* The glyph for the C4 kind, shown in place of a word. The kind is already
|
|
134
|
+
* carried by the box shape (see `nodeShape`); the icon lets the top row spend
|
|
135
|
+
* its one piece of text on the container's kind instead of repeating
|
|
136
|
+
* "container".
|
|
117
137
|
*/
|
|
118
|
-
export function
|
|
119
|
-
switch (node.
|
|
120
|
-
case '
|
|
121
|
-
return
|
|
122
|
-
case '
|
|
123
|
-
return
|
|
124
|
-
case '
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
return
|
|
138
|
+
export function nodeKindIcon(node: C4Node): LucideIcon {
|
|
139
|
+
switch (node.kind) {
|
|
140
|
+
case 'container':
|
|
141
|
+
return Container;
|
|
142
|
+
case 'component':
|
|
143
|
+
return Component;
|
|
144
|
+
case 'external-system':
|
|
145
|
+
return Cloud;
|
|
146
|
+
case 'person':
|
|
147
|
+
return UserRound;
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
/**
|
|
132
|
-
*
|
|
152
|
+
* The word beside the kind icon: what sort of container this is — C4's two,
|
|
153
|
+
* `application` or `data-store`. The other three kinds have no such subtype,
|
|
154
|
+
* so they show nothing; the icon and shape are the whole label.
|
|
155
|
+
*/
|
|
156
|
+
export function nodeTypeLabel(node: C4Node): string | undefined {
|
|
157
|
+
return node.kind === 'container' ? node.containerKind : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** How a box is drawn. Shape carries the C4 kind, independent of colour. */
|
|
161
|
+
export type C4ShapeKind = 'rect' | 'square' | 'pill';
|
|
162
|
+
|
|
163
|
+
export interface NodeShape {
|
|
164
|
+
kind: C4ShapeKind;
|
|
165
|
+
/** Corner radius, for the shapes that use one. */
|
|
166
|
+
radius: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Shape cue for the C4 kind, independent of colour and dash — both of which
|
|
171
|
+
* belong to confirmation state.
|
|
133
172
|
*
|
|
134
|
-
* Colour alone is not enough: a container and a component with no association
|
|
135
|
-
* both fall back to the muted border, so they were identical in grayscale.
|
|
136
173
|
* The convention:
|
|
137
174
|
*
|
|
138
|
-
* container
|
|
139
|
-
* component
|
|
140
|
-
* external
|
|
141
|
-
*
|
|
175
|
+
* container wide rectangle, sharp corners — a runtime boundary
|
|
176
|
+
* component square — a part inside one (see `nodeSize`)
|
|
177
|
+
* external system wide rectangle, rounded — someone else's system
|
|
178
|
+
* person pill — not a box at all
|
|
179
|
+
*
|
|
180
|
+
* An external system is separated from a container by radius alone, which is
|
|
181
|
+
* a weak cue. That is deliberate rather than an oversight: the border dash is
|
|
182
|
+
* the only channel that could say "outside our control" loudly, and it is
|
|
183
|
+
* already spent on proposed-vs-accepted. The kind icon says it, and the frame
|
|
184
|
+
* position says it structurally.
|
|
142
185
|
*/
|
|
143
|
-
export function nodeShape(node: C4Node):
|
|
186
|
+
export function nodeShape(node: C4Node): NodeShape {
|
|
144
187
|
switch (node.kind) {
|
|
145
|
-
case '
|
|
146
|
-
return { radius: NODE_H / 2
|
|
147
|
-
case 'external':
|
|
148
|
-
return {
|
|
188
|
+
case 'person':
|
|
189
|
+
return { kind: 'pill', radius: NODE_H / 2 };
|
|
190
|
+
case 'external-system':
|
|
191
|
+
return { kind: 'rect', radius: 14 };
|
|
149
192
|
case 'component':
|
|
150
|
-
return {
|
|
193
|
+
return { kind: 'square', radius: 6 };
|
|
151
194
|
default:
|
|
152
|
-
return {
|
|
195
|
+
return { kind: 'rect', radius: 4 };
|
|
153
196
|
}
|
|
154
197
|
}
|
|
155
198
|
|
|
156
199
|
/**
|
|
157
|
-
* C4 notation
|
|
158
|
-
*
|
|
200
|
+
* The gaps the C4 notation still wants on this element, named rather than left
|
|
201
|
+
* to be noticed.
|
|
202
|
+
*
|
|
203
|
+
* Containers are where the notation is strictest: it requires a technology and
|
|
204
|
+
* a description on every one. A component needs a technology too, but a
|
|
205
|
+
* component drawn without a parent already reports `unknown_container`, and
|
|
206
|
+
* that is the question worth asking first.
|
|
159
207
|
*/
|
|
160
208
|
export function nodeMissing(node: C4Node): string[] {
|
|
161
209
|
if (node.kind !== 'container') return [];
|
|
162
210
|
const missing: string[] = [];
|
|
163
|
-
if (!node.
|
|
164
|
-
if (!node.
|
|
211
|
+
if (!node.technology) missing.push('technology');
|
|
212
|
+
if (!node.description) missing.push('description');
|
|
165
213
|
return missing;
|
|
166
214
|
}
|
|
167
215
|
|
|
@@ -179,14 +227,17 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
|
|
|
179
227
|
const style = nodeStyle(node, theme, selected);
|
|
180
228
|
const shape = nodeShape(node);
|
|
181
229
|
const missing = nodeMissing(node);
|
|
182
|
-
const
|
|
230
|
+
const Icon = nodeKindIcon(node);
|
|
231
|
+
const typeLabel = nodeTypeLabel(node);
|
|
232
|
+
|
|
233
|
+
const size = nodeSize(node);
|
|
183
234
|
|
|
184
235
|
return (
|
|
185
236
|
<div
|
|
186
237
|
onClick={onClick}
|
|
187
238
|
style={{
|
|
188
|
-
width:
|
|
189
|
-
minHeight:
|
|
239
|
+
width: size.width,
|
|
240
|
+
minHeight: size.height,
|
|
190
241
|
boxSizing: 'border-box',
|
|
191
242
|
display: 'flex',
|
|
192
243
|
flexDirection: 'column',
|
|
@@ -204,48 +255,42 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
|
|
|
204
255
|
fontFamily: theme.fonts.body,
|
|
205
256
|
}}
|
|
206
257
|
>
|
|
207
|
-
{/*
|
|
208
|
-
|
|
209
|
-
|
|
258
|
+
{/* The kind is carried by the icon and the box shape; the one word here
|
|
259
|
+
is the container's kind. Confirmation state is carried by the border
|
|
260
|
+
(see `nodeStyle`), not by a word. */}
|
|
210
261
|
<span
|
|
211
262
|
style={{
|
|
212
263
|
display: 'flex',
|
|
213
|
-
alignItems: '
|
|
264
|
+
alignItems: 'center',
|
|
214
265
|
gap: 6,
|
|
215
266
|
fontFamily: theme.fonts.monospace,
|
|
216
267
|
fontSize: theme.fontSizes[0],
|
|
268
|
+
color: style.color,
|
|
217
269
|
letterSpacing: 0.5,
|
|
218
270
|
textTransform: 'uppercase',
|
|
271
|
+
minWidth: 0,
|
|
219
272
|
}}
|
|
220
273
|
>
|
|
221
274
|
<span
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
whiteSpace: 'nowrap',
|
|
226
|
-
overflow: 'hidden',
|
|
227
|
-
textOverflow: 'ellipsis',
|
|
228
|
-
minWidth: 0,
|
|
229
|
-
}}
|
|
230
|
-
>
|
|
231
|
-
{nodeTag(node)}
|
|
232
|
-
</span>
|
|
233
|
-
<span
|
|
234
|
-
title={unconfirmed ? 'Nobody has reviewed this element yet' : undefined}
|
|
235
|
-
style={{
|
|
236
|
-
color: unconfirmed ? muted : style.color,
|
|
237
|
-
// Unconfirmed is a quiet absence, not a loud claim.
|
|
238
|
-
opacity: unconfirmed ? 0.75 : 1,
|
|
239
|
-
letterSpacing: 0,
|
|
240
|
-
textTransform: 'none',
|
|
241
|
-
whiteSpace: 'nowrap',
|
|
242
|
-
overflow: 'hidden',
|
|
243
|
-
textOverflow: 'ellipsis',
|
|
244
|
-
minWidth: 0,
|
|
245
|
-
}}
|
|
275
|
+
title={nodeTag(node)}
|
|
276
|
+
aria-label={nodeTag(node)}
|
|
277
|
+
style={{ display: 'inline-flex', flexShrink: 0 }}
|
|
246
278
|
>
|
|
247
|
-
{
|
|
279
|
+
<Icon size={13} strokeWidth={2.25} />
|
|
248
280
|
</span>
|
|
281
|
+
{typeLabel && (
|
|
282
|
+
<span
|
|
283
|
+
style={{
|
|
284
|
+
fontWeight: 600,
|
|
285
|
+
whiteSpace: 'nowrap',
|
|
286
|
+
overflow: 'hidden',
|
|
287
|
+
textOverflow: 'ellipsis',
|
|
288
|
+
minWidth: 0,
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{typeLabel}
|
|
292
|
+
</span>
|
|
293
|
+
)}
|
|
249
294
|
</span>
|
|
250
295
|
{/* Two lines rather than an ellipsis: a truncated label hides the very
|
|
251
296
|
thing that distinguishes two containers from each other. The fixed
|
|
@@ -276,7 +321,7 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
|
|
|
276
321
|
overflow: 'hidden',
|
|
277
322
|
textOverflow: 'ellipsis',
|
|
278
323
|
}}
|
|
279
|
-
title={node.
|
|
324
|
+
title={node.description ?? node.members.join(', ')}
|
|
280
325
|
>
|
|
281
326
|
{nodeSubtitle(node)}
|
|
282
327
|
</span>
|
|
@@ -117,21 +117,46 @@ export function FileDrawer({
|
|
|
117
117
|
userSelect: "none",
|
|
118
118
|
}}
|
|
119
119
|
>
|
|
120
|
+
{showProgress && progress != null && (
|
|
121
|
+
<div
|
|
122
|
+
role="img"
|
|
123
|
+
aria-label={`Step ${progress.index + 1} of ${progress.total}`}
|
|
124
|
+
style={{
|
|
125
|
+
display: "flex",
|
|
126
|
+
gap: 2,
|
|
127
|
+
flexShrink: 0,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{Array.from({ length: progress.total }, (_, i) => (
|
|
131
|
+
<span
|
|
132
|
+
key={i}
|
|
133
|
+
style={{
|
|
134
|
+
flex: 1,
|
|
135
|
+
height: 2,
|
|
136
|
+
borderRadius: 1,
|
|
137
|
+
background:
|
|
138
|
+
i <= progress.index
|
|
139
|
+
? theme.colors.primary
|
|
140
|
+
: theme.colors.border,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
120
146
|
<div
|
|
121
147
|
style={{
|
|
122
148
|
display: "flex",
|
|
123
149
|
alignItems: "center",
|
|
124
150
|
gap: 8,
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
// Top runs deeper than the bottom so the title/label sits
|
|
152
|
+
// optically centered inside the sticky top edge.
|
|
153
|
+
padding: "10px 10px 8px",
|
|
154
|
+
// The header row is the last chrome row now, so its hairline is
|
|
155
|
+
// the boundary with the code below.
|
|
156
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
131
157
|
}}
|
|
132
158
|
>
|
|
133
159
|
<span
|
|
134
|
-
title={title ?? undefined}
|
|
135
160
|
style={{
|
|
136
161
|
flex: 1,
|
|
137
162
|
minWidth: 0,
|
|
@@ -197,33 +222,6 @@ export function FileDrawer({
|
|
|
197
222
|
<X size={14} />
|
|
198
223
|
</button>
|
|
199
224
|
</div>
|
|
200
|
-
{showProgress && progress != null && (
|
|
201
|
-
<div
|
|
202
|
-
role="img"
|
|
203
|
-
aria-label={`Step ${progress.index + 1} of ${progress.total}`}
|
|
204
|
-
style={{
|
|
205
|
-
display: "flex",
|
|
206
|
-
gap: 2,
|
|
207
|
-
flexShrink: 0,
|
|
208
|
-
padding: "0 10px 6px",
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
{Array.from({ length: progress.total }, (_, i) => (
|
|
212
|
-
<span
|
|
213
|
-
key={i}
|
|
214
|
-
style={{
|
|
215
|
-
flex: 1,
|
|
216
|
-
height: 2,
|
|
217
|
-
borderRadius: 1,
|
|
218
|
-
background:
|
|
219
|
-
i <= progress.index
|
|
220
|
-
? theme.colors.primary
|
|
221
|
-
: theme.colors.border,
|
|
222
|
-
}}
|
|
223
|
-
/>
|
|
224
|
-
))}
|
|
225
|
-
</div>
|
|
226
|
-
)}
|
|
227
225
|
</div>
|
|
228
226
|
<div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
|
|
229
227
|
{children}
|
|
@@ -17,10 +17,12 @@ import {
|
|
|
17
17
|
ChevronDown,
|
|
18
18
|
ChevronRight,
|
|
19
19
|
CircleCheck,
|
|
20
|
+
CircleHelp,
|
|
20
21
|
Component,
|
|
21
22
|
FileX,
|
|
22
23
|
FolderGit2,
|
|
23
24
|
Footprints,
|
|
25
|
+
History,
|
|
24
26
|
MapPin,
|
|
25
27
|
Network,
|
|
26
28
|
Route,
|
|
@@ -104,6 +106,8 @@ const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
|
|
|
104
106
|
boundary_module_file_mismatch: 'static-topology',
|
|
105
107
|
boundary_process_nest_disagree: 'dynamic-topology',
|
|
106
108
|
trail: 'trail',
|
|
109
|
+
step_unconfirmed: 'trail',
|
|
110
|
+
step_stale: 'trail',
|
|
107
111
|
};
|
|
108
112
|
|
|
109
113
|
/**
|
|
@@ -164,6 +168,9 @@ export const ISSUE_KIND_ICON: Record<string, LucideIcon> = {
|
|
|
164
168
|
// One module, more than one claimed process parent — a containment that can't
|
|
165
169
|
// be drawn, which is why the region sits at the root instead of nested.
|
|
166
170
|
boundary_process_nest_disagree: Split,
|
|
171
|
+
// Trail step verification: unconfirmed = no augmentation yet, stale = code changed
|
|
172
|
+
step_unconfirmed: CircleHelp,
|
|
173
|
+
step_stale: History,
|
|
167
174
|
};
|
|
168
175
|
|
|
169
176
|
/**
|
|
@@ -233,7 +240,7 @@ export function humanizeIssueKind(kind: string): string {
|
|
|
233
240
|
const KIND_LABEL: Record<string, string> = {
|
|
234
241
|
missing_file: 'Construct declaration file missing',
|
|
235
242
|
symbol_ambiguous: 'Multiple construct symbol matches',
|
|
236
|
-
symbol_unmatched: 'Construct symbol
|
|
243
|
+
symbol_unmatched: 'Construct symbol missing',
|
|
237
244
|
stale_declaration: 'Construct declaration drift',
|
|
238
245
|
construct_mismatch: 'Construct type mismatch',
|
|
239
246
|
construct_unconfirmed: 'Construct type unconfirmed',
|
|
@@ -244,6 +251,8 @@ const KIND_LABEL: Record<string, string> = {
|
|
|
244
251
|
boundary_module_file_mismatch: 'Declaration file outside module',
|
|
245
252
|
boundary_process_nest_disagree: 'Module spans multiple process contexts',
|
|
246
253
|
trail: 'Step issue',
|
|
254
|
+
step_unconfirmed: 'Call site not verified',
|
|
255
|
+
step_stale: 'Call site changed',
|
|
247
256
|
};
|
|
248
257
|
|
|
249
258
|
/** Label for an issue's kind. Explicit `kindLabel` wins; unknown kinds humanize. */
|
|
@@ -275,8 +284,10 @@ const KIND_ORDER: Record<string, number> = {
|
|
|
275
284
|
boundary_module_file_mismatch: 23,
|
|
276
285
|
// Runtime topology — process containment
|
|
277
286
|
boundary_process_nest_disagree: 30,
|
|
278
|
-
// Trail
|
|
287
|
+
// Trail — mechanical issues first, then step verification
|
|
279
288
|
trail: 40,
|
|
289
|
+
step_unconfirmed: 41,
|
|
290
|
+
step_stale: 42,
|
|
280
291
|
};
|
|
281
292
|
|
|
282
293
|
const KIND_ORDER_FALLBACK = 100;
|
|
@@ -869,16 +869,24 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
869
869
|
// the selected step until it does.
|
|
870
870
|
const [visibleStepIndex, setVisibleStepIndex] = useState<number | null>(null);
|
|
871
871
|
const drawerStepIndex = drawerTarget?.kind === 'trail' ? drawerTarget.stepIndex : null;
|
|
872
|
+
// Reset when the drawer switches trail (or closes) — synchronously, so the bar
|
|
873
|
+
// never paints the previous trail's position. A step change *within* the same
|
|
874
|
+
// trail is deliberately not reset: the code view scrolls to the new step and
|
|
875
|
+
// the viewport tracker carries the fill across that scroll. Resetting on step
|
|
876
|
+
// change flashed — the post-paint effect snapped the fill to the target, then
|
|
877
|
+
// Pierre's smooth scroll re-reported the old viewport, bouncing target → old →
|
|
878
|
+
// target.
|
|
879
|
+
const activeTrailId = focusedTrail?.id ?? null;
|
|
880
|
+
const [visibleTrailId, setVisibleTrailId] = useState(activeTrailId);
|
|
881
|
+
if (activeTrailId !== visibleTrailId) {
|
|
882
|
+
setVisibleTrailId(activeTrailId);
|
|
883
|
+
setVisibleStepIndex(null);
|
|
884
|
+
}
|
|
872
885
|
const drawerProgress = useMemo(() => {
|
|
873
886
|
if (drawerStepIndex == null || !focusedTrail) return null;
|
|
874
887
|
return { index: visibleStepIndex ?? drawerStepIndex, total: focusedTrail.steps.length };
|
|
875
888
|
}, [drawerStepIndex, focusedTrail, visibleStepIndex]);
|
|
876
889
|
|
|
877
|
-
// A new trail or step invalidates whatever the last scroll reported.
|
|
878
|
-
useEffect(() => {
|
|
879
|
-
setVisibleStepIndex(null);
|
|
880
|
-
}, [focusedTrail?.id, drawerStepIndex]);
|
|
881
|
-
|
|
882
890
|
// Refresh selected component when the components list updates (e.g. verify
|
|
883
891
|
// writes back declarationRef).
|
|
884
892
|
useEffect(() => {
|
|
@@ -1176,6 +1184,37 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
1176
1184
|
return map;
|
|
1177
1185
|
}, [issues]);
|
|
1178
1186
|
|
|
1187
|
+
// Per-edge step verification badge: maps edge IDs to step verification status.
|
|
1188
|
+
// Keyed by the React Flow edge id (from → mechanism → to). An edge can have
|
|
1189
|
+
// multiple steps with issues; we show the worst status (stale > unconfirmed).
|
|
1190
|
+
const stepIssueBadgeByEdgeId = useMemo(() => {
|
|
1191
|
+
const map = new Map<string, { kind: 'step_unconfirmed' | 'step_stale'; severity: 'info' | 'error'; count: number }>();
|
|
1192
|
+
if (!issues?.length || !trails?.length) {
|
|
1193
|
+
console.log('[stepIssueBadge] early return: issues=', issues?.length, 'trails=', trails?.length);
|
|
1194
|
+
return map;
|
|
1195
|
+
}
|
|
1196
|
+
for (const issue of issues) {
|
|
1197
|
+
if (issue.kind !== 'step_unconfirmed' && issue.kind !== 'step_stale') continue;
|
|
1198
|
+
const stepInfo = issueStep(issue);
|
|
1199
|
+
console.log('[stepIssueBadge] issue', issue.id, 'kind=', issue.kind, 'stepInfo=', stepInfo);
|
|
1200
|
+
if (!stepInfo) continue;
|
|
1201
|
+
const step = stepInfo.trail.steps[stepInfo.stepIndex];
|
|
1202
|
+
if (!step) continue;
|
|
1203
|
+
const edgeId = trailStepGraphEdgeId(step);
|
|
1204
|
+
console.log('[stepIssueBadge] -> edgeId=', edgeId);
|
|
1205
|
+
const prev = map.get(edgeId);
|
|
1206
|
+
// step_stale is worse than step_unconfirmed
|
|
1207
|
+
const isWorse = issue.kind === 'step_stale' && prev?.kind !== 'step_stale';
|
|
1208
|
+
map.set(edgeId, {
|
|
1209
|
+
kind: isWorse || !prev ? issue.kind as 'step_unconfirmed' | 'step_stale' : prev.kind,
|
|
1210
|
+
severity: prev?.severity === 'error' || issue.severity === 'error' ? 'error' : 'info',
|
|
1211
|
+
count: (prev?.count ?? 0) + 1,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
console.log('[stepIssueBadge] final map size=', map.size);
|
|
1215
|
+
return map;
|
|
1216
|
+
}, [issues, trails, issueStep]);
|
|
1217
|
+
|
|
1179
1218
|
// Expanded verification layers in the diagnostics list. When any layer with
|
|
1180
1219
|
// findings is expanded, the canvas dims everything that layer does not
|
|
1181
1220
|
// implicate (see `issueFocus`). The list publishes this through
|
|
@@ -2444,7 +2483,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2444
2483
|
// doesn't intercept pointer events). Uses ELK-computed label midpoints from
|
|
2445
2484
|
// the actual edge path (not node-center approximations).
|
|
2446
2485
|
const edgeLabels = useMemo(() => {
|
|
2447
|
-
|
|
2486
|
+
const labels = dispEdges
|
|
2448
2487
|
.filter((e) => {
|
|
2449
2488
|
if (e.hidden) return false;
|
|
2450
2489
|
const d = e?.data as { dimmed?: boolean } | undefined;
|
|
@@ -2459,6 +2498,8 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2459
2498
|
labelY?: number;
|
|
2460
2499
|
pathLength?: number;
|
|
2461
2500
|
} | undefined;
|
|
2501
|
+
const stepIssue = stepIssueBadgeByEdgeId.get(e.id);
|
|
2502
|
+
console.log('[edgeLabels] edge', e.id, 'stepIssue=', stepIssue);
|
|
2462
2503
|
return {
|
|
2463
2504
|
id: e.id,
|
|
2464
2505
|
mechanism: d?.mechanism ?? 'uses',
|
|
@@ -2468,9 +2509,12 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2468
2509
|
midY: d?.labelY ?? 0,
|
|
2469
2510
|
pathLength: d?.pathLength ?? 0,
|
|
2470
2511
|
stepNos: selectedFlowStepNos?.get(e.id),
|
|
2512
|
+
stepIssue,
|
|
2471
2513
|
};
|
|
2472
2514
|
});
|
|
2473
|
-
|
|
2515
|
+
console.log('[edgeLabels] total labels=', labels.length, 'with stepIssue=', labels.filter(l => l.stepIssue).length);
|
|
2516
|
+
return labels;
|
|
2517
|
+
}, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId]);
|
|
2474
2518
|
|
|
2475
2519
|
// Unique source files across components → sidebar file trees.
|
|
2476
2520
|
const treeFilePaths = treeFiles;
|
|
@@ -2950,6 +2994,53 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2950
2994
|
>
|
|
2951
2995
|
{text}
|
|
2952
2996
|
</span>
|
|
2997
|
+
{lbl.stepIssue && (() => {
|
|
2998
|
+
const StepIssueIcon = ISSUE_KIND_ICON[lbl.stepIssue.kind];
|
|
2999
|
+
// Match IssueList: error severity = red, info severity = amber
|
|
3000
|
+
const issueColor = lbl.stepIssue.severity === 'error'
|
|
3001
|
+
? (theme.colors.error ?? '#e5534b')
|
|
3002
|
+
: (theme.colors.warning ?? '#d4a017');
|
|
3003
|
+
const badgeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
|
|
3004
|
+
return (
|
|
3005
|
+
<span
|
|
3006
|
+
aria-hidden
|
|
3007
|
+
title={lbl.stepIssue.kind === 'step_stale'
|
|
3008
|
+
? 'Call site changed since verification'
|
|
3009
|
+
: 'Call site not yet verified'}
|
|
3010
|
+
style={{
|
|
3011
|
+
position: 'absolute',
|
|
3012
|
+
right: -7,
|
|
3013
|
+
bottom: -6,
|
|
3014
|
+
zIndex: 2,
|
|
3015
|
+
display: 'inline-flex',
|
|
3016
|
+
alignItems: 'center',
|
|
3017
|
+
justifyContent: 'center',
|
|
3018
|
+
gap: 3,
|
|
3019
|
+
minWidth: 18,
|
|
3020
|
+
padding: '2px 4px',
|
|
3021
|
+
borderRadius: 4,
|
|
3022
|
+
border: `1.5px solid ${issueColor}`,
|
|
3023
|
+
background: badgeBg,
|
|
3024
|
+
pointerEvents: 'none',
|
|
3025
|
+
}}
|
|
3026
|
+
>
|
|
3027
|
+
{StepIssueIcon && (
|
|
3028
|
+
<StepIssueIcon size={12} color={issueColor} />
|
|
3029
|
+
)}
|
|
3030
|
+
{lbl.stepIssue.count > 1 && (
|
|
3031
|
+
<span style={{
|
|
3032
|
+
color: issueColor,
|
|
3033
|
+
fontFamily: theme.fonts.monospace,
|
|
3034
|
+
fontSize: 10,
|
|
3035
|
+
fontWeight: 700,
|
|
3036
|
+
lineHeight: 1,
|
|
3037
|
+
}}>
|
|
3038
|
+
{lbl.stepIssue.count}
|
|
3039
|
+
</span>
|
|
3040
|
+
)}
|
|
3041
|
+
</span>
|
|
3042
|
+
);
|
|
3043
|
+
})()}
|
|
2953
3044
|
</div>
|
|
2954
3045
|
);
|
|
2955
3046
|
})}
|