@principal-ai/subsystems-react 0.20.5 → 0.21.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/README.md +1 -1
- package/dist/graphify/anchor.d.ts +17 -0
- package/dist/graphify/anchor.d.ts.map +1 -1
- package/dist/graphify/anchor.js +41 -0
- package/dist/graphify/anchor.js.map +1 -1
- package/dist/graphify/consolidated.d.ts +40 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/construct.d.ts +23 -0
- package/dist/graphify/construct.d.ts.map +1 -0
- package/dist/graphify/construct.js +77 -0
- package/dist/graphify/construct.js.map +1 -0
- package/dist/graphify/index.d.ts +2 -2
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +22 -0
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -0
- package/dist/pierre/PierreWalkthroughCodeView.js +198 -0
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -0
- package/dist/pierre/index.d.ts +2 -2
- package/dist/pierre/index.js +1 -1
- package/dist/pierre/pierreFileLang.d.ts +1 -1
- package/dist/pierre/pierreFileLang.js +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +14 -2
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +86 -20
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +1 -1
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts +2 -2
- package/dist/subsystem/FileDrawer.js +2 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -24
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +220 -194
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts +2 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +57 -33
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +109 -42
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +107 -5
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +43 -18
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +2 -2
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/package.json +3 -3
- package/src/graphify/anchor.test.ts +31 -2
- package/src/graphify/anchor.ts +46 -0
- package/src/graphify/consolidated.ts +43 -1
- package/src/graphify/{kind.test.ts → construct.test.ts} +20 -20
- package/src/graphify/{kind.ts → construct.ts} +17 -17
- package/src/graphify/index.ts +5 -2
- package/src/index.ts +20 -9
- package/src/pierre/{PierreThroughlineCodeView.tsx → PierreWalkthroughCodeView.tsx} +39 -47
- package/src/pierre/index.ts +2 -2
- package/src/pierre/pierreFileLang.ts +1 -1
- package/src/stories/Pierre/CodeView.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +14 -14
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +13 -15
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +16 -20
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +11 -10
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +13 -14
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +31 -66
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +254 -0
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +518 -33
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +13 -13
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +105 -21
- package/src/subsystem/ComponentDeclaration.tsx +1 -1
- package/src/subsystem/FileDrawer.tsx +2 -2
- package/src/subsystem/SubsystemComponentGraph.tsx +284 -254
- package/src/subsystem/formatDeclaration.test.ts +76 -1
- package/src/subsystem/formatDeclaration.ts +57 -33
- package/src/subsystem/model.test.ts +49 -15
- package/src/subsystem/model.ts +210 -51
- package/src/subsystem/nodes.tsx +63 -22
- package/src/subsystem/tokenizeComponent.ts +2 -2
package/src/subsystem/model.ts
CHANGED
|
@@ -21,6 +21,10 @@ import { computeElkLayout, calculatePathLength } from '../utils/elkLayout';
|
|
|
21
21
|
import type { GraphifyComponentDetail } from '../graphify';
|
|
22
22
|
import type { SubsystemDeclarationRef } from './declarationRef';
|
|
23
23
|
|
|
24
|
+
/** Structured declaration shape — same union as graphify drill-down payloads. */
|
|
25
|
+
export type SubsystemConstructDeclaration = GraphifyComponentDetail;
|
|
26
|
+
export type SubsystemDeclarationProvenance = 'verified' | 'authored';
|
|
27
|
+
|
|
24
28
|
export type SubsystemComponentConstruct =
|
|
25
29
|
| 'class'
|
|
26
30
|
| 'function'
|
|
@@ -82,20 +86,29 @@ export interface SubsystemDeclToken {
|
|
|
82
86
|
color?: string;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Topology relation type — structural / module / type claims.
|
|
91
|
+
* Belongs on `relations[]`, not on walkthrough hops.
|
|
92
|
+
*/
|
|
93
|
+
export type SubsystemRelationType =
|
|
86
94
|
| 'imports'
|
|
87
95
|
| 'imports_from'
|
|
88
96
|
| 're_exports'
|
|
89
97
|
| 'defines'
|
|
90
|
-
| 'calls'
|
|
91
98
|
| 'extends'
|
|
92
99
|
| 'inherits'
|
|
93
100
|
| 'implements'
|
|
94
101
|
| 'mixes_in'
|
|
95
|
-
| 'uses'
|
|
96
102
|
| 'method'
|
|
97
103
|
| 'references'
|
|
98
|
-
| 'contains'
|
|
104
|
+
| 'contains';
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Walkthrough hop mechanism — runtime seams with a `file:line` site.
|
|
108
|
+
*/
|
|
109
|
+
export type SubsystemWalkthroughMechanism =
|
|
110
|
+
| 'calls'
|
|
111
|
+
| 'uses'
|
|
99
112
|
| 'feeds'
|
|
100
113
|
| 'produces'
|
|
101
114
|
| 'writes'
|
|
@@ -103,6 +116,11 @@ export type SubsystemEdgeMechanism =
|
|
|
103
116
|
| 'watches'
|
|
104
117
|
| 'registers-into';
|
|
105
118
|
|
|
119
|
+
/** Union for derived graph-edge styling (relationType or hop mechanism). */
|
|
120
|
+
export type SubsystemEdgeMechanism =
|
|
121
|
+
| SubsystemRelationType
|
|
122
|
+
| SubsystemWalkthroughMechanism;
|
|
123
|
+
|
|
106
124
|
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
107
125
|
export interface SubsystemComponent {
|
|
108
126
|
id: string;
|
|
@@ -126,6 +144,13 @@ export interface SubsystemComponent {
|
|
|
126
144
|
purl: string;
|
|
127
145
|
/** One-line purpose shown on the node. */
|
|
128
146
|
purpose?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Design / migration placeholder — participates in edges and flows but is
|
|
149
|
+
* not a live source declaration yet. Verification skips source checks until
|
|
150
|
+
* promoted (`proposed` cleared, `file` + `symbol` filled). Orthogonal to
|
|
151
|
+
* `construct` (intended shape) and `role` (topology).
|
|
152
|
+
*/
|
|
153
|
+
proposed?: boolean;
|
|
129
154
|
/**
|
|
130
155
|
* Semantic role — where the node sits in the topology (boundary element,
|
|
131
156
|
* external system), orthogonal to `construct` (what it is). Drives the
|
|
@@ -180,27 +205,20 @@ export interface SubsystemComponent {
|
|
|
180
205
|
*/
|
|
181
206
|
layer?: number;
|
|
182
207
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* but a read-only investigation can still form one around an analyzed seam.
|
|
187
|
-
*/
|
|
188
|
-
capture?: 'edited' | 'analyzed' | 'referenced';
|
|
189
|
-
/**
|
|
190
|
-
* Graphify drill-down detail for this component, when the facet is anchored
|
|
191
|
-
* to a graphify node. Discriminated by kind (class/function/type/module/store/
|
|
192
|
-
* external); rendered in the detail panel on click.
|
|
208
|
+
* Structured declaration shape of the construct (params, members, type
|
|
209
|
+
* buckets, …). Single source of truth for the click panel — authored by
|
|
210
|
+
* agents/CLI or filled by verified graphify capture. Discriminated by kind.
|
|
193
211
|
*/
|
|
194
|
-
|
|
212
|
+
declaration?: SubsystemConstructDeclaration;
|
|
195
213
|
/**
|
|
196
|
-
* Where the
|
|
214
|
+
* Where the structured `declaration` came from. `verified` = extracted from
|
|
197
215
|
* source by tooling (graphify AST, signature extraction) — may be trusted
|
|
198
216
|
* as matching the code. `authored` = written by the authoring agent/human
|
|
199
217
|
* to highlight specific inputs/outputs — informative, not checked against
|
|
200
|
-
* source.
|
|
201
|
-
* may claim `verified`.
|
|
218
|
+
* source. Declaration without provenance is treated as `authored`; only
|
|
219
|
+
* tooling may claim `verified`.
|
|
202
220
|
*/
|
|
203
|
-
|
|
221
|
+
declarationProvenance?: SubsystemDeclarationProvenance;
|
|
204
222
|
/**
|
|
205
223
|
* Pre-tokenized declaration for the detail panel. When present, the
|
|
206
224
|
* renderer skips client-side tokenization. Tokens are language-agnostic;
|
|
@@ -214,7 +232,20 @@ export interface SubsystemComponent {
|
|
|
214
232
|
declarationRef?: SubsystemDeclarationRef;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
/** A
|
|
235
|
+
/** A topology relation between components (structural / module / type). */
|
|
236
|
+
export interface SubsystemRelation {
|
|
237
|
+
id: string;
|
|
238
|
+
from: string;
|
|
239
|
+
to: string;
|
|
240
|
+
relationType: SubsystemRelationType;
|
|
241
|
+
/** Concrete file/symbol refs backing the relation. */
|
|
242
|
+
refs?: string[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Derived / display graph edge used by renderers. Built from `relations`
|
|
247
|
+
* and/or walkthrough hops — not authored as its own document field.
|
|
248
|
+
*/
|
|
218
249
|
export interface SubsystemComponentEdge {
|
|
219
250
|
id: string;
|
|
220
251
|
from: string; // component id
|
|
@@ -225,41 +256,97 @@ export interface SubsystemComponentEdge {
|
|
|
225
256
|
}
|
|
226
257
|
|
|
227
258
|
/**
|
|
228
|
-
* A single
|
|
229
|
-
* seam
|
|
230
|
-
* (`from`, `to`, `mechanism`); a throughline step picks the concrete
|
|
231
|
-
* manifestation. One edge can appear in many steps.
|
|
259
|
+
* A single runtime hop — `from`/`to`/`mechanism` plus the exact `file:line`
|
|
260
|
+
* where that seam fires for a walkthrough.
|
|
232
261
|
*/
|
|
233
|
-
export interface
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
export interface SubsystemWalkthroughStep {
|
|
263
|
+
from: string;
|
|
264
|
+
to: string;
|
|
265
|
+
mechanism: SubsystemWalkthroughMechanism;
|
|
266
|
+
/** Repo-root-relative path of the file where the seam fires. */
|
|
237
267
|
file: string;
|
|
238
268
|
/** 1-based line of the site within `file`. */
|
|
239
269
|
line: number;
|
|
240
270
|
/**
|
|
241
271
|
* Frame name for this hop — the function/method/symbol on the stack at
|
|
242
|
-
* this site. Optional
|
|
243
|
-
*
|
|
272
|
+
* this site. Optional; when set the walkthroughs list shows it instead of
|
|
273
|
+
* mechanism + filename.
|
|
244
274
|
*/
|
|
245
275
|
symbol?: string;
|
|
246
276
|
/**
|
|
247
277
|
* Free-text note anchored to this hop's site line. Optional — informative
|
|
248
|
-
* only, never verified against source; the Pierre
|
|
278
|
+
* only, never verified against source; the Pierre walkthrough code view
|
|
249
279
|
* surfaces it in the annotation column next to the highlighted line.
|
|
250
280
|
*/
|
|
251
281
|
annotation?: string;
|
|
252
282
|
}
|
|
253
283
|
|
|
254
|
-
/**
|
|
255
|
-
|
|
256
|
-
* existing edge and the exact site where that relationship fires for a flow;
|
|
257
|
-
* ordering is the array. One throughline per flow (save flow, load flow, …).
|
|
258
|
-
*/
|
|
259
|
-
export interface SubsystemThroughline {
|
|
284
|
+
/** An ordered runtime walkthrough — one named behavior story. */
|
|
285
|
+
export interface SubsystemWalkthrough {
|
|
260
286
|
id: string;
|
|
261
287
|
title: string;
|
|
262
|
-
steps:
|
|
288
|
+
steps: SubsystemWalkthroughStep[];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface SubsystemModelDocument {
|
|
292
|
+
components: SubsystemComponent[];
|
|
293
|
+
/** Topology relations (structural / module / type). May be empty. */
|
|
294
|
+
relations: SubsystemRelation[];
|
|
295
|
+
/** Ordered runtime walkthroughs (one per named behavior). */
|
|
296
|
+
walkthroughs?: SubsystemWalkthrough[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Stable id for a derived graph edge from a relation or walkthrough hop. */
|
|
300
|
+
export function derivedGraphEdgeId(
|
|
301
|
+
from: string,
|
|
302
|
+
to: string,
|
|
303
|
+
mechanism: SubsystemEdgeMechanism,
|
|
304
|
+
): string {
|
|
305
|
+
return `${from}--${mechanism}-->${to}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build display edges for the graph canvas from topology relations and
|
|
310
|
+
* walkthrough hops (deduped by from/to/mechanism).
|
|
311
|
+
*/
|
|
312
|
+
/** React Flow / canvas edge id for a walkthrough hop. */
|
|
313
|
+
export function walkthroughStepGraphEdgeId(
|
|
314
|
+
step: Pick<SubsystemWalkthroughStep, 'from' | 'to' | 'mechanism'>,
|
|
315
|
+
): string {
|
|
316
|
+
return derivedGraphEdgeId(step.from, step.to, step.mechanism);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function deriveGraphEdges(doc: {
|
|
320
|
+
relations?: SubsystemRelation[];
|
|
321
|
+
walkthroughs?: SubsystemWalkthrough[];
|
|
322
|
+
}): SubsystemComponentEdge[] {
|
|
323
|
+
const byId = new Map<string, SubsystemComponentEdge>();
|
|
324
|
+
for (const r of doc.relations ?? []) {
|
|
325
|
+
const id = r.id || derivedGraphEdgeId(r.from, r.to, r.relationType);
|
|
326
|
+
if (!byId.has(id)) {
|
|
327
|
+
byId.set(id, {
|
|
328
|
+
id,
|
|
329
|
+
from: r.from,
|
|
330
|
+
to: r.to,
|
|
331
|
+
mechanism: r.relationType,
|
|
332
|
+
refs: r.refs,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const w of doc.walkthroughs ?? []) {
|
|
337
|
+
for (const step of w.steps) {
|
|
338
|
+
const id = derivedGraphEdgeId(step.from, step.to, step.mechanism);
|
|
339
|
+
if (!byId.has(id)) {
|
|
340
|
+
byId.set(id, {
|
|
341
|
+
id,
|
|
342
|
+
from: step.from,
|
|
343
|
+
to: step.to,
|
|
344
|
+
mechanism: step.mechanism,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return [...byId.values()];
|
|
263
350
|
}
|
|
264
351
|
|
|
265
352
|
/**
|
|
@@ -337,13 +424,6 @@ export function formatPurl(purl: string): string {
|
|
|
337
424
|
return identity;
|
|
338
425
|
}
|
|
339
426
|
|
|
340
|
-
export interface SubsystemModelDocument {
|
|
341
|
-
components: SubsystemComponent[];
|
|
342
|
-
edges: SubsystemComponentEdge[];
|
|
343
|
-
/** Ordered execution stories over the graph's edges (one per flow). */
|
|
344
|
-
throughlines?: SubsystemThroughline[];
|
|
345
|
-
}
|
|
346
|
-
|
|
347
427
|
// ---------------------------------------------------------------------------
|
|
348
428
|
// React Flow conversion
|
|
349
429
|
// ---------------------------------------------------------------------------
|
|
@@ -525,6 +605,77 @@ export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
|
525
605
|
service: 'service',
|
|
526
606
|
};
|
|
527
607
|
|
|
608
|
+
/** Right-badge accent for `proposed: true` (no role, or combined with role). */
|
|
609
|
+
export const PROPOSED_COLOR = '#b8860b'; // darkgoldenrod — not-yet-in-source
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Top-right badge label: proposed wins over role when both are set
|
|
613
|
+
* (proposed is the temporary exception to scan for; role stays on the model).
|
|
614
|
+
* Returns null when neither applies.
|
|
615
|
+
*/
|
|
616
|
+
export function rightBadgeLabel(component: {
|
|
617
|
+
role?: SubsystemComponentRole;
|
|
618
|
+
proposed?: boolean;
|
|
619
|
+
}): string | null {
|
|
620
|
+
if (component.proposed) return 'proposed';
|
|
621
|
+
if (component.role != null) return ROLE_LABEL[component.role];
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Color for the top-right badge (role and/or proposed). */
|
|
626
|
+
export function rightBadgeColor(component: {
|
|
627
|
+
role?: SubsystemComponentRole;
|
|
628
|
+
proposed?: boolean;
|
|
629
|
+
}): string | null {
|
|
630
|
+
if (component.proposed) return PROPOSED_COLOR;
|
|
631
|
+
if (component.role != null) return ROLE_COLOR[component.role];
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Display label per store `storage` backing. */
|
|
636
|
+
export const STORAGE_LABEL: Record<string, string> = {
|
|
637
|
+
memory: 'memory',
|
|
638
|
+
disk: 'disk',
|
|
639
|
+
external: 'db',
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
/** Right-badge accent per store `storage` backing — retention reads as its
|
|
643
|
+
* own hue so memory / disk / db are scannable from far on a canvas. */
|
|
644
|
+
export const STORAGE_COLOR: Record<string, string> = {
|
|
645
|
+
memory: '#5aa9e6', // sky — process-lifetime RAM
|
|
646
|
+
disk: '#e3b341', // amber — this process on the filesystem
|
|
647
|
+
external: '#c46fd8', // violet — mediated by another system (db/service)
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
function storeStorage(component: {
|
|
651
|
+
declaration?: GraphifyComponentDetail;
|
|
652
|
+
}): string | undefined {
|
|
653
|
+
return component.declaration?.kind === 'store'
|
|
654
|
+
? component.declaration.storage
|
|
655
|
+
: undefined;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Top-right storage badge for `construct: 'store'` nodes with an authored
|
|
659
|
+
* `storage` declaration — how the retained state is backed. Sits beside the
|
|
660
|
+
* role/proposed badge (storage is orthogonal to topology). Null otherwise. */
|
|
661
|
+
export function storageBadgeLabel(component: {
|
|
662
|
+
construct: SubsystemComponentConstruct;
|
|
663
|
+
declaration?: GraphifyComponentDetail;
|
|
664
|
+
}): string | null {
|
|
665
|
+
if (component.construct !== 'store') return null;
|
|
666
|
+
const storage = storeStorage(component);
|
|
667
|
+
return storage ? (STORAGE_LABEL[storage] ?? storage) : null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function storageBadgeColor(component: {
|
|
671
|
+
construct: SubsystemComponentConstruct;
|
|
672
|
+
declaration?: GraphifyComponentDetail;
|
|
673
|
+
}): string | null {
|
|
674
|
+
if (component.construct !== 'store') return null;
|
|
675
|
+
const storage = storeStorage(component);
|
|
676
|
+
return storage ? (STORAGE_COLOR[storage] ?? null) : null;
|
|
677
|
+
}
|
|
678
|
+
|
|
528
679
|
/**
|
|
529
680
|
* Primary badge text for a node: prefer framework stereotype over the
|
|
530
681
|
* language construct so a React UI unit reads as "component" / "hook"
|
|
@@ -576,13 +727,21 @@ export function nodeMinWidthForBadges(component: {
|
|
|
576
727
|
framework?: string;
|
|
577
728
|
stereotype?: string;
|
|
578
729
|
role?: SubsystemComponentRole;
|
|
730
|
+
proposed?: boolean;
|
|
579
731
|
entityKind?: string;
|
|
732
|
+
declaration?: GraphifyComponentDetail;
|
|
580
733
|
}): number {
|
|
581
734
|
const left = estimateBadgeLabelWidth(constructBadgeLabel(component));
|
|
582
|
-
|
|
735
|
+
const rightBadges = [
|
|
736
|
+
storageBadgeLabel(component),
|
|
737
|
+
rightBadgeLabel(component),
|
|
738
|
+
].filter((l): l is string => l != null);
|
|
739
|
+
if (rightBadges.length === 0) {
|
|
583
740
|
return Math.max(NODE_CSS_MIN_WIDTH, BADGE_EDGE_INSET + left + BADGE_EDGE_INSET);
|
|
584
741
|
}
|
|
585
|
-
const right =
|
|
742
|
+
const right = rightBadges
|
|
743
|
+
.map(estimateBadgeLabelWidth)
|
|
744
|
+
.reduce((acc, w) => acc + BADGE_PAIR_GAP + w);
|
|
586
745
|
return Math.max(
|
|
587
746
|
NODE_CSS_MIN_WIDTH,
|
|
588
747
|
BADGE_EDGE_INSET + left + BADGE_PAIR_GAP + right + BADGE_EDGE_INSET,
|
|
@@ -685,7 +844,7 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
|
|
|
685
844
|
const compIds = new Set(doc.components.map((c) => c.id));
|
|
686
845
|
const edges: SubsystemGraphEdge[] = [];
|
|
687
846
|
|
|
688
|
-
for (const e of doc
|
|
847
|
+
for (const e of deriveGraphEdges(doc)) {
|
|
689
848
|
const color = MECHANISM_COLOR[e.mechanism];
|
|
690
849
|
const style = MECHANISM_STYLE[e.mechanism];
|
|
691
850
|
// If `to` is a real component, connect directly; otherwise point at a stub node.
|
|
@@ -712,14 +871,14 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
|
|
|
712
871
|
|
|
713
872
|
/** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
|
|
714
873
|
export function subsystemGraphLayoutKey(
|
|
715
|
-
doc: Pick<SubsystemModelDocument, 'components' | '
|
|
874
|
+
doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'>,
|
|
716
875
|
): string {
|
|
717
876
|
const components = doc.components
|
|
718
877
|
.map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
|
|
719
878
|
[id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
|
|
720
879
|
.sort()
|
|
721
880
|
.join('\n');
|
|
722
|
-
const edgeKey = doc
|
|
881
|
+
const edgeKey = deriveGraphEdges(doc)
|
|
723
882
|
.map(({ id, from, to, mechanism }) => [id, from, to, mechanism].join('\0'))
|
|
724
883
|
.sort()
|
|
725
884
|
.join('\n');
|
|
@@ -759,7 +918,7 @@ export async function buildSubsystemGraph(
|
|
|
759
918
|
// cross-package edges have something to land on.
|
|
760
919
|
const realIds = new Set(doc.components.map((c) => c.id));
|
|
761
920
|
const externalIds: string[] = [];
|
|
762
|
-
for (const e of doc
|
|
921
|
+
for (const e of deriveGraphEdges(doc)) {
|
|
763
922
|
if (!realIds.has(e.to)) {
|
|
764
923
|
const extId = `external:${e.to}`;
|
|
765
924
|
if (!externalIds.includes(extId)) externalIds.push(extId);
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -19,9 +19,12 @@ import { useTheme } from '@principal-ade/industry-theme';
|
|
|
19
19
|
import {
|
|
20
20
|
MECHANISM_COLOR,
|
|
21
21
|
MECHANISM_STYLE,
|
|
22
|
-
|
|
23
|
-
ROLE_LABEL,
|
|
22
|
+
PROPOSED_COLOR,
|
|
24
23
|
constructBadgeLabel,
|
|
24
|
+
rightBadgeLabel,
|
|
25
|
+
rightBadgeColor,
|
|
26
|
+
storageBadgeLabel,
|
|
27
|
+
storageBadgeColor,
|
|
25
28
|
deriveNameFromSymbol,
|
|
26
29
|
BADGE_EDGE_INSET,
|
|
27
30
|
nodeMinWidthForBadges,
|
|
@@ -92,11 +95,23 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
92
95
|
// `symbol` is the source of truth; `name` is derived from it consistently.
|
|
93
96
|
const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file, c.stereotype);
|
|
94
97
|
// Top badges are absolutely positioned — widen the node so they nowrap
|
|
95
|
-
// instead of wrapping, including when construct + role
|
|
98
|
+
// instead of wrapping, including when construct + role/proposed share the top.
|
|
96
99
|
const badgeMinWidth = nodeMinWidthForBadges(c);
|
|
100
|
+
const topRightLabel = rightBadgeLabel(c);
|
|
101
|
+
const topRightColor = rightBadgeColor(c);
|
|
102
|
+
// Store retention backing badge — memory/disk/db, beside role (orthogonal).
|
|
103
|
+
const storageLabel = storageBadgeLabel(c);
|
|
104
|
+
const storageColor = storageBadgeColor(c);
|
|
97
105
|
// Set while a file is open in the drawer: true → spotlight, false → dim,
|
|
98
106
|
// absent (no file open) → neutral.
|
|
99
107
|
const fileMatch = data.fileMatch as boolean | undefined;
|
|
108
|
+
// Border: proposed uses the goldenrod accent (dashed); construct color stays
|
|
109
|
+
// on the left badge. File-open spotlight still wins with primary.
|
|
110
|
+
const borderColor = fileMatch
|
|
111
|
+
? theme.colors.primary
|
|
112
|
+
: c.proposed
|
|
113
|
+
? PROPOSED_COLOR
|
|
114
|
+
: color;
|
|
100
115
|
// Selection is stamped into data by the graph component — React Flow's own
|
|
101
116
|
// `selected` never updates because the node stops click propagation.
|
|
102
117
|
const isSelected = selected || (data.isSelected as boolean | undefined) === true;
|
|
@@ -129,10 +144,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
129
144
|
padding: '6px 10px',
|
|
130
145
|
borderRadius: 8,
|
|
131
146
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
132
|
-
// Selected / file-matched nodes get a thicker border
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
border: `${isSelected || fileMatch ? 4 : 2}px solid ${fileMatch ? theme.colors.primary : color}`,
|
|
147
|
+
// Selected / file-matched nodes get a thicker border. Proposed nodes
|
|
148
|
+
// use a dashed goldenrod border; left construct badge keeps construct color.
|
|
149
|
+
border: `${isSelected || fileMatch ? 4 : 2}px ${c.proposed ? 'dashed' : 'solid'} ${borderColor}`,
|
|
136
150
|
boxShadow: fileMatch
|
|
137
151
|
? `0 1px 4px rgba(0,0,0,0.25), 0 0 12px ${theme.colors.primary}55`
|
|
138
152
|
: '0 1px 4px rgba(0,0,0,0.25)',
|
|
@@ -167,30 +181,57 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
|
|
|
167
181
|
{constructBadgeLabel(c)}
|
|
168
182
|
</div>
|
|
169
183
|
|
|
170
|
-
{/*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{
|
|
184
|
+
{/* Right badge cluster — proposed wins over role when both are set;
|
|
185
|
+
store storage (retention backing) sits beside, since it is
|
|
186
|
+
orthogonal to topology. Persistent; pointer-events pass through. */}
|
|
187
|
+
{(topRightLabel != null || storageLabel != null) && (
|
|
174
188
|
<div
|
|
175
189
|
style={{
|
|
176
190
|
position: 'absolute',
|
|
177
191
|
top: -9,
|
|
178
192
|
right: BADGE_EDGE_INSET,
|
|
179
193
|
zIndex: 1,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
letterSpacing: 0.5,
|
|
183
|
-
textTransform: 'uppercase',
|
|
184
|
-
lineHeight: '17px',
|
|
194
|
+
display: 'flex',
|
|
195
|
+
gap: 6,
|
|
185
196
|
whiteSpace: 'nowrap',
|
|
186
|
-
color: ROLE_COLOR[c.role],
|
|
187
|
-
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
188
|
-
border: `1px solid ${ROLE_COLOR[c.role]}`,
|
|
189
|
-
borderRadius: 4,
|
|
190
|
-
padding: '0 5px',
|
|
191
197
|
}}
|
|
192
198
|
>
|
|
193
|
-
{
|
|
199
|
+
{storageLabel != null && storageColor != null && (
|
|
200
|
+
<span
|
|
201
|
+
style={{
|
|
202
|
+
fontFamily: theme.fonts.monospace,
|
|
203
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
204
|
+
letterSpacing: 0.5,
|
|
205
|
+
textTransform: 'uppercase',
|
|
206
|
+
lineHeight: '17px',
|
|
207
|
+
color: storageColor,
|
|
208
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
209
|
+
border: `1px solid ${storageColor}`,
|
|
210
|
+
borderRadius: 4,
|
|
211
|
+
padding: '0 5px',
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{storageLabel}
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
{topRightLabel != null && topRightColor != null && (
|
|
218
|
+
<span
|
|
219
|
+
style={{
|
|
220
|
+
fontFamily: theme.fonts.monospace,
|
|
221
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
222
|
+
letterSpacing: 0.5,
|
|
223
|
+
textTransform: 'uppercase',
|
|
224
|
+
lineHeight: '17px',
|
|
225
|
+
color: topRightColor,
|
|
226
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
227
|
+
border: `1px solid ${topRightColor}`,
|
|
228
|
+
borderRadius: 4,
|
|
229
|
+
padding: '0 5px',
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{topRightLabel}
|
|
233
|
+
</span>
|
|
234
|
+
)}
|
|
194
235
|
</div>
|
|
195
236
|
)}
|
|
196
237
|
|
|
@@ -50,9 +50,9 @@ export async function tokenizeComponent(
|
|
|
50
50
|
if (component.tokens) return component.tokens;
|
|
51
51
|
|
|
52
52
|
// External kind — not valid TypeScript, bypass Prettier.
|
|
53
|
-
const kind = component.
|
|
53
|
+
const kind = component.declaration?.kind ?? component.construct;
|
|
54
54
|
if (kind === 'external') {
|
|
55
|
-
const label = component.
|
|
55
|
+
const label = component.declaration?.kind === 'external' ? component.declaration.label : component.name;
|
|
56
56
|
const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
57
57
|
return tokenizeFormatted(`external '${escaped}'`, themeName);
|
|
58
58
|
}
|