@principal-ai/subsystems-react 0.37.12 → 0.38.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 (39) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  6. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
  7. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  8. package/dist/subsystem/SubsystemComponentGraph.d.ts +14 -1
  9. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  10. package/dist/subsystem/SubsystemComponentGraph.js +32 -12
  11. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  12. package/dist/subsystem/model.d.ts +123 -7
  13. package/dist/subsystem/model.d.ts.map +1 -1
  14. package/dist/subsystem/model.js +91 -12
  15. package/dist/subsystem/model.js.map +1 -1
  16. package/dist/subsystem/nodes.d.ts.map +1 -1
  17. package/dist/subsystem/nodes.js +9 -5
  18. package/dist/subsystem/nodes.js.map +1 -1
  19. package/dist/utils/elkLayout.d.ts +9 -0
  20. package/dist/utils/elkLayout.d.ts.map +1 -1
  21. package/dist/utils/elkLayout.js +21 -2
  22. package/dist/utils/elkLayout.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/graphify/__fixtures__/ego-samples.json +545 -0
  25. package/src/index.ts +12 -1
  26. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +2 -2
  27. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +9 -9
  28. package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +4 -4
  29. package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +5 -5
  30. package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +5 -5
  31. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +6 -6
  32. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
  33. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +354 -0
  34. package/src/subsystem/SubsystemComponentGraph.tsx +52 -15
  35. package/src/subsystem/model.test.ts +75 -2
  36. package/src/subsystem/model.ts +201 -17
  37. package/src/subsystem/nodes.tsx +10 -6
  38. package/src/subsystem/toC4.test.ts +3 -3
  39. package/src/utils/elkLayout.ts +32 -1
@@ -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
 
@@ -972,8 +1053,17 @@ export type SubsystemGraphNode =
972
1053
  | Node<SubsystemGroupNodeData, 'subsystem-group'>;
973
1054
 
974
1055
  export interface SubsystemGraphEdgeData extends Record<string, unknown> {
975
- mechanism: SubsystemEdgeMechanism;
1056
+ /** The edge verb (subsystem mechanism, or raw graphify relation). */
1057
+ mechanism: string;
1058
+ /** Origin of the edge; absent means `'subsystem'`. */
1059
+ provenance?: SubsystemEdgeProvenance;
976
1060
  refs?: string[];
1061
+ /** 1-based source line of the relation site (graphify edges when known). */
1062
+ line?: number;
1063
+ /** graphify's provenance tag (`EXTRACTED` / `INFERRED` / `AMBIGUOUS`). */
1064
+ confidence?: string;
1065
+ /** Reference context on `references` edges (e.g. `return_type`, `field`). */
1066
+ context?: string;
977
1067
  /** True while another edge is selected — render this edge (and its label)
978
1068
  * dimmed to focus the selected relationship. */
979
1069
  dimmed?: boolean;
@@ -1006,7 +1096,6 @@ export const SUBSYSTEM_RELATION_TYPES = [
1006
1096
  'implements',
1007
1097
  'mixes_in',
1008
1098
  'method',
1009
- 'references',
1010
1099
  ] as const satisfies readonly SubsystemRelationType[];
1011
1100
 
1012
1101
  /** Runtime vocabulary of walkthrough hop mechanisms — mirrors `SubsystemWalkthroughMechanism`. */
@@ -1048,7 +1137,6 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
1048
1137
  mixes_in: '#d474a8', // pink-magenta
1049
1138
  uses: '#e3b341', // gold
1050
1139
  method: '#c586c0', // magenta
1051
- references: '#3b82f6', // blue
1052
1140
  feeds: '#4ec9b0', // teal — data-flow into a processor
1053
1141
  produces: '#a78bfa', // violet — emits an output type
1054
1142
  writes: '#e8853a', // orange — mutates retained state
@@ -1065,7 +1153,6 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
1065
1153
  mixes_in: 'dashed',
1066
1154
  uses: 'solid',
1067
1155
  method: 'solid',
1068
- references: 'dotted',
1069
1156
  feeds: 'solid',
1070
1157
  produces: 'solid',
1071
1158
  writes: 'solid',
@@ -1074,6 +1161,77 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
1074
1161
  'registers-into': 'dashed',
1075
1162
  };
1076
1163
 
