@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.
Files changed (74) hide show
  1. package/dist/graphify/consolidated.d.ts +8 -0
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/construct.d.ts +18 -0
  4. package/dist/graphify/construct.d.ts.map +1 -1
  5. package/dist/graphify/construct.js +23 -0
  6. package/dist/graphify/construct.js.map +1 -1
  7. package/dist/graphify/index.d.ts +2 -2
  8. package/dist/graphify/index.d.ts.map +1 -1
  9. package/dist/graphify/index.js +1 -1
  10. package/dist/graphify/index.js.map +1 -1
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  16. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
  17. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  18. package/dist/subsystem/IssueList.d.ts +49 -3
  19. package/dist/subsystem/IssueList.d.ts.map +1 -1
  20. package/dist/subsystem/IssueList.js +180 -79
  21. package/dist/subsystem/IssueList.js.map +1 -1
  22. package/dist/subsystem/SubsystemComponentGraph.d.ts +23 -2
  23. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  24. package/dist/subsystem/SubsystemComponentGraph.js +492 -23
  25. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  26. package/dist/subsystem/formatDeclaration.js +19 -5
  27. package/dist/subsystem/formatDeclaration.js.map +1 -1
  28. package/dist/subsystem/model.d.ts +161 -7
  29. package/dist/subsystem/model.d.ts.map +1 -1
  30. package/dist/subsystem/model.js +91 -12
  31. package/dist/subsystem/model.js.map +1 -1
  32. package/dist/subsystem/nodes.d.ts.map +1 -1
  33. package/dist/subsystem/nodes.js +133 -72
  34. package/dist/subsystem/nodes.js.map +1 -1
  35. package/dist/subsystem/symbolRefs.d.ts.map +1 -1
  36. package/dist/subsystem/symbolRefs.js +2 -0
  37. package/dist/subsystem/symbolRefs.js.map +1 -1
  38. package/dist/utils/elkLayout.d.ts +9 -0
  39. package/dist/utils/elkLayout.d.ts.map +1 -1
  40. package/dist/utils/elkLayout.js +21 -2
  41. package/dist/utils/elkLayout.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/graphify/__fixtures__/ego-samples.json +545 -0
  44. package/src/graphify/consolidated.ts +8 -0
  45. package/src/graphify/construct.test.ts +61 -1
  46. package/src/graphify/construct.ts +33 -0
  47. package/src/graphify/index.ts +6 -1
  48. package/src/index.ts +13 -1
  49. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +2 -2
  50. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -9
  51. package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +4 -4
  52. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +625 -0
  53. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +7 -1
  54. package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +5 -5
  55. package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +5 -5
  56. package/src/stories/Subsystem/ComponentGraph/NodeAnatomy.stories.tsx +40 -73
  57. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
  58. package/src/stories/Subsystem/ComponentGraph/StoreValueType.stories.tsx +261 -0
  59. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
  60. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +354 -0
  61. package/src/stories/Subsystem/StoreValueTypeDeclaration.stories.tsx +182 -0
  62. package/src/subsystem/IssueList.focus.test.tsx +113 -0
  63. package/src/subsystem/IssueList.icon.test.tsx +95 -0
  64. package/src/subsystem/IssueList.tsx +267 -111
  65. package/src/subsystem/SubsystemComponentGraph.tsx +582 -27
  66. package/src/subsystem/formatDeclaration.test.ts +64 -0
  67. package/src/subsystem/formatDeclaration.ts +20 -5
  68. package/src/subsystem/model.test.ts +75 -2
  69. package/src/subsystem/model.ts +247 -17
  70. package/src/subsystem/nodes.group.test.tsx +52 -0
  71. package/src/subsystem/nodes.tsx +112 -8
  72. package/src/subsystem/symbolRefs.ts +2 -0
  73. package/src/subsystem/toC4.test.ts +3 -3
  74. 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 — ambient `declare const` lines for
227
- * the state members, never a class/method stub. The node's name labels 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 props = (store?.properties ?? [])
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 captured state members${backing}`;
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: 'references' as const },
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: 'references' as const, refs: ['bun/index.ts'] },
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
+ });
@@ -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
- mechanism: SubsystemEdgeMechanism;
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: SubsystemEdgeMechanism,
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: SubsystemEdgeMechanism;
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(doc: SubsystemModelDocument): SubsystemGraphEdge[] {
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 = MECHANISM_COLOR[e.mechanism];
1498
- const style = MECHANISM_STYLE[e.mechanism];
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: { mechanism: e.mechanism, refs: e.refs },
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,