@principal-ai/principal-view-react 0.16.56 → 0.16.58
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 +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- 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/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 +12 -11
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +401 -40
- 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 +89 -12
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +55 -25
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +32 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +122 -24
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +1 -1
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +4 -3
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -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 +3 -2
- package/src/pierre/constructColors.test.ts +52 -0
- package/src/pierre/constructColors.ts +112 -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/Flows.stories.tsx +181 -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 +13 -13
- package/src/subsystem/SubsystemComponentGraph.tsx +558 -44
- package/src/subsystem/formatDeclaration.ts +57 -11
- package/src/subsystem/model.test.ts +38 -6
- package/src/subsystem/model.ts +145 -32
- package/src/subsystem/nodes.test.ts +56 -0
- package/src/subsystem/nodes.tsx +161 -31
- package/src/subsystem/tokenizeComponent.ts +1 -1
- package/src/utils/elkLayout.ts +4 -3
- package/src/stories/SubsystemComponentGraph.stories.tsx +0 -1225
- /package/src/stories/{dashboard/sample-dashboards → data}/activity-feed-analytics.dashboard.json +0 -0
|
@@ -7,32 +7,46 @@
|
|
|
7
7
|
* handles all formatting.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { SubsystemComponent } from './model';
|
|
10
|
+
import type { SubsystemComponent, SubsystemComponentConstruct } from './model';
|
|
11
11
|
import type { GraphifyComponentDetail } from '../graphify';
|
|
12
12
|
|
|
13
|
+
const TYPE_FAMILY_CONSTRUCTS: ReadonlySet<string> = new Set([
|
|
14
|
+
'interface',
|
|
15
|
+
'type_alias',
|
|
16
|
+
'enum',
|
|
17
|
+
]);
|
|
18
|
+
|
|
13
19
|
export function generateDeclarationString(component: SubsystemComponent): string {
|
|
14
20
|
const detail = component.detail;
|
|
15
|
-
|
|
21
|
+
// Type-family constructs own their rendering even when the detail payload
|
|
22
|
+
// is the shared `type` shape — the construct says which keyword is honest.
|
|
23
|
+
const construct = TYPE_FAMILY_CONSTRUCTS.has(component.construct)
|
|
24
|
+
? component.construct
|
|
25
|
+
: detail?.kind ?? component.construct;
|
|
16
26
|
const rawName = component.symbol || component.name || 'untitled';
|
|
17
27
|
// Strip class/object prefix from dotted symbols (e.g. "SessionReader.normalize" → "normalize").
|
|
18
28
|
const name = rawName.includes('.') ? rawName.split('.').pop()! : rawName;
|
|
19
29
|
|
|
20
|
-
switch (
|
|
30
|
+
switch (construct) {
|
|
21
31
|
case 'class':
|
|
22
32
|
return generateClass(name, detail);
|
|
23
33
|
case 'function':
|
|
24
34
|
return generateFunction(name, detail);
|
|
25
35
|
case 'method':
|
|
26
36
|
return generateMethod(name, detail);
|
|
27
|
-
case '
|
|
28
|
-
|
|
37
|
+
case 'interface':
|
|
38
|
+
case 'type_alias':
|
|
39
|
+
case 'enum':
|
|
40
|
+
return generateType(name, component.construct, detail);
|
|
29
41
|
case 'module':
|
|
30
42
|
return generateModule(detail);
|
|
43
|
+
case 'store':
|
|
44
|
+
return generateStore(name, detail);
|
|
31
45
|
case 'external':
|
|
32
46
|
// Not valid TypeScript — caller should handle formatting.
|
|
33
47
|
return `external '${detail?.kind === 'external' ? detail.label : name}'`;
|
|
34
48
|
default:
|
|
35
|
-
return `${
|
|
49
|
+
return `${construct} ${name}`;
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
|
|
@@ -48,7 +62,7 @@ function formatParams(params: { name?: string; type: string }[]): string {
|
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
// ---------------------------------------------------------------------------
|
|
51
|
-
// Per-
|
|
65
|
+
// Per-construct generators
|
|
52
66
|
// ---------------------------------------------------------------------------
|
|
53
67
|
|
|
54
68
|
function generateClass(name: string, detail?: GraphifyComponentDetail): string {
|
|
@@ -98,16 +112,31 @@ function generateMethod(name: string, detail?: GraphifyComponentDetail): string
|
|
|
98
112
|
return `class ${hostClass} {\n ${name}(${params})${ret};\n}`;
|
|
99
113
|
}
|
|
100
114
|
|
|
101
|
-
function generateType(
|
|
115
|
+
function generateType(
|
|
116
|
+
name: string,
|
|
117
|
+
construct: SubsystemComponentConstruct,
|
|
118
|
+
detail?: GraphifyComponentDetail,
|
|
119
|
+
): string {
|
|
120
|
+
// The type-family constructs render their declaration keyword honestly —
|
|
121
|
+
// the construct itself says interface / type (alias) / enum / variable.
|
|
102
122
|
const tpe = detail?.kind === 'type' ? detail : undefined;
|
|
103
123
|
const props = (tpe?.properties ?? [])
|
|
104
124
|
.map((p) => ` ${p.name}${p.type ? `: ${p.type}` : ''};`)
|
|
105
125
|
.join('\n');
|
|
106
126
|
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
switch (construct) {
|
|
128
|
+
case 'enum':
|
|
129
|
+
return `enum ${name} { ${(tpe?.properties ?? []).map((p) => p.name).join(', ')} }`;
|
|
130
|
+
case 'type_alias':
|
|
131
|
+
return props
|
|
132
|
+
? `type ${name} = {\n${props}\n};`
|
|
133
|
+
: `type ${name} = unknown;`;
|
|
134
|
+
default:
|
|
135
|
+
if (props) {
|
|
136
|
+
return `interface ${name} {\n${props}\n}`;
|
|
137
|
+
}
|
|
138
|
+
return `interface ${name} {}`;
|
|
109
139
|
}
|
|
110
|
-
return `interface ${name} {}`;
|
|
111
140
|
}
|
|
112
141
|
|
|
113
142
|
function generateModule(detail?: GraphifyComponentDetail): string {
|
|
@@ -126,3 +155,20 @@ function generateModule(detail?: GraphifyComponentDetail): string {
|
|
|
126
155
|
|
|
127
156
|
return parts.join('\n') || `module {}`;
|
|
128
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A store renders as its retained state — ambient `declare const` lines for
|
|
161
|
+
* the state members, never a class/method stub. The node's name labels the
|
|
162
|
+
* block; the access mechanism lives in separate accessor nodes.
|
|
163
|
+
*/
|
|
164
|
+
function generateStore(name: string, detail?: GraphifyComponentDetail): string {
|
|
165
|
+
const store = detail?.kind === 'store' ? detail : undefined;
|
|
166
|
+
const props = (store?.properties ?? [])
|
|
167
|
+
.map((p) => `declare const ${p.name}${p.type ? `: ${p.type}` : ''};`)
|
|
168
|
+
.join('\n');
|
|
169
|
+
|
|
170
|
+
if (!props) {
|
|
171
|
+
return `// store: ${name} — no captured state members`;
|
|
172
|
+
}
|
|
173
|
+
return `// store: ${name}\n${props}`;
|
|
174
|
+
}
|
|
@@ -36,11 +36,30 @@ describe('subsystem graph model', () => {
|
|
|
36
36
|
expect(converted[0].target).toBe('reader');
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
test('buildSubsystemGraph tolerates external components with no file', async () => {
|
|
40
|
+
const withExternal: SubsystemComponent[] = [
|
|
41
|
+
...comps,
|
|
42
|
+
{
|
|
43
|
+
id: "proposed-watcher",
|
|
44
|
+
name: "watchDir",
|
|
45
|
+
kind: "external",
|
|
46
|
+
// Intentionally omit file/purl — agents often leave these off for externals.
|
|
47
|
+
file: undefined as unknown as string,
|
|
48
|
+
purl: undefined as unknown as string,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
const { nodes } = await buildSubsystemGraph({
|
|
52
|
+
components: withExternal,
|
|
53
|
+
edges: [{ id: 'e3', from: 'reader', to: 'proposed-watcher', mechanism: 'feeds' }],
|
|
54
|
+
});
|
|
55
|
+
expect(nodes.find((n) => n.id === 'proposed-watcher')).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
39
58
|
test('buildSubsystemGraph creates external stub nodes for non-component targets', async () => {
|
|
40
59
|
const { nodes, edges: gEdges } = await buildSubsystemGraph({ components: comps, edges });
|
|
41
60
|
const external = nodes.find((n) => n.id === 'external:host');
|
|
42
61
|
expect(external).toBeDefined();
|
|
43
|
-
expect(external!.data.component.
|
|
62
|
+
expect(external!.data.component.construct).toBe('external');
|
|
44
63
|
const crossEdge = gEdges.find((e) => e.target === 'external:host');
|
|
45
64
|
expect(crossEdge).toBeDefined();
|
|
46
65
|
});
|
|
@@ -62,14 +81,27 @@ describe('subsystem graph model', () => {
|
|
|
62
81
|
});
|
|
63
82
|
|
|
64
83
|
test('deriveNameFromSymbol is consistent per kind', () => {
|
|
65
|
-
//
|
|
66
|
-
expect(deriveNameFromSymbol('SessionReader', 'class')).toBe('SessionReader');
|
|
67
|
-
expect(deriveNameFromSymbol('SessionRecord', '
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
// brace-bodied constructs wear {}; module uses the symbol as-is.
|
|
85
|
+
expect(deriveNameFromSymbol('SessionReader', 'class')).toBe('SessionReader {}');
|
|
86
|
+
expect(deriveNameFromSymbol('SessionRecord', 'type_alias')).toBe('SessionRecord {}');
|
|
87
|
+
expect(deriveNameFromSymbol('transcript', 'module')).toBe('transcript');
|
|
88
|
+
// falls back to existing name when no symbol (still brace-decorated).
|
|
89
|
+
expect(deriveNameFromSymbol(undefined, 'class', 'SessionReader')).toBe('SessionReader {}');
|
|
70
90
|
expect(deriveNameFromSymbol('', 'external', 'trail-viewer-host')).toBe('trail-viewer-host');
|
|
71
91
|
});
|
|
72
92
|
|
|
93
|
+
test('executable constructs wear () on the node', () => {
|
|
94
|
+
expect(deriveNameFromSymbol('createSubsystemGraph', 'function')).toBe('createSubsystemGraph()');
|
|
95
|
+
// methods keep the dotted ownership symbol and wear the parens
|
|
96
|
+
expect(deriveNameFromSymbol('SessionCache.put', 'method')).toBe('SessionCache.put()');
|
|
97
|
+
// already-parenthesized labels don't double up
|
|
98
|
+
expect(deriveNameFromSymbol('run()', 'function')).toBe('run()');
|
|
99
|
+
// data-shaped constructs stay bare
|
|
100
|
+
expect(deriveNameFromSymbol('ROOT', 'store')).toBe('ROOT');
|
|
101
|
+
// brace bodies don't double up
|
|
102
|
+
expect(deriveNameFromSymbol('Foo {}', 'class')).toBe('Foo {}');
|
|
103
|
+
});
|
|
104
|
+
|
|
73
105
|
test('deriveNameFromSymbol falls back to file basename for modules', () => {
|
|
74
106
|
expect(deriveNameFromSymbol(undefined, 'module', undefined, 'transcript.ts')).toBe('transcript');
|
|
75
107
|
expect(deriveNameFromSymbol(undefined, 'module', undefined, 'src/event-processors/index.ts')).toBe('index');
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -65,19 +81,48 @@ export type SubsystemEdgeMechanism =
|
|
|
65
81
|
| 'contains'
|
|
66
82
|
| 'feeds'
|
|
67
83
|
| 'produces'
|
|
84
|
+
| 'writes'
|
|
85
|
+
| 'reads'
|
|
86
|
+
| 'watches'
|
|
68
87
|
| 'registers-into';
|
|
69
88
|
|
|
70
|
-
/** A component node — the named unit,
|
|
89
|
+
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
71
90
|
export interface SubsystemComponent {
|
|
72
91
|
id: string;
|
|
73
92
|
name: string;
|
|
74
|
-
|
|
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;
|
|
75
103
|
/** Source location the component lives in (repo-root-relative path). */
|
|
76
104
|
file: string;
|
|
77
105
|
/** PURL identifying the repo or package this component lives in (for subgraph grouping). */
|
|
78
106
|
purl: string;
|
|
79
107
|
/** One-line purpose shown on the node. */
|
|
80
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;
|
|
81
126
|
/** A symbol this component exposes / is (the node's identity). */
|
|
82
127
|
symbol?: string;
|
|
83
128
|
/**
|
|
@@ -96,7 +141,7 @@ export interface SubsystemComponent {
|
|
|
96
141
|
capture?: 'edited' | 'analyzed' | 'referenced';
|
|
97
142
|
/**
|
|
98
143
|
* Graphify drill-down detail for this component, when the facet is anchored
|
|
99
|
-
* to a graphify node. Discriminated by kind (class/function/type/module/
|
|
144
|
+
* to a graphify node. Discriminated by kind (class/function/type/module/store/
|
|
100
145
|
* external); rendered in the detail panel on click.
|
|
101
146
|
*/
|
|
102
147
|
detail?: GraphifyComponentDetail;
|
|
@@ -133,10 +178,42 @@ export interface SubsystemComponentEdge {
|
|
|
133
178
|
}
|
|
134
179
|
|
|
135
180
|
/**
|
|
136
|
-
*
|
|
181
|
+
* A single site on an existing edge — the exact `file:line` where that edge's
|
|
182
|
+
* seam manifests for a given flow. The edge stays the abstract contract
|
|
183
|
+
* (`from`, `to`, `mechanism`); a throughline step picks the concrete
|
|
184
|
+
* manifestation. One edge can appear in many steps.
|
|
185
|
+
*/
|
|
186
|
+
export interface SubsystemThroughlineStep {
|
|
187
|
+
/** Id of the existing edge this hop traverses. */
|
|
188
|
+
edgeId: string;
|
|
189
|
+
/** Repo-root-relative path of the file where the edge fires. */
|
|
190
|
+
file: string;
|
|
191
|
+
/** 1-based line of the site within `file`. */
|
|
192
|
+
line: number;
|
|
193
|
+
/**
|
|
194
|
+
* Frame name for this hop — the function/method/symbol on the stack at
|
|
195
|
+
* this site. Optional so existing throughlines keep working; when set the
|
|
196
|
+
* flows list shows it instead of mechanism + filename.
|
|
197
|
+
*/
|
|
198
|
+
symbol?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* An ordered execution story over a graph's edges — each step references an
|
|
203
|
+
* existing edge and the exact site where that relationship fires for a flow;
|
|
204
|
+
* ordering is the array. One throughline per flow (save flow, load flow, …).
|
|
205
|
+
*/
|
|
206
|
+
export interface SubsystemThroughline {
|
|
207
|
+
id: string;
|
|
208
|
+
title: string;
|
|
209
|
+
steps: SubsystemThroughlineStep[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Derive a consistent display `name` from a code `symbol` + construct.
|
|
137
214
|
*
|
|
138
|
-
* `symbol` is the source of truth (fully-qualified code identity).
|
|
139
|
-
*
|
|
215
|
+
* `symbol` is the source of truth (fully-qualified code identity). The name
|
|
216
|
+
* is the symbol itself:
|
|
140
217
|
* - class/type/module/function/script/... symbol → symbol (e.g. `SessionReader`)
|
|
141
218
|
* - method `Owner.method` → last segment (e.g. `SessionReader.normalize` → `normalize`)
|
|
142
219
|
* - module, no symbol → basename of `file` (e.g. `transcript.ts` → `transcript`) —
|
|
@@ -145,20 +222,37 @@ export interface SubsystemComponentEdge {
|
|
|
145
222
|
*/
|
|
146
223
|
export function deriveNameFromSymbol(
|
|
147
224
|
symbol: string | undefined,
|
|
148
|
-
|
|
225
|
+
construct: SubsystemComponentConstruct,
|
|
149
226
|
existingName?: string,
|
|
150
227
|
file?: string,
|
|
151
228
|
): string {
|
|
229
|
+
let name: string | undefined;
|
|
152
230
|
if (symbol && symbol.trim()) {
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
// Module nodes without a symbol are whole files → use the file basename.
|
|
156
|
-
if (kind === 'module' && file) {
|
|
231
|
+
name = symbol;
|
|
232
|
+
} else if (construct === 'module' && file) {
|
|
157
233
|
const base = file.split('/').pop() ?? '';
|
|
158
234
|
const clean = base.replace(/\.[^.]+$/, ''); // strip extension
|
|
159
|
-
if (clean)
|
|
235
|
+
if (clean) name = clean;
|
|
236
|
+
}
|
|
237
|
+
if (!name) name = existingName ?? 'untitled';
|
|
238
|
+
|
|
239
|
+
// Decoration = what the drill-down shows. Executable constructs wear `()`
|
|
240
|
+
// (a signature you can call); brace-bodied constructs wear ` {}` (a member
|
|
241
|
+
// body — fields for types/interfaces/enums, fields+methods for classes).
|
|
242
|
+
// Everything else (store, variable, module, external) renders bare.
|
|
243
|
+
if ((construct === 'function' || construct === 'method') && !name.endsWith('()')) {
|
|
244
|
+
name = `${name}()`;
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
(construct === 'class' ||
|
|
248
|
+
construct === 'interface' ||
|
|
249
|
+
construct === 'type_alias' ||
|
|
250
|
+
construct === 'enum') &&
|
|
251
|
+
!name.endsWith('{}')
|
|
252
|
+
) {
|
|
253
|
+
name = `${name} {}`;
|
|
160
254
|
}
|
|
161
|
-
return
|
|
255
|
+
return name;
|
|
162
256
|
}
|
|
163
257
|
|
|
164
258
|
/**
|
|
@@ -188,6 +282,8 @@ export function formatPurl(purl: string): string {
|
|
|
188
282
|
export interface SubsystemGraphDocument {
|
|
189
283
|
components: SubsystemComponent[];
|
|
190
284
|
edges: SubsystemComponentEdge[];
|
|
285
|
+
/** Ordered execution stories over the graph's edges (one per flow). */
|
|
286
|
+
throughlines?: SubsystemThroughline[];
|
|
191
287
|
}
|
|
192
288
|
|
|
193
289
|
// ---------------------------------------------------------------------------
|
|
@@ -202,6 +298,8 @@ export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
|
202
298
|
* lives in that file (spotlighted), false otherwise (dimmed). Absent when
|
|
203
299
|
* no file is open — render neutrally. */
|
|
204
300
|
fileMatch?: boolean;
|
|
301
|
+
/** True while this node is on an opened-but-unselected flow. */
|
|
302
|
+
dimmed?: boolean;
|
|
205
303
|
}
|
|
206
304
|
|
|
207
305
|
export type SubsystemGraphNode = Node<SubsystemGraphNodeData, SubsystemGraphNodeType>;
|
|
@@ -237,6 +335,9 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
237
335
|
contains: '#6c5ce7', // indigo
|
|
238
336
|
feeds: '#22c55e', // green — data-flow into a processor
|
|
239
337
|
produces: '#e07a5f', // terracotta — emits an output type
|
|
338
|
+
writes: '#2f9e44', // deep green — mutates retained state
|
|
339
|
+
reads: '#0ea5e9', // sky — pulls from retained state
|
|
340
|
+
watches: '#9ca3af', // gray — observes, owns nothing
|
|
240
341
|
'registers-into': '#ff6b35', // orange
|
|
241
342
|
};
|
|
242
343
|
|
|
@@ -256,6 +357,9 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
256
357
|
contains: 'solid',
|
|
257
358
|
feeds: 'solid',
|
|
258
359
|
produces: 'solid',
|
|
360
|
+
writes: 'solid',
|
|
361
|
+
reads: 'solid',
|
|
362
|
+
watches: 'dashed',
|
|
259
363
|
'registers-into': 'dashed',
|
|
260
364
|
};
|
|
261
365
|
|
|
@@ -274,14 +378,19 @@ export function packageColor(name: string): string {
|
|
|
274
378
|
return palette[h % palette.length];
|
|
275
379
|
}
|
|
276
380
|
|
|
277
|
-
/** Color per
|
|
278
|
-
|
|
279
|
-
class
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
381
|
+
/** Color per semantic role — applied ONLY to the role badge (top-right tab on
|
|
382
|
+
* nodes that carry a role). The node border stays construct-colored. Note:
|
|
383
|
+
* `service` currently equals the old class hex; harmless while role badges
|
|
384
|
+
* are the only surface using it, but pick a distinct value if roles ever
|
|
385
|
+
* take over node borders. */
|
|
386
|
+
export const ROLE_COLOR: Record<SubsystemComponentRole, string> = {
|
|
387
|
+
entry: '#ff6b35', // orange — boundary element
|
|
388
|
+
service: '#0893d2', // blue — external system
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
|
|
392
|
+
entry: 'entry',
|
|
393
|
+
service: 'service',
|
|
285
394
|
};
|
|
286
395
|
|
|
287
396
|
/**
|
|
@@ -297,12 +406,16 @@ export function convertSubsystemToNodes(
|
|
|
297
406
|
opts: { maxNodeWidth?: number } = {},
|
|
298
407
|
): SubsystemGraphNode[] {
|
|
299
408
|
const { maxNodeWidth } = opts;
|
|
300
|
-
// Group components by
|
|
409
|
+
// Group components into boundary regions by process when authored, else by
|
|
410
|
+
// package. Process is runtime membership (drawn as one boundary region);
|
|
411
|
+
// purl is code identity — nodes without a process (external actors,
|
|
412
|
+
// services, libraries) fall back to purl and sit outside every boundary.
|
|
301
413
|
const byPkg = new Map<string, SubsystemComponent[]>();
|
|
302
414
|
for (const c of doc.components) {
|
|
303
|
-
const
|
|
415
|
+
const regionKey = c.process ?? c.purl;
|
|
416
|
+
const list = byPkg.get(regionKey) ?? [];
|
|
304
417
|
list.push(c);
|
|
305
|
-
byPkg.set(
|
|
418
|
+
byPkg.set(regionKey, list);
|
|
306
419
|
}
|
|
307
420
|
|
|
308
421
|
const nodes: SubsystemGraphNode[] = [];
|
|
@@ -322,7 +435,7 @@ export function convertSubsystemToNodes(
|
|
|
322
435
|
const row = Math.floor(i / COLS);
|
|
323
436
|
// Estimate rendered node width from the longest text line so ELK reserves
|
|
324
437
|
// the right space (names can wrap, so we cap at the configurable max).
|
|
325
|
-
const text = [c.symbol, c.name, c.file
|
|
438
|
+
const text = [c.symbol, c.name, c.file?.split('/').pop() ?? '']
|
|
326
439
|
.filter((t): t is string => !!t)
|
|
327
440
|
.sort((a, b) => b.length - a.length)[0];
|
|
328
441
|
const cap = maxNodeWidth ?? 300;
|
|
@@ -370,7 +483,7 @@ export function convertSubsystemToEdges(doc: SubsystemGraphDocument): SubsystemG
|
|
|
370
483
|
target: targetId,
|
|
371
484
|
data: { mechanism: e.mechanism, refs: e.refs },
|
|
372
485
|
type: 'subsystem-edge',
|
|
373
|
-
markerEnd: { type: MarkerType.ArrowClosed, color, width:
|
|
486
|
+
markerEnd: { type: MarkerType.ArrowClosed, color, width: 32, height: 32 },
|
|
374
487
|
style: { color, stroke: color, strokeDasharray: style === 'dashed' ? '6 4' : undefined },
|
|
375
488
|
// `label` feeds ELK's label-space reservation only; the visible label is
|
|
376
489
|
// rendered by the custom SubsystemEdge as an HTML overlay.
|
|
@@ -387,8 +500,8 @@ export function subsystemGraphLayoutKey(
|
|
|
387
500
|
doc: Pick<SubsystemGraphDocument, 'components' | 'edges'>,
|
|
388
501
|
): string {
|
|
389
502
|
const components = doc.components
|
|
390
|
-
.map(({ id, purl, name, symbol,
|
|
391
|
-
[id, purl, name, symbol ?? '',
|
|
503
|
+
.map(({ id, purl, name, symbol, construct, file, purpose, process }) =>
|
|
504
|
+
[id, purl, name, symbol ?? '', construct, file, purpose ?? '', process ?? ''].join('\0'))
|
|
392
505
|
.sort()
|
|
393
506
|
.join('\n');
|
|
394
507
|
const edgeKey = doc.edges
|
|
@@ -437,7 +550,7 @@ export async function buildSubsystemGraph(
|
|
|
437
550
|
component: {
|
|
438
551
|
id: extId,
|
|
439
552
|
name: label,
|
|
440
|
-
|
|
553
|
+
construct: 'external',
|
|
441
554
|
purl: 'external',
|
|
442
555
|
file: '',
|
|
443
556
|
purpose: 'cross-package integration target (not a member node)',
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility, hexWithAlpha } from './nodes';
|
|
3
|
+
|
|
4
|
+
describe('hexWithAlpha', () => {
|
|
5
|
+
test('appends a two-digit alpha to #rrggbb', () => {
|
|
6
|
+
expect(hexWithAlpha('#4ec9b0', 1)).toBe('#4ec9b0ff');
|
|
7
|
+
expect(hexWithAlpha('#4ec9b0', EDGE_DIM_ALPHA)).toBe('#4ec9b026');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('expands #rgb', () => {
|
|
11
|
+
expect(hexWithAlpha('#abc', 1)).toBe('#aabbccff');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('leaves non-hex values alone', () => {
|
|
15
|
+
expect(hexWithAlpha('teal', 0.15)).toBe('teal');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('fileMatchForNode', () => {
|
|
20
|
+
test('no open file → neutral', () => {
|
|
21
|
+
expect(fileMatchForNode('src/a.ts', null, false)).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('open file spotlights matches and dims others', () => {
|
|
25
|
+
expect(fileMatchForNode('src/a.ts', 'src/a.ts', false)).toBe(true);
|
|
26
|
+
expect(fileMatchForNode('src/b.ts', 'src/a.ts', false)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('focused-edge endpoint is not dimmed when it lives in another file', () => {
|
|
30
|
+
expect(fileMatchForNode('src/target.ts', 'src/source.ts', true)).toBeUndefined();
|
|
31
|
+
expect(fileMatchForNode('src/source.ts', 'src/source.ts', true)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('flowElementVisibility', () => {
|
|
36
|
+
test('nothing open or selected → everything full', () => {
|
|
37
|
+
expect(flowElementVisibility({ inOpened: false, inSelected: false, anyOpened: false, anySelected: false }))
|
|
38
|
+
.toEqual({ hidden: false, dimmed: false });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('opened, nothing selected → opened members full, rest hidden', () => {
|
|
42
|
+
expect(flowElementVisibility({ inOpened: true, inSelected: false, anyOpened: true, anySelected: false }))
|
|
43
|
+
.toEqual({ hidden: false, dimmed: false });
|
|
44
|
+
expect(flowElementVisibility({ inOpened: false, inSelected: false, anyOpened: true, anySelected: false }))
|
|
45
|
+
.toEqual({ hidden: true, dimmed: false });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('selected flow or step full; other opened members dimmed; rest hidden', () => {
|
|
49
|
+
expect(flowElementVisibility({ inOpened: true, inSelected: true, anyOpened: true, anySelected: true }))
|
|
50
|
+
.toEqual({ hidden: false, dimmed: false });
|
|
51
|
+
expect(flowElementVisibility({ inOpened: true, inSelected: false, anyOpened: true, anySelected: true }))
|
|
52
|
+
.toEqual({ hidden: false, dimmed: true });
|
|
53
|
+
expect(flowElementVisibility({ inOpened: false, inSelected: false, anyOpened: true, anySelected: true }))
|
|
54
|
+
.toEqual({ hidden: true, dimmed: false });
|
|
55
|
+
});
|
|
56
|
+
});
|