1164
+ /** Fallback hue for a mechanism outside the closed palette (defensive — the
1165
+ * vocabulary is closed, so this only guards an unexpected verb from rendering
1166
+ * an undefined stroke). */
1167
+ export const MECHANISM_FALLBACK_COLOR = '#888888';
1168
+
1169
+ /**
1170
+ * graphify-native relation hues — a palette deliberately SEPARATE from
1171
+ * `MECHANISM_COLOR`.
1172
+ *
1173
+ * graphify edges are derived from a static symbol graph, not authored subsystem
1174
+ * semantics, so they get their own (cooler, desaturated) family and their own
1175
+ * stroke treatment. Keeping the hue variables separate means a graphify fact
1176
+ * can never be mistaken for a subsystem claim, and a new graphify verb can
1177
+ * never accidentally inherit a mechanism hue.
1178
+ *
1179
+ * Keyed by graphify's relation verbs. Open-ended: graphify adds verbs, so an
1180
+ * unlisted verb falls back to `GRAPHIFY_RELATION_FALLBACK_COLOR`.
1181
+ */
1182
+ export const GRAPHIFY_RELATION_COLOR: Record<string, string> = {
1183
+ contains: '#5c6b7a', // slate — structural containment
1184
+ defines: '#6e7d8c', // steel — defines a member
1185
+ imports: '#4c7fb5', // steel blue — module import
1186
+ imports_from: '#6699cc', // lighter steel blue
1187
+ re_exports: '#7b7fd4', // indigo — barrel re-export
1188
+ dynamic_import: '#4f9aa8', // steel cyan — lazy import
1189
+ indirect_call: '#8494a4', // grey — non-direct call
1190
+ calls: '#3f9e78', // muted green — call-graph edge
1191
+ uses: '#b0924e', // muted gold
1192
+ references: '#5f7fa6', // dusty blue
1193
+ extends: '#8c74b0', // muted violet
1194
+ inherits: '#7a68a6', // muted violet (darker)
1195
+ implements: '#a4739e', // muted mauve
1196
+ method: '#a4739e', // muted mauve
1197
+ decorator: '#b57fa0', // muted rose
1198
+ rationale_for: '#bda15e', // muted ochre
1199
+ semantically_similar_to: '#9a80bf', // muted lavender
1200
+ };
1201
+
1202
+ /** Fallback hue for a graphify verb absent from `GRAPHIFY_RELATION_COLOR`. */
1203
+ export const GRAPHIFY_RELATION_FALLBACK_COLOR = '#7d8794';
1204
+
1205
+ /**
1206
+ * graphify-native edges share one stroke treatment so provenance still reads
1207
+ * even where a hue happens to sit near a mechanism hue. Mirrors the
1208
+ * `MECHANISM_STYLE` value space.
1209
+ */
1210
+ export const GRAPHIFY_RELATION_STYLE: 'solid' | 'dashed' | 'dotted' = 'dashed';
1211
+
1212
+ /** Resolve an edge's stroke color from its provenance + verb. */
1213
+ export function edgeColor(
1214
+ edge: Pick<SubsystemComponentEdge, 'mechanism' | 'provenance'>,
1215
+ ): string {
1216
+ if (edge.provenance === 'graphify') {
1217
+ return (
1218
+ GRAPHIFY_RELATION_COLOR[edge.mechanism] ?? GRAPHIFY_RELATION_FALLBACK_COLOR
1219
+ );
1220
+ }
1221
+ return (
1222
+ MECHANISM_COLOR[edge.mechanism as SubsystemEdgeMechanism] ??
1223
+ MECHANISM_FALLBACK_COLOR
1224
+ );
1225
+ }
1226
+
1227
+ /** Resolve an edge's dash treatment from its provenance + verb. */
1228
+ export function edgeStrokeStyle(
1229
+ edge: Pick<SubsystemComponentEdge, 'mechanism' | 'provenance'>,
1230
+ ): 'solid' | 'dashed' | 'dotted' {
1231
+ if (edge.provenance === 'graphify') return GRAPHIFY_RELATION_STYLE;
1232
+ return MECHANISM_STYLE[edge.mechanism as SubsystemEdgeMechanism] ?? 'solid';
1233
+ }
1234
+
1077
1235
  /**
1078
1236
  * Region colors for process boundaries in the aggregate graph. Kept separate
1079
1237
  * from MECHANISM_COLOR on purpose: that palette encodes edge semantics on thin
@@ -1165,7 +1323,6 @@ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][]
1165
1323
  ['mixes_in', 'applies mixin', true],
1166
1324
  ['uses', 'general dependency (import, call, or reference)', false],
1167
1325
  ['method', 'structural: has method / member', true],
1168
- ['references', 'type / symbol reference (not a call)', true],
1169
1326
  ['feeds', 'data flow: output feeds into input', false],
1170
1327
  ['produces', 'data flow: produces / outputs', false],
1171
1328
  ['writes', 'state access: mutates retained state', true],
@@ -1489,13 +1646,16 @@ export function convertSubsystemToGroups(
1489
1646
  * is an external label (not a component alias) point at a synthetic stub so the
1490
1647
  * relationship is visible without a member node.
1491
1648
  */
