@principal-ai/subsystems-react 0.37.12 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graphify/consolidated.d.ts +8 -0
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/construct.d.ts +18 -0
- package/dist/graphify/construct.d.ts.map +1 -1
- package/dist/graphify/construct.js +23 -0
- package/dist/graphify/construct.js.map +1 -1
- 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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +49 -3
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +180 -79
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +23 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +492 -23
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.js +19 -5
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +161 -7
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +91 -12
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +133 -72
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/symbolRefs.d.ts.map +1 -1
- package/dist/subsystem/symbolRefs.js +2 -0
- package/dist/subsystem/symbolRefs.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +9 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +21 -2
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/graphify/__fixtures__/ego-samples.json +545 -0
- package/src/graphify/consolidated.ts +8 -0
- package/src/graphify/construct.test.ts +61 -1
- package/src/graphify/construct.ts +33 -0
- package/src/graphify/index.ts +6 -1
- package/src/index.ts +13 -1
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -9
- package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +625 -0
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +7 -1
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +5 -5
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +5 -5
- package/src/stories/Subsystem/ComponentGraph/NodeAnatomy.stories.tsx +40 -73
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/StoreValueType.stories.tsx +261 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +354 -0
- package/src/stories/Subsystem/StoreValueTypeDeclaration.stories.tsx +182 -0
- package/src/subsystem/IssueList.focus.test.tsx +113 -0
- package/src/subsystem/IssueList.icon.test.tsx +95 -0
- package/src/subsystem/IssueList.tsx +267 -111
- package/src/subsystem/SubsystemComponentGraph.tsx +582 -27
- package/src/subsystem/formatDeclaration.test.ts +64 -0
- package/src/subsystem/formatDeclaration.ts +20 -5
- package/src/subsystem/model.test.ts +75 -2
- package/src/subsystem/model.ts +247 -17
- package/src/subsystem/nodes.group.test.tsx +52 -0
- package/src/subsystem/nodes.tsx +112 -8
- package/src/subsystem/symbolRefs.ts +2 -0
- package/src/subsystem/toC4.test.ts +3 -3
- package/src/utils/elkLayout.ts +32 -1
|
@@ -2,6 +2,70 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import { generateDeclarationString, resolveComponentDeclaration } from './formatDeclaration';
|
|
3
3
|
import type { SubsystemComponent } from './model';
|
|
4
4
|
|
|
5
|
+
describe('generateDeclarationString — store', () => {
|
|
6
|
+
const base: Omit<SubsystemComponent, 'declaration'> = {
|
|
7
|
+
id: 'st',
|
|
8
|
+
name: 'feeds',
|
|
9
|
+
construct: 'store',
|
|
10
|
+
symbol: 'feeds',
|
|
11
|
+
file: 'src/live.ts',
|
|
12
|
+
purl: 'pkg:github/acme/app',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
test('renders a declared valueType as the store signature', () => {
|
|
16
|
+
const component: SubsystemComponent = {
|
|
17
|
+
...base,
|
|
18
|
+
declaration: {
|
|
19
|
+
kind: 'store',
|
|
20
|
+
storage: 'memory',
|
|
21
|
+
properties: [],
|
|
22
|
+
valueType: 'Map<string, OpencodeLiveFeedState>',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
expect(generateDeclarationString(component)).toBe(
|
|
26
|
+
'// store: feeds — Map<string, OpencodeLiveFeedState>\n// backing: memory',
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('named members render as a body under the valueType', () => {
|
|
31
|
+
const component: SubsystemComponent = {
|
|
32
|
+
...base,
|
|
33
|
+
declaration: {
|
|
34
|
+
kind: 'store',
|
|
35
|
+
storage: 'memory',
|
|
36
|
+
valueType: 'Map<string, { width: number }>',
|
|
37
|
+
properties: [{ name: 'current', type: 'Map<string, { width: number }>' }],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
expect(generateDeclarationString(component)).toBe(
|
|
41
|
+
'// store: feeds — Map<string, { width: number }>\n// backing: memory\n current: Map<string, { width: number }>;',
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('members without a valueType still render as declare const lines', () => {
|
|
46
|
+
const component: SubsystemComponent = {
|
|
47
|
+
...base,
|
|
48
|
+
declaration: {
|
|
49
|
+
kind: 'store',
|
|
50
|
+
properties: [{ name: 'ROOT', type: 'string' }],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
expect(generateDeclarationString(component)).toBe(
|
|
54
|
+
'// store: feeds\ndeclare const ROOT: string;',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('a store with neither valueType nor members says so explicitly', () => {
|
|
59
|
+
const component: SubsystemComponent = {
|
|
60
|
+
...base,
|
|
61
|
+
declaration: { kind: 'store', storage: 'memory', properties: [] },
|
|
62
|
+
};
|
|
63
|
+
expect(generateDeclarationString(component)).toBe(
|
|
64
|
+
'// store: feeds — no declared type (no valueType or state members)\n// backing: memory',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
5
69
|
describe('generateDeclarationString — custom_entity', () => {
|
|
6
70
|
test('renders entity identity + entityKind, no attributes when none authored', () => {
|
|
7
71
|
const component: SubsystemComponent = {
|
|
@@ -223,19 +223,34 @@ function generateType(
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* A store renders as its retained state —
|
|
227
|
-
* the
|
|
228
|
-
* block; the access mechanism lives in separate accessor nodes.
|
|
226
|
+
* A store renders as its retained state — the declaration of what it holds,
|
|
227
|
+
* the way every other construct renders its signature. The node's name labels
|
|
228
|
+
* the block; the access mechanism lives in separate accessor nodes.
|
|
229
|
+
*
|
|
230
|
+
* Order: the backing comment, then the store's own typed declaration
|
|
231
|
+
* (`valueType` when declared, otherwise the named members as a body), then any
|
|
232
|
+
* remaining members. A store that declares neither says so explicitly rather
|
|
233
|
+
* than rendering as an empty block.
|
|
229
234
|
*/
|
|
230
235
|
function generateStore(name: string, declaration?: GraphifyComponentDetail): string {
|
|
231
236
|
const store = declaration?.kind === 'store' ? declaration : undefined;
|
|
232
237
|
const backing = store?.storage ? `\n// backing: ${store.storage}` : '';
|
|
233
|
-
const
|
|
238
|
+
const members = store?.properties ?? [];
|
|
239
|
+
const props = members
|
|
234
240
|
.map((p) => `declare const ${p.name}${p.type ? `: ${p.type}` : ''};`)
|
|
235
241
|
.join('\n');
|
|
236
242
|
|
|
243
|
+
if (store?.valueType) {
|
|
244
|
+
const named = members.length
|
|
245
|
+
? `\n${members
|
|
246
|
+
.map((p) => ` ${p.name}${p.type ? `: ${p.type}` : ''};`)
|
|
247
|
+
.join('\n')}`
|
|
248
|
+
: '';
|
|
249
|
+
return `// store: ${name} — ${store.valueType}${backing}${named}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
237
252
|
if (!props) {
|
|
238
|
-
return `// store: ${name} — no
|
|
253
|
+
return `// store: ${name} — no declared type (no valueType or state members)${backing}`;
|
|
239
254
|
}
|
|
240
255
|
return `// store: ${name}${backing}\n${props}`;
|
|
241
256
|
}
|
|
@@ -2,6 +2,13 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import {
|
|
3
3
|
convertSubsystemToNodes,
|
|
4
4
|
convertSubsystemToEdges,
|
|
5
|
+
deriveGraphEdges,
|
|
6
|
+
edgeColor,
|
|
7
|
+
edgeStrokeStyle,
|
|
8
|
+
MECHANISM_COLOR,
|
|
9
|
+
GRAPHIFY_RELATION_COLOR,
|
|
10
|
+
GRAPHIFY_RELATION_FALLBACK_COLOR,
|
|
11
|
+
GRAPHIFY_RELATION_STYLE,
|
|
5
12
|
convertSubsystemToGroups,
|
|
6
13
|
getSubsystemRegions,
|
|
7
14
|
getSubsystemModuleRegions,
|
|
@@ -42,9 +49,9 @@ const comps: SubsystemComponent[] = [
|
|
|
42
49
|
];
|
|
43
50
|
|
|
44
51
|
const relations = [
|
|
45
|
-
{ id: 'e1', from: 'transcript', to: 'reader', relationType: '
|
|
52
|
+
{ id: 'e1', from: 'transcript', to: 'reader', relationType: 'method' as const },
|
|
46
53
|
// 'host' is NOT a component — this is the cross-package external case.
|
|
47
|
-
{ id: 'e2', from: 'reader', to: 'host', relationType: '
|
|
54
|
+
{ id: 'e2', from: 'reader', to: 'host', relationType: 'method' as const, refs: ['bun/index.ts'] },
|
|
48
55
|
];
|
|
49
56
|
|
|
50
57
|
const doc = { components: comps, relations };
|
|
@@ -796,6 +803,15 @@ describe('isConstructsOnlyModel', () => {
|
|
|
796
803
|
expect(isConstructsOnlyModel({ components: comps })).toBe(true);
|
|
797
804
|
});
|
|
798
805
|
|
|
806
|
+
test('false when only graphify-native edges exist', () => {
|
|
807
|
+
expect(
|
|
808
|
+
isConstructsOnlyModel({
|
|
809
|
+
components: comps,
|
|
810
|
+
graphifyRelations: [{ id: 'g', from: 'reader', to: 'transcript', relation: 'imports' }],
|
|
811
|
+
}),
|
|
812
|
+
).toBe(false);
|
|
813
|
+
});
|
|
814
|
+
|
|
799
815
|
test('false when empty, or when relations or walkthrough hops exist', () => {
|
|
800
816
|
expect(isConstructsOnlyModel({ components: [], relations: [] })).toBe(false);
|
|
801
817
|
expect(isConstructsOnlyModel({ components: comps, relations })).toBe(false);
|
|
@@ -973,3 +989,60 @@ describe('reorderTargetIndex', () => {
|
|
|
973
989
|
expect(reorderWalkthroughs(wts, 0, target).map((w) => w.id)).toEqual(['b', 'a', 'c']);
|
|
974
990
|
});
|
|
975
991
|
});
|
|
992
|
+
|
|
993
|
+
describe('graphify-native edges', () => {
|
|
994
|
+
const doc = {
|
|
995
|
+
components: comps,
|
|
996
|
+
relations: [],
|
|
997
|
+
};
|
|
998
|
+
const graphify = [
|
|
999
|
+
{ id: 'g1', from: 'reader', to: 'dst', relation: 'imports' },
|
|
1000
|
+
{ id: 'g2', from: 'reader', to: 'dst', relation: 'contains' },
|
|
1001
|
+
];
|
|
1002
|
+
|
|
1003
|
+
test('deriveGraphEdges tags graphify relations with provenance + raw verb', () => {
|
|
1004
|
+
const [e] = deriveGraphEdges({ ...doc, graphifyRelations: graphify });
|
|
1005
|
+
expect(e.provenance).toBe('graphify');
|
|
1006
|
+
expect(e.mechanism).toBe('imports');
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
test('subsystem relations keep no graphify provenance', () => {
|
|
1010
|
+
const [e] = deriveGraphEdges({
|
|
1011
|
+
relations: [{ id: 'r', from: 'reader', to: 'dst', relationType: 'method' }],
|
|
1012
|
+
});
|
|
1013
|
+
expect(e.provenance).toBeUndefined();
|
|
1014
|
+
expect(e.mechanism).toBe('method');
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
test('graphify edges color from the separate palette', () => {
|
|
1018
|
+
expect(edgeColor({ mechanism: 'imports', provenance: 'graphify' })).toBe(
|
|
1019
|
+
GRAPHIFY_RELATION_COLOR.imports,
|
|
1020
|
+
);
|
|
1021
|
+
expect(edgeColor({ mechanism: 'imports', provenance: 'graphify' })).not.toBe(
|
|
1022
|
+
MECHANISM_COLOR.method,
|
|
1023
|
+
);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test('unknown graphify verb falls back, never undefined', () => {
|
|
1027
|
+
expect(edgeColor({ mechanism: 'brand_new_verb', provenance: 'graphify' })).toBe(
|
|
1028
|
+
GRAPHIFY_RELATION_FALLBACK_COLOR,
|
|
1029
|
+
);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test('subsystem mechanism color is unaffected by the graphify palette', () => {
|
|
1033
|
+
expect(edgeColor({ mechanism: 'method' })).toBe(MECHANISM_COLOR.method);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
test('graphify edges all share the graphify stroke style', () => {
|
|
1037
|
+
expect(edgeStrokeStyle({ mechanism: 'imports', provenance: 'graphify' })).toBe(
|
|
1038
|
+
GRAPHIFY_RELATION_STYLE,
|
|
1039
|
+
);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
test('convertSubsystemToEdges carries provenance into edge data', () => {
|
|
1043
|
+
const edges = convertSubsystemToEdges(doc, graphify);
|
|
1044
|
+
const g = edges.find((e) => e.id === 'g1');
|
|
1045
|
+
expect((g?.data as { provenance?: string })?.provenance).toBe('graphify');
|
|
1046
|
+
expect((g?.data as { mechanism?: string })?.mechanism).toBe('imports');
|
|
1047
|
+
});
|
|
1048
|
+
});
|
package/src/subsystem/model.ts
CHANGED
|
@@ -117,8 +117,7 @@ export type SubsystemRelationType =
|
|
|
117
117
|
| 'inherits'
|
|
118
118
|
| 'implements'
|
|
119
119
|
| 'mixes_in'
|
|
120
|
-
| 'method'
|
|
121
|
-
| 'references';
|
|
120
|
+
| 'method';
|
|
122
121
|
|
|
123
122
|
/**
|
|
124
123
|
* Walkthrough hop mechanism — runtime seams with a `file:line` site.
|
|
@@ -146,6 +145,50 @@ export type SubsystemEdgeMechanism =
|
|
|
146
145
|
*/
|
|
147
146
|
export type SubsystemEdgeView = 'relations' | 'walkthroughs';
|
|
148
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Where a display edge came from.
|
|
150
|
+
*
|
|
151
|
+
* - `subsystem` (default): a verb from the authored vocabularies
|
|
152
|
+
* (`SubsystemRelationType` / `SubsystemWalkthroughMechanism`), colored from
|
|
153
|
+
* `MECHANISM_COLOR`.
|
|
154
|
+
* - `graphify`: a raw relation read off graphify's static symbol graph
|
|
155
|
+
* (`imports`, `contains`, `re_exports`, …). These are DERIVED, never authored,
|
|
156
|
+
* and are colored from `GRAPHIFY_RELATION_COLOR` so a reader can tell a
|
|
157
|
+
* graphify fact apart from a subsystem claim at a glance.
|
|
158
|
+
*/
|
|
159
|
+
export type SubsystemEdgeProvenance = 'subsystem' | 'graphify';
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A graphify-native topology edge — a raw graphify relation that has no
|
|
163
|
+
* subsystem mechanism equivalent.
|
|
164
|
+
*
|
|
165
|
+
* Kept structurally separate from `SubsystemRelation`: those are authored into
|
|
166
|
+
* a portable model and validated against a closed vocabulary, whereas these are
|
|
167
|
+
* derived from a graphify run and carry graphify's own (open) verb set. They are
|
|
168
|
+
* a display input only — never written back into a `SubsystemModelDocument`.
|
|
169
|
+
*/
|
|
170
|
+
export interface SubsystemGraphifyRelation {
|
|
171
|
+
id: string;
|
|
172
|
+
/** Source component alias. */
|
|
173
|
+
from: string;
|
|
174
|
+
/** Target component alias. */
|
|
175
|
+
to: string;
|
|
176
|
+
/** Raw graphify relation verb (e.g. `imports`, `contains`, `re_exports`). */
|
|
177
|
+
relation: string;
|
|
178
|
+
/** Concrete file/symbol refs backing the relation. */
|
|
179
|
+
refs?: string[];
|
|
180
|
+
/**
|
|
181
|
+
* 1-based source line of the relation site. Optional — available on
|
|
182
|
+
* `calls`/`references`-style edges; used to order an ego graph's callees by
|
|
183
|
+
* call site when a host opts into line-ordered layering.
|
|
184
|
+
*/
|
|
185
|
+
line?: number;
|
|
186
|
+
/** graphify's provenance tag (`EXTRACTED` / `INFERRED` / `AMBIGUOUS`). */
|
|
187
|
+
confidence?: string;
|
|
188
|
+
/** Reference context on `references` edges (e.g. `return_type`, `field`). */
|
|
189
|
+
context?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
149
192
|
/** A component node — the named unit, construct-tagged; `file` is its location. */
|
|
150
193
|
export interface SubsystemComponent {
|
|
151
194
|
/**
|
|
@@ -305,9 +348,28 @@ export interface SubsystemComponentEdge {
|
|
|
305
348
|
id: string;
|
|
306
349
|
from: string; // component alias
|
|
307
350
|
to: string; // component alias or external target label
|
|
308
|
-
|
|
351
|
+
/**
|
|
352
|
+
* The edge verb. For `provenance: 'subsystem'` (the default) this is a
|
|
353
|
+
* `SubsystemEdgeMechanism`; for `provenance: 'graphify'` it is the raw
|
|
354
|
+
* graphify relation. Typed as `string` because the display edge is a derived
|
|
355
|
+
* structure and graphify's verb set is open — the authored vocabularies
|
|
356
|
+
* (`SubsystemRelationType` / `SubsystemWalkthroughMechanism`) stay closed.
|
|
357
|
+
*/
|
|
358
|
+
mechanism: string;
|
|
359
|
+
/** Origin of the edge; absent means `'subsystem'`. */
|
|
360
|
+
provenance?: SubsystemEdgeProvenance;
|
|
309
361
|
/** Concrete file/symbol refs backing the edge (the seam). */
|
|
310
362
|
refs?: string[];
|
|
363
|
+
/**
|
|
364
|
+
* 1-based source line of the RELATION SITE — where the edge's verb was
|
|
365
|
+
* observed (e.g. a `calls` edge's call-site line inside the caller), NOT
|
|
366
|
+
* where the target is declared.
|
|
367
|
+
*/
|
|
368
|
+
line?: number;
|
|
369
|
+
/** graphify's provenance tag (`EXTRACTED` / `INFERRED` / `AMBIGUOUS`). */
|
|
370
|
+
confidence?: string;
|
|
371
|
+
/** Reference context on `references` edges (e.g. `return_type`, `field`). */
|
|
372
|
+
context?: string;
|
|
311
373
|
}
|
|
312
374
|
|
|
313
375
|
/**
|
|
@@ -362,7 +424,7 @@ export interface SubsystemModelDocument {
|
|
|
362
424
|
export function derivedGraphEdgeId(
|
|
363
425
|
from: string,
|
|
364
426
|
to: string,
|
|
365
|
-
mechanism:
|
|
427
|
+
mechanism: string,
|
|
366
428
|
): string {
|
|
367
429
|
return `${from}--${mechanism}-->${to}`;
|
|
368
430
|
}
|
|
@@ -412,6 +474,7 @@ export function reorderTargetIndex(boundary: number, from: number): number {
|
|
|
412
474
|
export function deriveGraphEdges(doc: {
|
|
413
475
|
relations?: readonly SubsystemRelation[];
|
|
414
476
|
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
477
|
+
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
415
478
|
}): SubsystemComponentEdge[] {
|
|
416
479
|
const byId = new Map<string, SubsystemComponentEdge>();
|
|
417
480
|
for (const r of doc.relations ?? []) {
|
|
@@ -439,6 +502,22 @@ export function deriveGraphEdges(doc: {
|
|
|
439
502
|
}
|
|
440
503
|
}
|
|
441
504
|
}
|
|
505
|
+
for (const g of doc.graphifyRelations ?? []) {
|
|
506
|
+
const id = g.id || derivedGraphEdgeId(g.from, g.to, g.relation);
|
|
507
|
+
if (!byId.has(id)) {
|
|
508
|
+
byId.set(id, {
|
|
509
|
+
id,
|
|
510
|
+
from: g.from,
|
|
511
|
+
to: g.to,
|
|
512
|
+
mechanism: g.relation,
|
|
513
|
+
provenance: 'graphify',
|
|
514
|
+
refs: g.refs,
|
|
515
|
+
line: g.line,
|
|
516
|
+
confidence: g.confidence,
|
|
517
|
+
context: g.context,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
442
521
|
return [...byId.values()];
|
|
443
522
|
}
|
|
444
523
|
|
|
@@ -450,11 +529,13 @@ export function isConstructsOnlyModel(doc: {
|
|
|
450
529
|
components: readonly { alias: string }[];
|
|
451
530
|
relations?: readonly SubsystemRelation[];
|
|
452
531
|
walkthroughs?: readonly SubsystemWalkthrough[];
|
|
532
|
+
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
453
533
|
}): boolean {
|
|
454
534
|
if (doc.components.length === 0) return false;
|
|
455
535
|
return deriveGraphEdges({
|
|
456
536
|
relations: doc.relations,
|
|
457
537
|
walkthroughs: doc.walkthroughs,
|
|
538
|
+
graphifyRelations: doc.graphifyRelations,
|
|
458
539
|
}).length === 0;
|
|
459
540
|
}
|
|
460
541
|
|
|
@@ -957,6 +1038,50 @@ export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
|
957
1038
|
fileMatch?: boolean;
|
|
958
1039
|
/** True while this node is on an opened-but-unselected flow. */
|
|
959
1040
|
dimmed?: boolean;
|
|
1041
|
+
/** Diagnostics badge — see `SubsystemNodeIssue`. Absent when no findings. */
|
|
1042
|
+
issue?: SubsystemNodeIssue;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* The per-construct verification ladder, in order. A finding's rung is the
|
|
1047
|
+
* earliest step it fails: file → symbol → declaration → type → signature.
|
|
1048
|
+
*/
|
|
1049
|
+
export type SubsystemIssueRung =
|
|
1050
|
+
| 'file'
|
|
1051
|
+
| 'symbol'
|
|
1052
|
+
| 'declaration'
|
|
1053
|
+
| 'type'
|
|
1054
|
+
| 'signature';
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Per-node diagnostics badge — the proposed issue overlay (severity ring +
|
|
1058
|
+
* earliest-rung corner chip). The graph derives one per component from the
|
|
1059
|
+
* audit issues whose component target resolves to that node. Absent when the
|
|
1060
|
+
* node has no rung-mapped findings.
|
|
1061
|
+
*/
|
|
1062
|
+
export interface SubsystemNodeIssue {
|
|
1063
|
+
/** Worst severity on the node — `error` (contradicts source) beats `info`. */
|
|
1064
|
+
severity: 'error' | 'info';
|
|
1065
|
+
/** Earliest failing rung across the node's findings. */
|
|
1066
|
+
rung: SubsystemIssueRung;
|
|
1067
|
+
/** Total findings on this node. */
|
|
1068
|
+
count: number;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Per-frame diagnostics badge — a finding about the BOUNDARY itself (its
|
|
1073
|
+
* containment / nesting), not about any member construct. Separate from
|
|
1074
|
+
* `SubsystemNodeIssue` because these have no construct rung to report: the
|
|
1075
|
+
* fault is the region's shape, which is also why it badges the frame rather
|
|
1076
|
+
* than a leaf. `kind` selects the icon (see `ISSUE_KIND_ICON`).
|
|
1077
|
+
*/
|
|
1078
|
+
export interface SubsystemRegionIssue {
|
|
1079
|
+
/** Worst severity across the region's findings — `error` beats `info`. */
|
|
1080
|
+
severity: 'error' | 'info';
|
|
1081
|
+
/** The kind that supplies the badge icon. */
|
|
1082
|
+
kind: string;
|
|
1083
|
+
/** Total findings on this region. */
|
|
1084
|
+
count: number;
|
|
960
1085
|
}
|
|
961
1086
|
|
|
962
1087
|
export interface SubsystemGroupNodeData extends Record<string, unknown> {
|
|
@@ -965,6 +1090,8 @@ export interface SubsystemGroupNodeData extends Record<string, unknown> {
|
|
|
965
1090
|
dimmed?: boolean;
|
|
966
1091
|
/** Host-supplied frame color override; falls back to the derived color. */
|
|
967
1092
|
color?: string;
|
|
1093
|
+
/** Boundary diagnostics badge — see `SubsystemRegionIssue`. */
|
|
1094
|
+
issue?: SubsystemRegionIssue;
|
|
968
1095
|
}
|
|
969
1096
|
|
|
970
1097
|
export type SubsystemGraphNode =
|
|
@@ -972,8 +1099,17 @@ export type SubsystemGraphNode =
|
|
|
972
1099
|
| Node<SubsystemGroupNodeData, 'subsystem-group'>;
|
|
973
1100
|
|
|
974
1101
|
export interface SubsystemGraphEdgeData extends Record<string, unknown> {
|
|
975
|
-
mechanism
|
|
1102
|
+
/** The edge verb (subsystem mechanism, or raw graphify relation). */
|
|
1103
|
+
mechanism: string;
|
|
1104
|
+
/** Origin of the edge; absent means `'subsystem'`. */
|
|
1105
|
+
provenance?: SubsystemEdgeProvenance;
|
|
976
1106
|
refs?: string[];
|
|
1107
|
+
/** 1-based source line of the relation site (graphify edges when known). */
|
|
1108
|
+
line?: number;
|
|
1109
|
+
/** graphify's provenance tag (`EXTRACTED` / `INFERRED` / `AMBIGUOUS`). */
|
|
1110
|
+
confidence?: string;
|
|
1111
|
+
/** Reference context on `references` edges (e.g. `return_type`, `field`). */
|
|
1112
|
+
context?: string;
|
|
977
1113
|
/** True while another edge is selected — render this edge (and its label)
|
|
978
1114
|
* dimmed to focus the selected relationship. */
|
|
979
1115
|
dimmed?: boolean;
|
|
@@ -1006,7 +1142,6 @@ export const SUBSYSTEM_RELATION_TYPES = [
|
|
|
1006
1142
|
'implements',
|
|
1007
1143
|
'mixes_in',
|
|
1008
1144
|
'method',
|
|
1009
|
-
'references',
|
|
1010
1145
|
] as const satisfies readonly SubsystemRelationType[];
|
|
1011
1146
|
|
|
1012
1147
|
/** Runtime vocabulary of walkthrough hop mechanisms — mirrors `SubsystemWalkthroughMechanism`. */
|
|
@@ -1048,7 +1183,6 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
1048
1183
|
mixes_in: '#d474a8', // pink-magenta
|
|
1049
1184
|
uses: '#e3b341', // gold
|
|
1050
1185
|
method: '#c586c0', // magenta
|
|
1051
|
-
references: '#3b82f6', // blue
|
|
1052
1186
|
feeds: '#4ec9b0', // teal — data-flow into a processor
|
|
1053
1187
|
produces: '#a78bfa', // violet — emits an output type
|
|
1054
1188
|
writes: '#e8853a', // orange — mutates retained state
|
|
@@ -1065,7 +1199,6 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
1065
1199
|
mixes_in: 'dashed',
|
|
1066
1200
|
uses: 'solid',
|
|
1067
1201
|
method: 'solid',
|
|
1068
|
-
references: 'dotted',
|
|
1069
1202
|
feeds: 'solid',
|
|
1070
1203
|
produces: 'solid',
|
|
1071
1204
|
writes: 'solid',
|
|
@@ -1074,6 +1207,77 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
1074
1207
|
'registers-into': 'dashed',
|
|
1075
1208
|
};
|
|
1076
1209
|
|
|
1210
|
+
/** Fallback hue for a mechanism outside the closed palette (defensive — the
|
|
1211
|
+
* vocabulary is closed, so this only guards an unexpected verb from rendering
|
|
1212
|
+
* an undefined stroke). */
|
|
1213
|
+
export const MECHANISM_FALLBACK_COLOR = '#888888';
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* graphify-native relation hues — a palette deliberately SEPARATE from
|
|
1217
|
+
* `MECHANISM_COLOR`.
|
|
1218
|
+
*
|
|
1219
|
+
* graphify edges are derived from a static symbol graph, not authored subsystem
|
|
1220
|
+
* semantics, so they get their own (cooler, desaturated) family and their own
|
|
1221
|
+
* stroke treatment. Keeping the hue variables separate means a graphify fact
|
|
1222
|
+
* can never be mistaken for a subsystem claim, and a new graphify verb can
|
|
1223
|
+
* never accidentally inherit a mechanism hue.
|
|
1224
|
+
*
|
|
1225
|
+
* Keyed by graphify's relation verbs. Open-ended: graphify adds verbs, so an
|
|
1226
|
+
* unlisted verb falls back to `GRAPHIFY_RELATION_FALLBACK_COLOR`.
|
|
1227
|
+
*/
|
|
1228
|
+
export const GRAPHIFY_RELATION_COLOR: Record<string, string> = {
|
|
1229
|
+
contains: '#5c6b7a', // slate — structural containment
|
|
1230
|
+
defines: '#6e7d8c', // steel — defines a member
|
|
1231
|
+
imports: '#4c7fb5', // steel blue — module import
|
|
1232
|
+
imports_from: '#6699cc', // lighter steel blue
|
|
1233
|
+
re_exports: '#7b7fd4', // indigo — barrel re-export
|
|
1234
|
+
dynamic_import: '#4f9aa8', // steel cyan — lazy import
|
|
1235
|
+
indirect_call: '#8494a4', // grey — non-direct call
|
|
1236
|
+
calls: '#3f9e78', // muted green — call-graph edge
|
|
1237
|
+
uses: '#b0924e', // muted gold
|
|
1238
|
+
references: '#5f7fa6', // dusty blue
|
|
1239
|
+
extends: '#8c74b0', // muted violet
|
|
1240
|
+
inherits: '#7a68a6', // muted violet (darker)
|
|
1241
|
+
implements: '#a4739e', // muted mauve
|
|
1242
|
+
method: '#a4739e', // muted mauve
|
|
1243
|
+
decorator: '#b57fa0', // muted rose
|
|
1244
|
+
rationale_for: '#bda15e', // muted ochre
|
|
1245
|
+
semantically_similar_to: '#9a80bf', // muted lavender
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
/** Fallback hue for a graphify verb absent from `GRAPHIFY_RELATION_COLOR`. */
|
|
1249
|
+
export const GRAPHIFY_RELATION_FALLBACK_COLOR = '#7d8794';
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* graphify-native edges share one stroke treatment so provenance still reads
|
|
1253
|
+
* even where a hue happens to sit near a mechanism hue. Mirrors the
|
|
1254
|
+
* `MECHANISM_STYLE` value space.
|
|
1255
|
+
*/
|
|
1256
|
+
export const GRAPHIFY_RELATION_STYLE: 'solid' | 'dashed' | 'dotted' = 'dashed';
|
|
1257
|
+
|
|
1258
|
+
/** Resolve an edge's stroke color from its provenance + verb. */
|
|
1259
|
+
export function edgeColor(
|
|
1260
|
+
edge: Pick<SubsystemComponentEdge, 'mechanism' | 'provenance'>,
|
|
1261
|
+
): string {
|
|
1262
|
+
if (edge.provenance === 'graphify') {
|
|
1263
|
+
return (
|
|
1264
|
+
GRAPHIFY_RELATION_COLOR[edge.mechanism] ?? GRAPHIFY_RELATION_FALLBACK_COLOR
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return (
|
|
1268
|
+
MECHANISM_COLOR[edge.mechanism as SubsystemEdgeMechanism] ??
|
|
1269
|
+
MECHANISM_FALLBACK_COLOR
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** Resolve an edge's dash treatment from its provenance + verb. */
|
|
1274
|
+
export function edgeStrokeStyle(
|
|
1275
|
+
edge: Pick<SubsystemComponentEdge, 'mechanism' | 'provenance'>,
|
|
1276
|
+
): 'solid' | 'dashed' | 'dotted' {
|
|
1277
|
+
if (edge.provenance === 'graphify') return GRAPHIFY_RELATION_STYLE;
|
|
1278
|
+
return MECHANISM_STYLE[edge.mechanism as SubsystemEdgeMechanism] ?? 'solid';
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1077
1281
|
/**
|
|
1078
1282
|
* Region colors for process boundaries in the aggregate graph. Kept separate
|
|
1079
1283
|
* from MECHANISM_COLOR on purpose: that palette encodes edge semantics on thin
|
|
@@ -1165,7 +1369,6 @@ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][]
|
|
|
1165
1369
|
['mixes_in', 'applies mixin', true],
|
|
1166
1370
|
['uses', 'general dependency (import, call, or reference)', false],
|
|
1167
1371
|
['method', 'structural: has method / member', true],
|
|
1168
|
-
['references', 'type / symbol reference (not a call)', true],
|
|
1169
1372
|
['feeds', 'data flow: output feeds into input', false],
|
|
1170
1373
|
['produces', 'data flow: produces / outputs', false],
|
|
1171
1374
|
['writes', 'state access: mutates retained state', true],
|
|
@@ -1489,13 +1692,16 @@ export function convertSubsystemToGroups(
|
|
|
1489
1692
|
* is an external label (not a component alias) point at a synthetic stub so the
|
|
1490
1693
|
* relationship is visible without a member node.
|
|
1491
1694
|
*/
|
|
1492
|
-
export function convertSubsystemToEdges(
|
|
1695
|
+
export function convertSubsystemToEdges(
|
|
1696
|
+
doc: SubsystemModelDocument,
|
|
1697
|
+
graphifyRelations: readonly SubsystemGraphifyRelation[] = [],
|
|
1698
|
+
): SubsystemGraphEdge[] {
|
|
1493
1699
|
const compAliases = new Set(doc.components.map((c) => c.alias));
|
|
1494
1700
|
const edges: SubsystemGraphEdge[] = [];
|
|
1495
1701
|
|
|
1496
|
-
for (const e of deriveGraphEdges(doc)) {
|
|
1497
|
-
const color =
|
|
1498
|
-
const style =
|
|
1702
|
+
for (const e of deriveGraphEdges({ ...doc, graphifyRelations })) {
|
|
1703
|
+
const color = edgeColor(e);
|
|
1704
|
+
const style = edgeStrokeStyle(e);
|
|
1499
1705
|
// If `to` is a real component, connect directly; otherwise point at a stub node.
|
|
1500
1706
|
const isExternal = !compAliases.has(e.to);
|
|
1501
1707
|
const targetId = isExternal ? `external:${e.to}` : e.to;
|
|
@@ -1504,7 +1710,14 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
|
|
|
1504
1710
|
id: e.id,
|
|
1505
1711
|
source: e.from,
|
|
1506
1712
|
target: targetId,
|
|
1507
|
-
data: {
|
|
1713
|
+
data: {
|
|
1714
|
+
mechanism: e.mechanism,
|
|
1715
|
+
provenance: e.provenance,
|
|
1716
|
+
refs: e.refs,
|
|
1717
|
+
line: e.line,
|
|
1718
|
+
confidence: e.confidence,
|
|
1719
|
+
context: e.context,
|
|
1720
|
+
},
|
|
1508
1721
|
type: 'subsystem-edge',
|
|
1509
1722
|
markerEnd: { type: MarkerType.ArrowClosed, color, width: 32, height: 32 },
|
|
1510
1723
|
style: { color, stroke: color, strokeDasharray: style === 'dashed' ? '6 4' : undefined },
|
|
@@ -1520,7 +1733,9 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
|
|
|
1520
1733
|
|
|
1521
1734
|
/** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
|
|
1522
1735
|
export function subsystemGraphLayoutKey(
|
|
1523
|
-
doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'
|
|
1736
|
+
doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'> & {
|
|
1737
|
+
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
1738
|
+
},
|
|
1524
1739
|
): string {
|
|
1525
1740
|
const components = doc.components
|
|
1526
1741
|
.map(({ alias, purl, name, symbol, construct, file, purpose, process, module }) =>
|
|
@@ -1546,6 +1761,18 @@ export async function buildSubsystemGraph(
|
|
|
1546
1761
|
showEdgeLabels?: boolean;
|
|
1547
1762
|
measuredWidths?: Map<string, number>;
|
|
1548
1763
|
measuredHeights?: Map<string, number>;
|
|
1764
|
+
/** graphify-native relations to merge into the display graph, drawn with
|
|
1765
|
+
* the separate graphify palette. Display-only; never authored. */
|
|
1766
|
+
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
1767
|
+
/**
|
|
1768
|
+
* Order same-layer nodes by `component.line` (ascending) instead of ELK's
|
|
1769
|
+
* crossing-minimizer. Only meaningful when components carry a `line` — and
|
|
1770
|
+
* callers are expected to stamp `line` with the RELEVANT site for their
|
|
1771
|
+
* graph (e.g. an ego graph stamps each callee with the center's call-site
|
|
1772
|
+
* line, not the callee's declaration line).
|
|
1773
|
+
* @default false
|
|
1774
|
+
*/
|
|
1775
|
+
orderByLine?: boolean;
|
|
1549
1776
|
} & BoundaryFrameOptions = {},
|
|
1550
1777
|
): Promise<{
|
|
1551
1778
|
nodes: SubsystemGraphNode[];
|
|
@@ -1565,10 +1792,12 @@ export async function buildSubsystemGraph(
|
|
|
1565
1792
|
measuredHeights,
|
|
1566
1793
|
showSingletonFrames,
|
|
1567
1794
|
packageFrames,
|
|
1795
|
+
graphifyRelations,
|
|
1796
|
+
orderByLine = false,
|
|
1568
1797
|
} = opts;
|
|
1569
1798
|
const frameOpts: BoundaryFrameOptions = { showSingletonFrames, packageFrames };
|
|
1570
1799
|
const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
|
|
1571
|
-
const edges = convertSubsystemToEdges(doc);
|
|
1800
|
+
const edges = convertSubsystemToEdges(doc, graphifyRelations);
|
|
1572
1801
|
// Nested boundary tree: package → process → module → leaves.
|
|
1573
1802
|
const layoutGroups = buildBoundaryLayoutGroups(doc, frameOpts);
|
|
1574
1803
|
const regions = layoutGroups.map((g) => g.region);
|
|
@@ -1602,7 +1831,7 @@ export async function buildSubsystemGraph(
|
|
|
1602
1831
|
// cross-package edges have something to land on.
|
|
1603
1832
|
const realAliases = new Set(doc.components.map((c) => c.alias));
|
|
1604
1833
|
const externalIds: string[] = [];
|
|
1605
|
-
for (const e of deriveGraphEdges(doc)) {
|
|
1834
|
+
for (const e of deriveGraphEdges({ ...doc, graphifyRelations })) {
|
|
1606
1835
|
if (!realAliases.has(e.to)) {
|
|
1607
1836
|
const extId = `external:${e.to}`;
|
|
1608
1837
|
if (!externalIds.includes(extId)) externalIds.push(extId);
|
|
@@ -1669,6 +1898,7 @@ export async function buildSubsystemGraph(
|
|
|
1669
1898
|
interLayerSpacing: showEdgeLabels === false ? 120 : EDGE_LABEL_SIDE_PADDING,
|
|
1670
1899
|
endpointInset: EDGE_ARROW_INSET,
|
|
1671
1900
|
preserveNodePositions: false,
|
|
1901
|
+
orderByLine,
|
|
1672
1902
|
edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
|
|
1673
1903
|
groups: layoutGroups.map((g) => ({
|
|
1674
1904
|
id: g.id,
|