@principal-ai/principal-view-react 0.16.55 → 0.16.57
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/graphify/consolidated.d.ts +16 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/index.d.ts +1 -1
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/graphify/kind.d.ts +1 -1
- package/dist/graphify/kind.d.ts.map +1 -1
- package/dist/graphify/kind.js +7 -1
- package/dist/graphify/kind.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/pierre/constructColors.d.ts +24 -0
- package/dist/pierre/constructColors.d.ts.map +1 -0
- package/dist/pierre/constructColors.js +91 -0
- package/dist/pierre/constructColors.js.map +1 -0
- package/dist/pierre/index.d.ts +2 -0
- package/dist/pierre/index.d.ts.map +1 -1
- package/dist/pierre/index.js +1 -0
- package/dist/pierre/index.js.map +1 -1
- package/dist/pierre/pierreSyntaxTheme.d.ts +11 -0
- package/dist/pierre/pierreSyntaxTheme.d.ts.map +1 -0
- package/dist/pierre/pierreSyntaxTheme.js +20 -0
- package/dist/pierre/pierreSyntaxTheme.js.map +1 -0
- package/dist/pierre/syntaxTokens.d.ts +11 -0
- package/dist/pierre/syntaxTokens.d.ts.map +1 -0
- package/dist/pierre/syntaxTokens.js +51 -0
- package/dist/pierre/syntaxTokens.js.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
- package/dist/subsystem/ComponentDeclaration.d.ts +2 -2
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +47 -24
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +23 -2
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +49 -10
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +57 -12
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +54 -24
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +4 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +70 -21
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.d.ts +4 -3
- package/dist/subsystem/tokenizeComponent.d.ts.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +7 -12
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/dist/subsystem/tokenizeFormatted.d.ts +3 -6
- package/dist/subsystem/tokenizeFormatted.d.ts.map +1 -1
- package/dist/subsystem/tokenizeFormatted.js +4 -75
- package/dist/subsystem/tokenizeFormatted.js.map +1 -1
- package/package.json +3 -1
- package/src/graphify/consolidated.ts +18 -1
- package/src/graphify/index.ts +1 -0
- package/src/graphify/kind.ts +7 -1
- package/src/index.ts +5 -3
- package/src/pierre/constructColors.test.ts +52 -0
- package/src/pierre/constructColors.ts +112 -0
- package/src/pierre/index.ts +5 -0
- package/src/pierre/pierreSyntaxTheme.ts +23 -0
- package/src/pierre/syntaxTokens.test.ts +35 -0
- package/src/pierre/syntaxTokens.ts +60 -0
- package/src/stories/{BoundaryNodes.stories.tsx → Audit/BoundaryNodes.stories.tsx} +1 -1
- package/src/stories/{CanvasEdgeTypes.stories.tsx → Audit/CanvasEdgeTypes.stories.tsx} +1 -1
- package/src/stories/{CanvasNodeTypes.stories.tsx → Audit/CanvasNodeTypes.stories.tsx} +1 -1
- package/src/stories/{ColorPriority.stories.tsx → Audit/ColorPriority.stories.tsx} +1 -1
- package/src/stories/{DiamondBadges.stories.tsx → Audit/DiamondBadges.stories.tsx} +1 -1
- package/src/stories/{NodeDimensionsTesting.stories.tsx → Audit/NodeDimensionsTesting.stories.tsx} +2 -2
- package/src/stories/{NodeFieldsAudit.stories.tsx → Audit/NodeFieldsAudit.stories.tsx} +1 -1
- package/src/stories/{NodeFontSizeTesting.stories.tsx → Audit/NodeFontSizeTesting.stories.tsx} +2 -2
- package/src/stories/{NodeShapes.stories.tsx → Audit/NodeShapes.stories.tsx} +1 -1
- package/src/stories/{WideNodesTesting.stories.tsx → Audit/WideNodesTesting.stories.tsx} +2 -2
- package/src/stories/{dashboard → Components}/DashboardRenderer.stories.tsx +2 -2
- package/src/stories/{GraphRenderer.stories.tsx → Components/GraphRenderer.stories.tsx} +2 -2
- package/src/stories/{MultiCanvasRenderer.stories.tsx → Components/MultiCanvasRenderer.stories.tsx} +2 -2
- package/src/stories/{SequenceDiagram.stories.tsx → Components/SequenceDiagram.stories.tsx} +2 -2
- package/src/stories/{StateView.stories.tsx → Components/StateView.stories.tsx} +2 -2
- package/src/stories/{TraceViewer.stories.tsx → Components/TraceViewer.stories.tsx} +3 -3
- package/src/stories/{WorkflowSequenceDiagram.stories.tsx → Components/WorkflowSequenceDiagram.stories.tsx} +1 -1
- package/src/stories/{AnimationWorkshop.stories.tsx → Features/AnimationWorkshop.stories.tsx} +5 -5
- package/src/stories/{ChangeTypeVisual.stories.tsx → Features/ChangeTypeVisual.stories.tsx} +2 -2
- package/src/stories/{EventDrivenAnimations.stories.tsx → Features/EventDrivenAnimations.stories.tsx} +2 -2
- package/src/stories/{MultiConfig.stories.tsx → Features/MultiConfig.stories.tsx} +2 -2
- package/src/stories/{ElkEdgeRouting.stories.tsx → Layout/ElkEdgeRouting.stories.tsx} +2 -2
- package/src/stories/{GraphOrientation.stories.tsx → Layout/GraphOrientation.stories.tsx} +3 -3
- package/src/stories/{MultiDirectionalConnections.stories.tsx → Layout/MultiDirectionalConnections.stories.tsx} +2 -2
- package/src/stories/{NodeDefinitionComparison.stories.tsx → Nodes/NodeDefinitionComparison.stories.tsx} +2 -2
- package/src/stories/{NodeNameFallbacks.stories.tsx → Nodes/NodeNameFallbacks.stories.tsx} +2 -2
- package/src/stories/{NumericColors.stories.tsx → Nodes/NumericColors.stories.tsx} +2 -2
- package/src/stories/{EventNameDisplay.stories.tsx → OTEL/EventNameDisplay.stories.tsx} +2 -2
- package/src/stories/{EventNameFallback.stories.tsx → OTEL/EventNameFallback.stories.tsx} +2 -2
- package/src/stories/{OtelComponents.stories.tsx → OTEL/OtelComponents.stories.tsx} +3 -3
- package/src/stories/{OtelNodeTypesPrototype.stories.tsx → OTEL/OtelNodeTypesPrototype.stories.tsx} +2 -2
- package/src/stories/{RawEventCards.stories.tsx → OTEL/RawEventCards.stories.tsx} +3 -3
- package/src/stories/{SpanBadges.stories.tsx → OTEL/SpanBadges.stories.tsx} +1 -1
- package/src/stories/{ValidationEventsCanvas.stories.tsx → OTEL/ValidationEventsCanvas.stories.tsx} +3 -3
- package/src/stories/{BacklogMdCanvases.stories.tsx → RealWorld/BacklogMdCanvases.stories.tsx} +2 -2
- package/src/stories/{FileCitySequence.stories.tsx → RealWorld/FileCitySequence.stories.tsx} +4 -4
- package/src/stories/{RealCanvasFiles.stories.tsx → RealWorld/RealCanvasFiles.stories.tsx} +7 -7
- package/src/stories/{ComponentDeclarationAudit.stories.tsx → Subsystem/ComponentDeclarationAudit.stories.tsx} +19 -20
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +646 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
- package/src/subsystem/ComponentDeclaration.tsx +64 -26
- package/src/subsystem/SubsystemComponentGraph.tsx +26 -6
- package/src/subsystem/formatDeclaration.ts +57 -11
- package/src/subsystem/model.test.ts +38 -6
- package/src/subsystem/model.ts +110 -31
- package/src/subsystem/nodes.tsx +103 -27
- package/src/subsystem/tokenizeComponent.ts +12 -12
- package/src/subsystem/tokenizeFormatted.ts +9 -82
- package/src/stories/SubsystemComponentGraph.stories.tsx +0 -1225
- /package/src/stories/{dashboard/sample-dashboards → data}/activity-feed-analytics.dashboard.json +0 -0
package/src/subsystem/model.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Subsystem component-graph data model + converters.
|
|
3
3
|
*
|
|
4
4
|
* A subsystem snapshot (see the "Subsystem artifact: facets" topic) is captured
|
|
5
|
-
* as component nodes (
|
|
5
|
+
* as component nodes (construct-tagged source units), file refs, integration edges,
|
|
6
6
|
* and entry points. This module defines the minimal document shape for the
|
|
7
7
|
* *graph* facet and converts it to React Flow nodes/edges — packages render as
|
|
8
8
|
* subgraphs, only cross-package edges leave the box, and shared seams
|
|
@@ -21,14 +21,30 @@ import { computeElkLayout } from '../utils/elkLayout';
|
|
|
21
21
|
import type { GraphifyComponentDetail } from '../graphify';
|
|
22
22
|
import type { SubsystemDeclarationRef } from './declarationRef';
|
|
23
23
|
|
|
24
|
-
export type
|
|
24
|
+
export type SubsystemComponentConstruct =
|
|
25
25
|
| 'class'
|
|
26
26
|
| 'function'
|
|
27
27
|
| 'method'
|
|
28
|
-
| '
|
|
28
|
+
| 'interface'
|
|
29
|
+
| 'type_alias'
|
|
30
|
+
| 'enum'
|
|
29
31
|
| 'module'
|
|
32
|
+
| 'store'
|
|
30
33
|
| 'external';
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Semantic role — where the node sits in the topology, orthogonal to
|
|
37
|
+
* `construct` (what it is). Roles have *inherited* anatomy: an entry renders
|
|
38
|
+
* as its real code shape (function dispatcher, type contract); a service has
|
|
39
|
+
* no source at all (`construct: 'external'` + purl identity). Contrast with
|
|
40
|
+
* `construct: 'store'`, which introduces its own anatomy (the state block) —
|
|
41
|
+
* that is why store is a construct and not a role. Role nodes must stay anchored to real code: an
|
|
42
|
+
* `entry` is a boundary element (route dispatcher, message contract) carrying
|
|
43
|
+
* the wire address as identity; a `service` is an external system the process
|
|
44
|
+
* calls out to (identity via purl, no `process` — it belongs to no region).
|
|
45
|
+
*/
|
|
46
|
+
export type SubsystemComponentRole = 'entry' | 'service';
|
|
47
|
+
|
|
32
48
|
// ---------------------------------------------------------------------------
|
|
33
49
|
// Declaration tokens — structured source representation
|
|
34
50
|
// ---------------------------------------------------------------------------
|
|
@@ -45,6 +61,8 @@ export type SubsystemDeclTokenKind =
|
|
|
45
61
|
export interface SubsystemDeclToken {
|
|
46
62
|
text: string;
|
|
47
63
|
kind: SubsystemDeclTokenKind;
|
|
64
|
+
/** Shiki/Pierre foreground when tokenized client-side; omitted on legacy wire tokens. */
|
|
65
|
+
color?: string;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
export type SubsystemEdgeMechanism =
|
|
@@ -63,19 +81,48 @@ export type SubsystemEdgeMechanism =
|
|
|
63
81
|
| 'contains'
|
|
64
82
|
| 'feeds'
|
|
65
83
|
| 'produces'
|
|
84
|
+
| 'writes'
|
|
85
|
+
| 'reads'
|
|
86
|
+
| 'watches'
|
|
66
87
|
| 'registers-into';
|
|
67
88
|
|
|
68
|
-
/** A component node — the named unit,
|
|
89
|
+
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
69
90
|
export interface SubsystemComponent {
|
|
70
91
|
id: string;
|
|
71
92
|
name: string;
|
|
72
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The node's construct — what it IS as a declaration (class, function,
|
|
95
|
+
* method, interface, type alias, enum, store, external), driving node
|
|
96
|
+
* anatomy, color, badge, and the verification strategy. Every construct
|
|
97
|
+
* anchors to a definition; runtime occurrences (variables, activations,
|
|
98
|
+
* instances) are NOT constructs — they belong to a future execution-mode
|
|
99
|
+
* graph whose occurrence nodes reference these definitions. Ontology:
|
|
100
|
+
* construct = what it is, role = where it sits, process = where it runs.
|
|
101
|
+
*/
|
|
102
|
+
construct: SubsystemComponentConstruct;
|
|
73
103
|
/** Source location the component lives in (repo-root-relative path). */
|
|
74
104
|
file: string;
|
|
75
105
|
/** PURL identifying the repo or package this component lives in (for subgraph grouping). */
|
|
76
106
|
purl: string;
|
|
77
107
|
/** One-line purpose shown on the node. */
|
|
78
108
|
purpose?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Semantic role — where the node sits in the topology (boundary element,
|
|
111
|
+
* external system), orthogonal to `construct` (what it is). Drives the
|
|
112
|
+
* glyph and the edge-pairing rules: boundary-crossing edges must terminate
|
|
113
|
+
* at an entry or a service. Retained state is NOT a role — it is
|
|
114
|
+
* `construct: 'store'` (state-block anatomy, `writes`/`reads`/`watches`
|
|
115
|
+
* inbound, `produces` outbound).
|
|
116
|
+
*/
|
|
117
|
+
role?: SubsystemComponentRole;
|
|
118
|
+
/**
|
|
119
|
+
* Runtime process membership — which deployment unit this node is a
|
|
120
|
+
* member of (e.g. `trail-viewer/host`, `trail-viewer/renderer`). Nodes
|
|
121
|
+
* sharing a `process` are drawn inside one boundary region (grouping is
|
|
122
|
+
* `process ?? purl`); nodes without one sit outside every boundary
|
|
123
|
+
* (external actors, services, libraries).
|
|
124
|
+
*/
|
|
125
|
+
process?: string;
|
|
79
126
|
/** A symbol this component exposes / is (the node's identity). */
|
|
80
127
|
symbol?: string;
|
|
81
128
|
/**
|
|
@@ -94,7 +141,7 @@ export interface SubsystemComponent {
|
|
|
94
141
|
capture?: 'edited' | 'analyzed' | 'referenced';
|
|
95
142
|
/**
|
|
96
143
|
* Graphify drill-down detail for this component, when the facet is anchored
|
|
97
|
-
* to a graphify node. Discriminated by kind (class/function/type/module/
|
|
144
|
+
* to a graphify node. Discriminated by kind (class/function/type/module/store/
|
|
98
145
|
* external); rendered in the detail panel on click.
|
|
99
146
|
*/
|
|
100
147
|
detail?: GraphifyComponentDetail;
|
|
@@ -131,10 +178,10 @@ export interface SubsystemComponentEdge {
|
|
|
131
178
|
}
|
|
132
179
|
|
|
133
180
|
/**
|
|
134
|
-
* Derive a consistent display `name` from a code `symbol` +
|
|
181
|
+
* Derive a consistent display `name` from a code `symbol` + construct.
|
|
135
182
|
*
|
|
136
|
-
* `symbol` is the source of truth (fully-qualified code identity).
|
|
137
|
-
*
|
|
183
|
+
* `symbol` is the source of truth (fully-qualified code identity). The name
|
|
184
|
+
* is the symbol itself:
|
|
138
185
|
* - class/type/module/function/script/... symbol → symbol (e.g. `SessionReader`)
|
|
139
186
|
* - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
|
|
140
187
|
* - module, no symbol → basename of `file` (e.g. `transcript.ts` → `transcript`) —
|
|
@@ -143,20 +190,37 @@ export interface SubsystemComponentEdge {
|
|
|
143
190
|
*/
|
|
144
191
|
export function deriveNameFromSymbol(
|
|
145
192
|
symbol: string | undefined,
|
|
146
|
-
|
|
193
|
+
construct: SubsystemComponentConstruct,
|
|
147
194
|
existingName?: string,
|
|
148
195
|
file?: string,
|
|
149
196
|
): string {
|
|
197
|
+
let name: string | undefined;
|
|
150
198
|
if (symbol && symbol.trim()) {
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
// Module nodes without a symbol are whole files → use the file basename.
|
|
154
|
-
if (kind === 'module' && file) {
|
|
199
|
+
name = symbol;
|
|
200
|
+
} else if (construct === 'module' && file) {
|
|
155
201
|
const base = file.split('/').pop() ?? '';
|
|
156
202
|
const clean = base.replace(/\.[^.]+$/, ''); // strip extension
|
|
157
|
-
if (clean)
|
|
203
|
+
if (clean) name = clean;
|
|
204
|
+
}
|
|
205
|
+
if (!name) name = existingName ?? 'untitled';
|
|
206
|
+
|
|
207
|
+
// Decoration = what the drill-down shows. Executable constructs wear `()`
|
|
208
|
+
// (a signature you can call); brace-bodied constructs wear ` {}` (a member
|
|
209
|
+
// body — fields for types/interfaces/enums, fields+methods for classes).
|
|
210
|
+
// Everything else (store, variable, module, external) renders bare.
|
|
211
|
+
if ((construct === 'function' || construct === 'method') && !name.endsWith('()')) {
|
|
212
|
+
name = `${name}()`;
|
|
158
213
|
}
|
|
159
|
-
|
|
214
|
+
if (
|
|
215
|
+
(construct === 'class' ||
|
|
216
|
+
construct === 'interface' ||
|
|
217
|
+
construct === 'type_alias' ||
|
|
218
|
+
construct === 'enum') &&
|
|
219
|
+
!name.endsWith('{}')
|
|
220
|
+
) {
|
|
221
|
+
name = `${name} {}`;
|
|
222
|
+
}
|
|
223
|
+
return name;
|
|
160
224
|
}
|
|
161
225
|
|
|
162
226
|
/**
|
|
@@ -235,6 +299,9 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
235
299
|
contains: '#6c5ce7', // indigo
|
|
236
300
|
feeds: '#22c55e', // green — data-flow into a processor
|
|
237
301
|
produces: '#e07a5f', // terracotta — emits an output type
|
|
302
|
+
writes: '#2f9e44', // deep green — mutates retained state
|
|
303
|
+
reads: '#0ea5e9', // sky — pulls from retained state
|
|
304
|
+
watches: '#9ca3af', // gray — observes, owns nothing
|
|
238
305
|
'registers-into': '#ff6b35', // orange
|
|
239
306
|
};
|
|
240
307
|
|
|
@@ -254,6 +321,9 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
254
321
|
contains: 'solid',
|
|
255
322
|
feeds: 'solid',
|
|
256
323
|
produces: 'solid',
|
|
324
|
+
writes: 'solid',
|
|
325
|
+
reads: 'solid',
|
|
326
|
+
watches: 'dashed',
|
|
257
327
|
'registers-into': 'dashed',
|
|
258
328
|
};
|
|
259
329
|
|
|
@@ -272,14 +342,19 @@ export function packageColor(name: string): string {
|
|
|
272
342
|
return palette[h % palette.length];
|
|
273
343
|
}
|
|
274
344
|
|
|
275
|
-
/** Color per
|
|
276
|
-
|
|
277
|
-
class
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
345
|
+
/** Color per semantic role — applied ONLY to the role badge (top-right tab on
|
|
346
|
+
* nodes that carry a role). The node border stays construct-colored. Note:
|
|
347
|
+
* `service` currently equals the old class hex; harmless while role badges
|
|
348
|
+
* are the only surface using it, but pick a distinct value if roles ever
|
|
349
|
+
* take over node borders. */
|
|
350
|
+
export const ROLE_COLOR: Record<SubsystemComponentRole, string> = {
|
|
351
|
+
entry: '#ff6b35', // orange — boundary element
|
|
352
|
+
service: '#0893d2', // blue — external system
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
356
|
+
entry: 'entry',
|
|
357
|
+
service: 'service',
|
|
283
358
|
};
|
|
284
359
|
|
|
285
360
|
/**
|
|
@@ -295,12 +370,16 @@ export function convertSubsystemToNodes(
|
|
|
295
370
|
opts: { maxNodeWidth?: number } = {},
|
|
296
371
|
): SubsystemGraphNode[] {
|
|
297
372
|
const { maxNodeWidth } = opts;
|
|
298
|
-
// Group components by
|
|
373
|
+
// Group components into boundary regions by process when authored, else by
|
|
374
|
+
// package. Process is runtime membership (drawn as one boundary region);
|
|
375
|
+
// purl is code identity — nodes without a process (external actors,
|
|
376
|
+
// services, libraries) fall back to purl and sit outside every boundary.
|
|
299
377
|
const byPkg = new Map<string, SubsystemComponent[]>();
|
|
300
378
|
for (const c of doc.components) {
|
|
301
|
-
const
|
|
379
|
+
const regionKey = c.process ?? c.purl;
|
|
380
|
+
const list = byPkg.get(regionKey) ?? [];
|
|
302
381
|
list.push(c);
|
|
303
|
-
byPkg.set(
|
|
382
|
+
byPkg.set(regionKey, list);
|
|
304
383
|
}
|
|
305
384
|
|
|
306
385
|
const nodes: SubsystemGraphNode[] = [];
|
|
@@ -320,7 +399,7 @@ export function convertSubsystemToNodes(
|
|
|
320
399
|
const row = Math.floor(i / COLS);
|
|
321
400
|
// Estimate rendered node width from the longest text line so ELK reserves
|
|
322
401
|
// the right space (names can wrap, so we cap at the configurable max).
|
|
323
|
-
const text = [c.symbol, c.name, c.file
|
|
402
|
+
const text = [c.symbol, c.name, c.file?.split('/').pop() ?? '']
|
|
324
403
|
.filter((t): t is string => !!t)
|
|
325
404
|
.sort((a, b) => b.length - a.length)[0];
|
|
326
405
|
const cap = maxNodeWidth ?? 300;
|
|
@@ -385,8 +464,8 @@ export function subsystemGraphLayoutKey(
|
|
|
385
464
|
doc: Pick<SubsystemGraphDocument, 'components' | 'edges'>,
|
|
386
465
|
): string {
|
|
387
466
|
const components = doc.components
|
|
388
|
-
.map(({ id, purl, name, symbol,
|
|
389
|
-
[id, purl, name, symbol ?? '',
|
|
467
|
+
.map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
|
|
468
|
+
[id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
|
|
390
469
|
.sort()
|
|
391
470
|
.join('\n');
|
|
392
471
|
const edgeKey = doc.edges
|
|
@@ -435,7 +514,7 @@ export async function buildSubsystemGraph(
|
|
|
435
514
|
component: {
|
|
436
515
|
id: extId,
|
|
437
516
|
name: label,
|
|
438
|
-
|
|
517
|
+
construct: 'external',
|
|
439
518
|
purl: 'external',
|
|
440
519
|
file: '',
|
|
441
520
|
purpose: 'cross-package integration target (not a member node)',
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -16,18 +16,26 @@ import {
|
|
|
16
16
|
} from '@xyflow/react';
|
|
17
17
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
18
18
|
import {
|
|
19
|
-
KIND_COLOR,
|
|
20
19
|
MECHANISM_COLOR,
|
|
20
|
+
MECHANISM_STYLE,
|
|
21
|
+
ROLE_COLOR,
|
|
22
|
+
ROLE_LABEL,
|
|
21
23
|
deriveNameFromSymbol,
|
|
22
24
|
type SubsystemGraphNode,
|
|
23
25
|
type SubsystemGraphEdge,
|
|
24
26
|
} from './model';
|
|
27
|
+
import { constructColorsFromPierreTheme } from '../pierre/constructColors';
|
|
28
|
+
import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
25
29
|
|
|
26
|
-
export const
|
|
30
|
+
export const CONSTRUCT_LABEL: Record<string, string> = {
|
|
27
31
|
class: 'class',
|
|
28
32
|
function: 'function',
|
|
29
|
-
|
|
33
|
+
method: 'method',
|
|
34
|
+
interface: 'interface',
|
|
35
|
+
type_alias: 'type alias',
|
|
36
|
+
enum: 'enum',
|
|
30
37
|
module: 'module',
|
|
38
|
+
store: 'store',
|
|
31
39
|
external: 'external',
|
|
32
40
|
};
|
|
33
41
|
|
|
@@ -48,6 +56,9 @@ function breakWords(s: string): string {
|
|
|
48
56
|
export interface SubsystemGraphCallbacks {
|
|
49
57
|
/** Click a component — open its file/entry point. */
|
|
50
58
|
onSelect?: (componentId: string) => void;
|
|
59
|
+
/** Click the filename badge — open that component's file in the drawer
|
|
60
|
+
* (same path as clicking the file link in the declaration panel). */
|
|
61
|
+
onOpenFile?: (componentId: string) => void;
|
|
51
62
|
/** Click an edge (or its label) — select the relationship. */
|
|
52
63
|
onEdgeSelect?: (edgeId: string) => void;
|
|
53
64
|
/** Hover a component (null on leave) — associates it with the file tree. */
|
|
@@ -60,15 +71,19 @@ export interface SubsystemGraphCallbacks {
|
|
|
60
71
|
export const SUBSYSTEM_CALLBACKS: SubsystemGraphCallbacks = {};
|
|
61
72
|
|
|
62
73
|
export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
63
|
-
const { theme } = useTheme();
|
|
74
|
+
const { theme, mode } = useTheme();
|
|
64
75
|
const { data, selected, width: nodeWidth, height: nodeHeight } = props;
|
|
65
76
|
const c = data.component;
|
|
66
|
-
|
|
77
|
+
// Construct owns node color, derived from the active Pierre syntax theme —
|
|
78
|
+
// the same palette the declaration panel and file drawer render with. Role
|
|
79
|
+
// shows as the hover badge, not as color — for now; a role glyph/accent may
|
|
80
|
+
// come later.
|
|
81
|
+
const color = constructColorsFromPierreTheme(resolvePierreSyntaxThemeName(mode))[c.construct];
|
|
67
82
|
const [hover, setHover] = useState(false);
|
|
68
83
|
const configuredMax = SUBSYSTEM_CALLBACKS.maxNodeWidth;
|
|
69
84
|
const maxWidth = configuredMax ?? 300;
|
|
70
85
|
// `symbol` is the source of truth; `name` is derived from it consistently.
|
|
71
|
-
const displayName = deriveNameFromSymbol(c.symbol, c.
|
|
86
|
+
const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file);
|
|
72
87
|
// Set while a file is open in the drawer: true → spotlight, false → dim,
|
|
73
88
|
// absent (no file open) → neutral.
|
|
74
89
|
const fileMatch = data.fileMatch as boolean | undefined;
|
|
@@ -117,36 +132,91 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
117
132
|
fontFamily: theme.fonts.body,
|
|
118
133
|
}}
|
|
119
134
|
>
|
|
120
|
-
{/*
|
|
121
|
-
|
|
135
|
+
{/* Construct badge — a small tab riding the top-right border, in the
|
|
136
|
+
construct color. Persistent (no hover needed); pointer-events none so
|
|
137
|
+
clicks pass through to the node. */}
|
|
138
|
+
<div
|
|
139
|
+
style={{
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
top: -9,
|
|
142
|
+
left: 10,
|
|
143
|
+
zIndex: 1,
|
|
144
|
+
fontFamily: theme.fonts.monospace,
|
|
145
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
146
|
+
letterSpacing: 0.5,
|
|
147
|
+
textTransform: 'uppercase',
|
|
148
|
+
lineHeight: '17px',
|
|
149
|
+
color,
|
|
150
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
151
|
+
border: `1px solid ${color}`,
|
|
152
|
+
borderRadius: 4,
|
|
153
|
+
padding: '0 5px',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{CONSTRUCT_LABEL[c.construct] ?? c.construct}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Role badge — top-right, only when the node carries a topology role.
|
|
160
|
+
Role color (entry orange / service blue) on the badge; the node
|
|
161
|
+
border stays construct-colored. */}
|
|
162
|
+
{c.role != null && (
|
|
122
163
|
<div
|
|
123
164
|
style={{
|
|
124
165
|
position: 'absolute',
|
|
125
|
-
top: -
|
|
126
|
-
|
|
127
|
-
zIndex:
|
|
128
|
-
display: 'flex',
|
|
129
|
-
alignItems: 'center',
|
|
130
|
-
gap: 6,
|
|
131
|
-
whiteSpace: 'nowrap',
|
|
132
|
-
fontSize: theme.fontSizes[0] * 0.8,
|
|
166
|
+
top: -9,
|
|
167
|
+
right: 10,
|
|
168
|
+
zIndex: 1,
|
|
133
169
|
fontFamily: theme.fonts.monospace,
|
|
170
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
134
171
|
letterSpacing: 0.5,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
172
|
+
textTransform: 'uppercase',
|
|
173
|
+
lineHeight: '17px',
|
|
174
|
+
color: ROLE_COLOR[c.role],
|
|
175
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
176
|
+
border: `1px solid ${ROLE_COLOR[c.role]}`,
|
|
138
177
|
borderRadius: 4,
|
|
139
|
-
padding: '
|
|
178
|
+
padding: '0 5px',
|
|
140
179
|
}}
|
|
141
180
|
>
|
|
142
|
-
|
|
143
|
-
{KIND_LABEL[c.kind] ?? c.kind}
|
|
144
|
-
</span>
|
|
145
|
-
{c.file ? ` · ${c.file.split('/').pop()}` : ''}
|
|
146
|
-
{c.capture && c.capture !== 'edited' ? ` · ${c.capture}` : ''}
|
|
181
|
+
{ROLE_LABEL[c.role]}
|
|
147
182
|
</div>
|
|
148
183
|
)}
|
|
149
184
|
|
|
185
|
+
{/* Filename badge — a strip across the bottom border, shown on hover
|
|
186
|
+
and pinned while this node's file is open in the drawer (fileMatch).
|
|
187
|
+
Mirrors the construct/role tab badges; hidden for fileless nodes. */}
|
|
188
|
+
{(hover || fileMatch === true) && c.file ? (
|
|
189
|
+
<div
|
|
190
|
+
style={{
|
|
191
|
+
position: 'absolute',
|
|
192
|
+
bottom: -13,
|
|
193
|
+
left: '50%',
|
|
194
|
+
transform: 'translateX(-50%)',
|
|
195
|
+
zIndex: 1,
|
|
196
|
+
fontFamily: theme.fonts.monospace,
|
|
197
|
+
fontSize: theme.fontSizes[0] * 1.1,
|
|
198
|
+
letterSpacing: 0.5,
|
|
199
|
+
lineHeight: '17px',
|
|
200
|
+
whiteSpace: 'nowrap',
|
|
201
|
+
// filename stays neutral — the construct color belongs to the
|
|
202
|
+
// borders and badges, not the file name
|
|
203
|
+
color: theme.colors.text ?? theme.colors.textSecondary,
|
|
204
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
205
|
+
border: `1px solid ${color}`,
|
|
206
|
+
borderRadius: 4,
|
|
207
|
+
padding: '0 5px',
|
|
208
|
+
cursor: 'pointer',
|
|
209
|
+
}}
|
|
210
|
+
onClick={(e) => {
|
|
211
|
+
// Open the file directly; don't also toggle node selection.
|
|
212
|
+
e.stopPropagation();
|
|
213
|
+
SUBSYSTEM_CALLBACKS.onOpenFile?.(c.id);
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{c.file.split('/').pop()}
|
|
217
|
+
</div>
|
|
218
|
+
) : null}
|
|
219
|
+
|
|
150
220
|
{/* Purpose is no longer shown as a hover tooltip below the node — it
|
|
151
221
|
lives in the declaration panel and the graphify detail payload. */}
|
|
152
222
|
|
|
@@ -168,7 +238,11 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
168
238
|
{breakWords(displayName)}
|
|
169
239
|
</div>
|
|
170
240
|
|
|
171
|
-
{
|
|
241
|
+
{/* Hide the identity line when the symbol is just the title without its
|
|
242
|
+
decoration (`()` or ` {}`) — only show it when it adds information
|
|
243
|
+
(e.g. the dotted host on methods, or a different code identity). */}
|
|
244
|
+
{c.symbol &&
|
|
245
|
+
c.symbol !== displayName.replace(/ ?\{\}$/, '').replace(/\(\)$/, '') && (
|
|
172
246
|
<div
|
|
173
247
|
style={{
|
|
174
248
|
fontSize: theme.fontSizes[0] * 0.82,
|
|
@@ -204,7 +278,9 @@ export function SubsystemEdge({
|
|
|
204
278
|
const path = data?.elkPath ?? '';
|
|
205
279
|
const mechanism = data?.mechanism ?? 'imports';
|
|
206
280
|
const color = MECHANISM_COLOR[mechanism] ?? '#888';
|
|
207
|
-
|
|
281
|
+
// Dash style comes from the mechanism table (dashed = inverted-control or
|
|
282
|
+
// observational relationships: hierarchy, registration, watches).
|
|
283
|
+
const isDashed = MECHANISM_STYLE[mechanism] === 'dashed';
|
|
208
284
|
const dimmed = data?.dimmed === true;
|
|
209
285
|
const opacity = dimmed ? 0.15 : 1;
|
|
210
286
|
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Tokenize a SubsystemComponent into a flat token stream for the detail panel.
|
|
3
3
|
*
|
|
4
4
|
* Pipeline: generate declaration string → format with Prettier → tokenize
|
|
5
|
-
* with
|
|
6
|
-
* the entire pipeline (for pre-tokenized data from graphify).
|
|
5
|
+
* with Shiki + Pierre themes. The `component.tokens` field, when present,
|
|
6
|
+
* overrides the entire pipeline (for pre-tokenized data from graphify).
|
|
7
7
|
*
|
|
8
8
|
* This function is async because Prettier's format() is async.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
11
12
|
import type { SubsystemComponent, SubsystemDeclToken } from './model';
|
|
12
13
|
import { generateDeclarationString } from './formatDeclaration';
|
|
13
14
|
import { tokenizeFormatted } from './tokenizeFormatted';
|
|
@@ -40,21 +41,20 @@ async function getPrettier() {
|
|
|
40
41
|
* it's returned as-is. Otherwise the pipeline generates a TypeScript
|
|
41
42
|
* declaration string, formats it with Prettier, and tokenizes the output.
|
|
42
43
|
*/
|
|
43
|
-
export async function tokenizeComponent(
|
|
44
|
+
export async function tokenizeComponent(
|
|
45
|
+
component: SubsystemComponent,
|
|
46
|
+
printWidth = 80,
|
|
47
|
+
themeName: PierreSyntaxThemeName = 'pierre-dark',
|
|
48
|
+
): Promise<SubsystemDeclToken[]> {
|
|
44
49
|
// Pre-tokenized tokens from the wire take precedence.
|
|
45
50
|
if (component.tokens) return component.tokens;
|
|
46
51
|
|
|
47
52
|
// External kind — not valid TypeScript, bypass Prettier.
|
|
48
|
-
const kind = component.detail?.kind ?? component.
|
|
53
|
+
const kind = component.detail?.kind ?? component.construct;
|
|
49
54
|
if (kind === 'external') {
|
|
50
55
|
const label = component.detail?.kind === 'external' ? component.detail.label : component.name;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{ text: ' ', kind: 'punctuation' },
|
|
54
|
-
{ text: "'", kind: 'punctuation' },
|
|
55
|
-
{ text: label, kind: 'string' },
|
|
56
|
-
{ text: "'", kind: 'punctuation' },
|
|
57
|
-
];
|
|
56
|
+
const escaped = label.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
57
|
+
return tokenizeFormatted(`external '${escaped}'`, themeName);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// Generate → format → tokenize
|
|
@@ -66,5 +66,5 @@ export async function tokenizeComponent(component: SubsystemComponent, printWidt
|
|
|
66
66
|
printWidth,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
return tokenizeFormatted(formatted);
|
|
69
|
+
return tokenizeFormatted(formatted, themeName);
|
|
70
70
|
}
|
|
@@ -1,92 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tokenize a formatted TypeScript declaration string into SubsystemDeclToken[]
|
|
3
|
-
* using
|
|
4
|
-
*
|
|
5
|
-
* highlight.js produces HTML with `<span class="hljs-...">` wrappers. This
|
|
6
|
-
* module parses that output into our token format so the renderer stays
|
|
7
|
-
* language-agnostic.
|
|
3
|
+
* using Shiki + Pierre themes (same highlighter stack as @pierre/diffs).
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import type { SubsystemDeclToken
|
|
13
|
-
|
|
14
|
-
// Register once — safe to call multiple times.
|
|
15
|
-
try {
|
|
16
|
-
hljs.registerLanguage('typescript', typescript);
|
|
17
|
-
} catch {
|
|
18
|
-
// Already registered.
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// hljs class → our token kind
|
|
22
|
-
const CLASS_MAP: Record<string, SubsystemDeclTokenKind> = {
|
|
23
|
-
'hljs-keyword': 'keyword',
|
|
24
|
-
'hljs-title': 'name',
|
|
25
|
-
'hljs-title.function_': 'name',
|
|
26
|
-
'hljs-params': 'member',
|
|
27
|
-
'hljs-type': 'type',
|
|
28
|
-
'hljs-built_in': 'type',
|
|
29
|
-
'hljs-string': 'string',
|
|
30
|
-
'hljs-number': 'string',
|
|
31
|
-
'hljs-comment': 'punctuation',
|
|
32
|
-
};
|
|
6
|
+
import { tokenizeWithPierreSyntax } from '../pierre/syntaxTokens';
|
|
7
|
+
import type { PierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
|
|
8
|
+
import type { SubsystemDeclToken } from './model';
|
|
33
9
|
|
|
34
10
|
/**
|
|
35
11
|
* Tokenize a formatted TypeScript string into SubsystemDeclToken[].
|
|
36
12
|
* The input should already be formatted by Prettier (or be valid TS).
|
|
37
13
|
*/
|
|
38
|
-
export function tokenizeFormatted(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// HTML parser — walks the hljs output and extracts tokens
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
function unescapeHtml(text: string): string {
|
|
48
|
-
return text
|
|
49
|
-
.replace(/"/g, '"')
|
|
50
|
-
.replace(/</g, '<')
|
|
51
|
-
.replace(/>/g, '>')
|
|
52
|
-
.replace(/&/g, '&')
|
|
53
|
-
.replace(/'/g, "'");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parseHighlightedHtml(html: string): SubsystemDeclToken[] {
|
|
57
|
-
const tokens: SubsystemDeclToken[] = [];
|
|
58
|
-
// Regex matches either `<span class="hljs-...">`, `</span>`, or text content.
|
|
59
|
-
const re = /<span class="([^"]+)">|<\/span>|[^<]+/g;
|
|
60
|
-
const classStack: string[] = [];
|
|
61
|
-
let match: RegExpExecArray | null;
|
|
62
|
-
|
|
63
|
-
while ((match = re.exec(html)) !== null) {
|
|
64
|
-
const chunk = match[0];
|
|
65
|
-
|
|
66
|
-
if (chunk.startsWith('<span')) {
|
|
67
|
-
// Opening tag — push class onto stack.
|
|
68
|
-
classStack.push(match[1]);
|
|
69
|
-
} else if (chunk === '</span>') {
|
|
70
|
-
// Closing tag — pop class.
|
|
71
|
-
classStack.pop();
|
|
72
|
-
} else {
|
|
73
|
-
// Text content — emit tokens for each line segment.
|
|
74
|
-
const currentClass = classStack[classStack.length - 1] ?? '';
|
|
75
|
-
const kind = CLASS_MAP[currentClass] ?? 'punctuation';
|
|
76
|
-
const text = chunk;
|
|
77
|
-
|
|
78
|
-
// Split on newlines so each line becomes its own token set.
|
|
79
|
-
const lines = text.split('\n');
|
|
80
|
-
for (let i = 0; i < lines.length; i++) {
|
|
81
|
-
if (i > 0) {
|
|
82
|
-
tokens.push({ text: '', kind: 'newline' });
|
|
83
|
-
}
|
|
84
|
-
if (lines[i]) {
|
|
85
|
-
tokens.push({ text: unescapeHtml(lines[i]), kind });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return tokens;
|
|
14
|
+
export function tokenizeFormatted(
|
|
15
|
+
code: string,
|
|
16
|
+
themeName: PierreSyntaxThemeName = 'pierre-dark',
|
|
17
|
+
): Promise<SubsystemDeclToken[]> {
|
|
18
|
+
return tokenizeWithPierreSyntax(code, themeName);
|
|
92
19
|
}
|