1492
- export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemGraphEdge[] {
1649
+ export function convertSubsystemToEdges(
1650
+ doc: SubsystemModelDocument,
1651
+ graphifyRelations: readonly SubsystemGraphifyRelation[] = [],
1652
+ ): SubsystemGraphEdge[] {
1493
1653
  const compAliases = new Set(doc.components.map((c) => c.alias));
1494
1654
  const edges: SubsystemGraphEdge[] = [];
1495
1655
 
1496
- for (const e of deriveGraphEdges(doc)) {
1497
- const color = MECHANISM_COLOR[e.mechanism];
1498
- const style = MECHANISM_STYLE[e.mechanism];
1656
+ for (const e of deriveGraphEdges({ ...doc, graphifyRelations })) {
1657
+ const color = edgeColor(e);
1658
+ const style = edgeStrokeStyle(e);
1499
1659
  // If `to` is a real component, connect directly; otherwise point at a stub node.
1500
1660
  const isExternal = !compAliases.has(e.to);
1501
1661
  const targetId = isExternal ? `external:${e.to}` : e.to;
@@ -1504,7 +1664,14 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
1504
1664
  id: e.id,
1505
1665
  source: e.from,
1506
1666
  target: targetId,
1507
- data: { mechanism: e.mechanism, refs: e.refs },
1667
+ data: {
1668
+ mechanism: e.mechanism,
1669
+ provenance: e.provenance,
1670
+ refs: e.refs,
1671
+ line: e.line,
1672
+ confidence: e.confidence,
1673
+ context: e.context,
1674
+ },
1508
1675
  type: 'subsystem-edge',
1509
1676
  markerEnd: { type: MarkerType.ArrowClosed, color, width: 32, height: 32 },
1510
1677
  style: { color, stroke: color, strokeDasharray: style === 'dashed' ? '6 4' : undefined },
@@ -1520,7 +1687,9 @@ export function convertSubsystemToEdges(doc: SubsystemModelDocument): SubsystemG
1520
1687
 
1521
1688
  /** Stable key for layout-affecting graph fields (ignores declarationRef, etc.). */
1522
1689
  export function subsystemGraphLayoutKey(
1523
- doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'>,
1690
+ doc: Pick<SubsystemModelDocument, 'components' | 'relations' | 'walkthroughs'> & {
1691
+ graphifyRelations?: readonly SubsystemGraphifyRelation[];
1692
+ },
1524
1693
  ): string {
1525
1694
  const components = doc.components
1526
1695
  .map(({ alias, purl, name, symbol, construct, file, purpose, process, module }) =>
@@ -1546,6 +1715,18 @@ export async function buildSubsystemGraph(
1546
1715
  showEdgeLabels?: boolean;
1547
1716
  measuredWidths?: Map<string, number>;
1548
1717
  measuredHeights?: Map<string, number>;
1718
+ /** graphify-native relations to merge into the display graph, drawn with
1719
+ * the separate graphify palette. Display-only; never authored. */
1720
+ graphifyRelations?: readonly SubsystemGraphifyRelation[];
1721
+ /**
1722
+ * Order same-layer nodes by `component.line` (ascending) instead of ELK's
1723
+ * crossing-minimizer. Only meaningful when components carry a `line` — and
1724
+ * callers are expected to stamp `line` with the RELEVANT site for their
1725
+ * graph (e.g. an ego graph stamps each callee with the center's call-site
1726
+ * line, not the callee's declaration line).
1727
+ * @default false
1728
+ */
1729
+ orderByLine?: boolean;
1549
1730
  } & BoundaryFrameOptions = {},
1550
1731
  ): Promise<{
1551
1732
  nodes: SubsystemGraphNode[];
@@ -1565,10 +1746,12 @@ export async function buildSubsystemGraph(
1565
1746
  measuredHeights,
1566
1747
  showSingletonFrames,
1567
1748
  packageFrames,
1749
+ graphifyRelations,
1750
+ orderByLine = false,
1568
1751
  } = opts;
1569
1752
  const frameOpts: BoundaryFrameOptions = { showSingletonFrames, packageFrames };
1570
1753
  const nodes = convertSubsystemToNodes(doc, { maxNodeWidth });
1571
- const edges = convertSubsystemToEdges(doc);
1754
+ const edges = convertSubsystemToEdges(doc, graphifyRelations);
1572
1755
  // Nested boundary tree: package → process → module → leaves.
1573
1756
  const layoutGroups = buildBoundaryLayoutGroups(doc, frameOpts);
1574
1757
  const regions = layoutGroups.map((g) => g.region);
@@ -1602,7 +1785,7 @@ export async function buildSubsystemGraph(
1602
1785
  // cross-package edges have something to land on.
1603
1786
  const realAliases = new Set(doc.components.map((c) => c.alias));
1604
1787
  const externalIds: string[] = [];
1605
- for (const e of deriveGraphEdges(doc)) {
1788
+ for (const e of deriveGraphEdges({ ...doc, graphifyRelations })) {
1606
1789
  if (!realAliases.has(e.to)) {
1607
1790
  const extId = `external:${e.to}`;
1608
1791
  if (!externalIds.includes(extId)) externalIds.push(extId);
@@ -1669,6 +1852,7 @@ export async function buildSubsystemGraph(
1669
1852
  interLayerSpacing: showEdgeLabels === false ? 120 : EDGE_LABEL_SIDE_PADDING,
1670
1853
  endpointInset: EDGE_ARROW_INSET,
1671
1854
  preserveNodePositions: false,
1855
+ orderByLine,
1672
1856
  edgeLabels: showEdgeLabels === false ? { enabled: false } : { enabled: true, placement: 'CENTER' },
1673
1857
  groups: layoutGroups.map((g) => ({
1674
1858
  id: g.id,
@@ -17,8 +17,8 @@ import {
17
17
  } from '@xyflow/react';
18
18
  import { useTheme } from '@principal-ade/industry-theme';
19
19
  import {
20
- MECHANISM_COLOR,
21
- MECHANISM_STYLE,
20
+ edgeColor,
21
+ edgeStrokeStyle,
22
22
  PROPOSED_COLOR,
23
23
  constructBadgeColor,
24
24
  constructBadgeLabel,
@@ -560,10 +560,14 @@ export function SubsystemEdge({
560
560
  }: EdgeProps<SubsystemGraphEdge>) {
561
561
  const path = data?.elkPath ?? '';
562
562
  const mechanism = data?.mechanism ?? 'uses';
563
- const color = MECHANISM_COLOR[mechanism] ?? '#888';
564
- // Dash style comes from the mechanism table (dashed = inverted-control or
565
- // observational relationships: hierarchy, registration, watches).
566
- const isDashed = MECHANISM_STYLE[mechanism] === 'dashed';
563
+ // Color + dash resolve from provenance: subsystem mechanisms use the
564
+ // MECHANISM_* tables, graphify-native relations use the separate
565
+ // GRAPHIFY_RELATION_* palette (see edgeColor / edgeStrokeStyle).
566
+ const edgeRef = { mechanism, provenance: data?.provenance };
567
+ const color = edgeColor(edgeRef);
568
+ // Dash style encodes provenance (graphify) and relationship kind (dashed =
569
+ // inverted-control or observational: hierarchy, registration, watches).
570
+ const isDashed = edgeStrokeStyle(edgeRef) === 'dashed';
567
571
  const dimmed = data?.dimmed === true;
568
572
  // Dim the stroke by color, never via path `opacity`. SVG markers are shared
569
573
  // by id; opacity on the referencing path paints every arrowhead that uses
@@ -20,9 +20,9 @@ const doc: SubsystemModelDocument = {
20
20
  ],
21
21
  relations: [
22
22
  { id: 'r1', from: 'a', to: 'b', relationType: 'method' }, // intra p1
23
- { id: 'r2', from: 'a', to: 'c', relationType: 'references' },
24
- { id: 'r3', from: 'b', to: 'c', relationType: 'references' },
25
- { id: 'r4', from: 'a', to: 'x', relationType: 'references' },
23
+ { id: 'r2', from: 'a', to: 'c', relationType: 'method' },
24
+ { id: 'r3', from: 'b', to: 'c', relationType: 'method' },
25
+ { id: 'r4', from: 'a', to: 'x', relationType: 'method' },
26
26
  ],
27
27
  walkthroughs: [
28
28
  {
@@ -84,6 +84,16 @@ export interface ElkLayoutOptions {
84
84
  */
85
85
  direction?: 'RIGHT' | 'LEFT' | 'DOWN' | 'UP';
86
86
 
87
+ /**
88
+ * Order same-layer nodes by their source line (ascending) instead of leaving
89
+ * it to the crossing-minimizer. Nodes must carry a `data.component.line`
90
+ * (used when set; nodes without one keep ELK's neutral ordering). Off by
91
+ * default — the generic layered layout is tuned for crossings, and line order
92
+ * only makes sense for graphs that model a source region top-to-bottom.
93
+ * @default false
94
+ */
95
+ orderByLine?: boolean;
96
+
87
97
  /**
88
98
  * Compound groups — each becomes an ELK parent whose `memberIds` are laid
89
99
  * out inside it. `memberIds` may be leaf node ids or other group ids
@@ -323,11 +333,15 @@ function getElkOptions(options: ElkLayoutOptions): LayoutOptions {
323
333
  interLayerSpacing = 0,
324
334
  edgeLabels,
325
335
  direction = 'RIGHT',
336
+ orderByLine = false,
326
337
  } = options;
327
338
 
328
339
  const baseOptions: LayoutOptions = {
329
340
  'elk.algorithm': 'layered',
330
341
  'elk.direction': direction,
342
+ // Keep same-layer ordering stable (not reversed / randomized) so a
343
+ // line-ordered rewrite or ELK's own ordering is predictable.
344
+ 'elk.layered.crossingMinimization.semiInteractive': 'true',
331
345
  // Spacing
332
346
  'elk.spacing.nodeNode': String(nodeSpacing),
333
347
  'elk.spacing.edgeEdge': String(edgeSpacing),
@@ -346,6 +360,14 @@ function getElkOptions(options: ElkLayoutOptions): LayoutOptions {
346
360
  'elk.layered.thoroughness': '50',
347
361
  };
348
362
 
363
+ // Model order (position hints) so a per-node `data.line` can drive same-layer
364
+ // ordering. ELK honours the `y` of each node as a position hint against the
365
+ // model order.
366
+ if (orderByLine) {
367
+ baseOptions['elk.layered.fixedAlignment'] = 'NONE';
368
+ baseOptions['elk.layered.considerModelOrder.strategy'] = 'NODES_AND_EDGES';
369
+ }
370
+
349
371
  // Reserve space for inline edge labels so they don't overlap nodes/edges.
350
372
  if (edgeLabels?.enabled !== false) {
351
373
  const placement = edgeLabels?.placement ?? 'CENTER';
@@ -507,6 +529,7 @@ export async function computeElkLayout(
507
529
  const edgeLabels = options.edgeLabels;
508
530
  const endpointInset = options.endpointInset ?? 0;
509
531
  const direction = options.direction ?? 'RIGHT';
532
+ const orderByLine = options.orderByLine ?? false;
510
533
 
511
534
  // Build a map of original node positions BEFORE passing to ELK
512
535
  // (ELK mutates the input nodes in place, so we must save positions first)
@@ -527,12 +550,20 @@ export async function computeElkLayout(
527
550
  const nd = node.data as { component?: { layer?: number }; layer?: number } | undefined;
528
551
  layer = nd?.layer ?? nd?.component?.layer;
529
552
 
553
+ // Optional source-line ordering: seed each node's `y` hint from its line so
554
+ // ELK's model-order placement puts lower lines further down within a layer.
555
+ let y = node.position.y;
556
+ if (orderByLine) {
557
+ const line = (node.data as { component?: { line?: number } } | undefined)?.component?.line;
558
+ if (typeof line === 'number') y = line;
559
+ }
560
+
530
561
  return {
531
562
  id: node.id,
532
563
  width,
533
564
  height,
534
565
  x: node.position.x,
535
- y: node.position.y,
566
+ y,
536
567
  // Add ports on each side for edge connections
537
568
  ports: [
538
569
  { id: `${node.id}_top`, properties: { 'port.side': 'NORTH' } },