@principal-ai/principal-view-react 0.16.56 → 0.16.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/graphify/consolidated.d.ts +16 -1
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/index.d.ts +1 -1
  4. package/dist/graphify/index.d.ts.map +1 -1
  5. package/dist/graphify/index.js.map +1 -1
  6. package/dist/graphify/kind.d.ts +1 -1
  7. package/dist/graphify/kind.d.ts.map +1 -1
  8. package/dist/graphify/kind.js +7 -1
  9. package/dist/graphify/kind.js.map +1 -1
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/pierre/constructColors.d.ts +24 -0
  15. package/dist/pierre/constructColors.d.ts.map +1 -0
  16. package/dist/pierre/constructColors.js +91 -0
  17. package/dist/pierre/constructColors.js.map +1 -0
  18. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
  19. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
  20. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
  21. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
  22. package/dist/subsystem/ComponentDeclaration.d.ts +2 -2
  23. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  24. package/dist/subsystem/ComponentDeclaration.js +12 -11
  25. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  26. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  27. package/dist/subsystem/SubsystemComponentGraph.js +22 -1
  28. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  29. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  30. package/dist/subsystem/formatDeclaration.js +49 -10
  31. package/dist/subsystem/formatDeclaration.js.map +1 -1
  32. package/dist/subsystem/model.d.ts +55 -12
  33. package/dist/subsystem/model.d.ts.map +1 -1
  34. package/dist/subsystem/model.js +54 -24
  35. package/dist/subsystem/model.js.map +1 -1
  36. package/dist/subsystem/nodes.d.ts +4 -1
  37. package/dist/subsystem/nodes.d.ts.map +1 -1
  38. package/dist/subsystem/nodes.js +70 -21
  39. package/dist/subsystem/nodes.js.map +1 -1
  40. package/dist/subsystem/tokenizeComponent.js +1 -1
  41. package/dist/subsystem/tokenizeComponent.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/graphify/consolidated.ts +18 -1
  44. package/src/graphify/index.ts +1 -0
  45. package/src/graphify/kind.ts +7 -1
  46. package/src/index.ts +3 -2
  47. package/src/pierre/constructColors.test.ts +52 -0
  48. package/src/pierre/constructColors.ts +112 -0
  49. package/src/stories/{BoundaryNodes.stories.tsx → Audit/BoundaryNodes.stories.tsx} +1 -1
  50. package/src/stories/{CanvasEdgeTypes.stories.tsx → Audit/CanvasEdgeTypes.stories.tsx} +1 -1
  51. package/src/stories/{CanvasNodeTypes.stories.tsx → Audit/CanvasNodeTypes.stories.tsx} +1 -1
  52. package/src/stories/{ColorPriority.stories.tsx → Audit/ColorPriority.stories.tsx} +1 -1
  53. package/src/stories/{DiamondBadges.stories.tsx → Audit/DiamondBadges.stories.tsx} +1 -1
  54. package/src/stories/{NodeDimensionsTesting.stories.tsx → Audit/NodeDimensionsTesting.stories.tsx} +2 -2
  55. package/src/stories/{NodeFieldsAudit.stories.tsx → Audit/NodeFieldsAudit.stories.tsx} +1 -1
  56. package/src/stories/{NodeFontSizeTesting.stories.tsx → Audit/NodeFontSizeTesting.stories.tsx} +2 -2
  57. package/src/stories/{NodeShapes.stories.tsx → Audit/NodeShapes.stories.tsx} +1 -1
  58. package/src/stories/{WideNodesTesting.stories.tsx → Audit/WideNodesTesting.stories.tsx} +2 -2
  59. package/src/stories/{dashboard → Components}/DashboardRenderer.stories.tsx +2 -2
  60. package/src/stories/{GraphRenderer.stories.tsx → Components/GraphRenderer.stories.tsx} +2 -2
  61. package/src/stories/{MultiCanvasRenderer.stories.tsx → Components/MultiCanvasRenderer.stories.tsx} +2 -2
  62. package/src/stories/{SequenceDiagram.stories.tsx → Components/SequenceDiagram.stories.tsx} +2 -2
  63. package/src/stories/{StateView.stories.tsx → Components/StateView.stories.tsx} +2 -2
  64. package/src/stories/{TraceViewer.stories.tsx → Components/TraceViewer.stories.tsx} +3 -3
  65. package/src/stories/{WorkflowSequenceDiagram.stories.tsx → Components/WorkflowSequenceDiagram.stories.tsx} +1 -1
  66. package/src/stories/{AnimationWorkshop.stories.tsx → Features/AnimationWorkshop.stories.tsx} +5 -5
  67. package/src/stories/{ChangeTypeVisual.stories.tsx → Features/ChangeTypeVisual.stories.tsx} +2 -2
  68. package/src/stories/{EventDrivenAnimations.stories.tsx → Features/EventDrivenAnimations.stories.tsx} +2 -2
  69. package/src/stories/{MultiConfig.stories.tsx → Features/MultiConfig.stories.tsx} +2 -2
  70. package/src/stories/{ElkEdgeRouting.stories.tsx → Layout/ElkEdgeRouting.stories.tsx} +2 -2
  71. package/src/stories/{GraphOrientation.stories.tsx → Layout/GraphOrientation.stories.tsx} +3 -3
  72. package/src/stories/{MultiDirectionalConnections.stories.tsx → Layout/MultiDirectionalConnections.stories.tsx} +2 -2
  73. package/src/stories/{NodeDefinitionComparison.stories.tsx → Nodes/NodeDefinitionComparison.stories.tsx} +2 -2
  74. package/src/stories/{NodeNameFallbacks.stories.tsx → Nodes/NodeNameFallbacks.stories.tsx} +2 -2
  75. package/src/stories/{NumericColors.stories.tsx → Nodes/NumericColors.stories.tsx} +2 -2
  76. package/src/stories/{EventNameDisplay.stories.tsx → OTEL/EventNameDisplay.stories.tsx} +2 -2
  77. package/src/stories/{EventNameFallback.stories.tsx → OTEL/EventNameFallback.stories.tsx} +2 -2
  78. package/src/stories/{OtelComponents.stories.tsx → OTEL/OtelComponents.stories.tsx} +3 -3
  79. package/src/stories/{OtelNodeTypesPrototype.stories.tsx → OTEL/OtelNodeTypesPrototype.stories.tsx} +2 -2
  80. package/src/stories/{RawEventCards.stories.tsx → OTEL/RawEventCards.stories.tsx} +3 -3
  81. package/src/stories/{SpanBadges.stories.tsx → OTEL/SpanBadges.stories.tsx} +1 -1
  82. package/src/stories/{ValidationEventsCanvas.stories.tsx → OTEL/ValidationEventsCanvas.stories.tsx} +3 -3
  83. package/src/stories/{BacklogMdCanvases.stories.tsx → RealWorld/BacklogMdCanvases.stories.tsx} +2 -2
  84. package/src/stories/{FileCitySequence.stories.tsx → RealWorld/FileCitySequence.stories.tsx} +4 -4
  85. package/src/stories/{RealCanvasFiles.stories.tsx → RealWorld/RealCanvasFiles.stories.tsx} +7 -7
  86. package/src/stories/{ComponentDeclarationAudit.stories.tsx → Subsystem/ComponentDeclarationAudit.stories.tsx} +19 -20
  87. package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
  88. package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
  89. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
  90. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
  91. package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
  92. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +646 -0
  93. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
  94. package/src/subsystem/ComponentDeclaration.tsx +13 -13
  95. package/src/subsystem/SubsystemComponentGraph.tsx +25 -5
  96. package/src/subsystem/formatDeclaration.ts +57 -11
  97. package/src/subsystem/model.test.ts +38 -6
  98. package/src/subsystem/model.ts +108 -31
  99. package/src/subsystem/nodes.tsx +103 -27
  100. package/src/subsystem/tokenizeComponent.ts +1 -1
  101. package/src/stories/SubsystemComponentGraph.stories.tsx +0 -1225
  102. /package/src/stories/{dashboard/sample-dashboards → data}/activity-feed-analytics.dashboard.json +0 -0
@@ -0,0 +1,129 @@
1
+ import type {
2
+ SubsystemComponent,
3
+ SubsystemComponentEdge,
4
+ } from '../../../subsystem/model';
5
+ import type { GraphifyComponentDetail } from '../../../graphify';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Build a subsystem graph from a compact spec - helpers
9
+ // ---------------------------------------------------------------------------
10
+ export function components(
11
+ spec: Array<[id: string, name: string, construct: SubsystemComponent['construct'], file: string, purl: string, purpose?: string, symbol?: string, detail?: GraphifyComponentDetail]>,
12
+ ): SubsystemComponent[] {
13
+ return spec.map(([id, name, construct, file, purl, purpose, symbol, detail]) => ({
14
+ id,
15
+ name,
16
+ construct,
17
+ file,
18
+ purl,
19
+ purpose,
20
+ symbol,
21
+ detail,
22
+ }));
23
+ }
24
+
25
+ export function edges(
26
+ spec: Array<[from: string, to: string, mechanism: SubsystemComponentEdge['mechanism'], refs?: string[]]>,
27
+ ): SubsystemComponentEdge[] {
28
+ return spec.map(([from, to, mechanism, refs], i) => ({
29
+ id: `e${i}`,
30
+ from,
31
+ to,
32
+ mechanism,
33
+ refs,
34
+ }));
35
+ }
36
+
37
+ export const readerDetail: GraphifyComponentDetail = {
38
+ kind: 'class',
39
+ methods: [
40
+ { nodeId: 'm1', name: 'normalize', returnType: 'SessionEvent[]' },
41
+ { nodeId: 'm2', name: 'readSession', parameters: [{ type: 'string' }], returnType: 'SessionRecord' },
42
+ { nodeId: 'm3', name: 'toUniversalEvents', returnType: 'UniversalEvent[]' },
43
+ ],
44
+ properties: [{ name: 'sessionId', type: 'string' }],
45
+ extends: [],
46
+ implements: ['SessionReaderLike'],
47
+ instantiations: [{ nodeId: 'caller1', name: 'capture-session' }],
48
+ references: [{ nodeId: 'ref1', name: 'supported-agents', context: 'type' }],
49
+ };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Single package (no consumers) - the investigate-and-pin pattern.
53
+ // Conveys the idea via LAYERS: readers (input) → accumulator (processing).
54
+ // ---------------------------------------------------------------------------
55
+ export const investigateOnlyComponents: SubsystemComponent[] = [
56
+ {
57
+ id: 'v1',
58
+ name: 'V1EventBridgeProcessor',
59
+ construct: 'class',
60
+ file: 'src/event-processing/V1EventBridge.ts',
61
+ purl: 'pkg:github/principal-ai/agent-monitoring',
62
+ purpose: 'normalizes V1 DB rows into universal events',
63
+ symbol: 'V1EventBridgeProcessor',
64
+ },
65
+ {
66
+ id: 'v2',
67
+ name: 'V2EventBridgeProcessor',
68
+ construct: 'class',
69
+ file: 'src/event-processing/V2EventBridge.ts',
70
+ purl: 'pkg:github/principal-ai/agent-monitoring',
71
+ purpose: 'normalizes V2 durable events into universal events',
72
+ symbol: 'V2EventBridgeProcessor',
73
+ },
74
+ {
75
+ id: 'input',
76
+ name: 'RepoNormalizedUniversalAgentSessionEvent',
77
+ construct: 'interface',
78
+ file: 'types/RepoNormalizedUniversalAgentSessionEvent.ts',
79
+ purl: 'pkg:github/principal-ai/agent-monitoring',
80
+ purpose: 'the subsystem\u2019s input type — a repo-normalized universal event the readers produce and the accumulator consumes',
81
+ symbol: 'RepoNormalizedUniversalAgentSessionEvent',
82
+ detail: {
83
+ kind: 'type',
84
+ properties: [
85
+ { name: 'eventType', type: 'NormalizedEventType' },
86
+ { name: 'sessionId', type: 'string' },
87
+ { name: 'timestamp', type: 'number' },
88
+ ],
89
+ usedBy: [{ nodeId: 'acc', name: 'accumulateToAgentSessionEvents', context: 'parameter_type' }],
90
+ implementors: [],
91
+ } satisfies GraphifyComponentDetail,
92
+ },
93
+ {
94
+ id: 'acc',
95
+ name: 'accumulateToAgentSessionEvents',
96
+ construct: 'function',
97
+ file: 'src/event-processing/accumulator.ts',
98
+ purl: 'pkg:github/principal-ai/agent-monitoring',
99
+ purpose: 'accumulates normalized events into agent session events',
100
+ symbol: 'accumulateToAgentSessionEvents',
101
+ },
102
+ {
103
+ id: 'out',
104
+ name: 'AgentSessionEvent',
105
+ construct: 'interface',
106
+ file: 'src/event-processing/accumulator.ts',
107
+ purl: 'pkg:github/principal-ai/agent-monitoring',
108
+ purpose: 'the subsystem\u2019s output type — an accumulated agent-session event',
109
+ symbol: 'AgentSessionEvent',
110
+ detail: {
111
+ kind: 'type',
112
+ properties: [
113
+ { name: 'sessionName', type: 'string' },
114
+ { name: 'operation', type: 'AgentSessionEventOperation' },
115
+ { name: 'description', type: 'string' },
116
+ ],
117
+ usedBy: [{ nodeId: 'acc', name: 'accumulateToAgentSessionEvents', context: 'return_type' }],
118
+ implementors: [],
119
+ } satisfies GraphifyComponentDetail,
120
+ },
121
+ ];
122
+
123
+ export const investigateOnlyEdges: SubsystemComponentEdge[] = edges([
124
+ // Concept-level data flow — the LLM's semantic intent.
125
+ ['v1', 'input', 'produces'],
126
+ ['v2', 'input', 'produces'],
127
+ ['input', 'acc', 'feeds'],
128
+ ['acc', 'out', 'produces'],
129
+ ]);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ComponentDeclaration — the selected subsystem component rendered as it would
3
- * appear in code: a file-path comment + doc comment, then the kind's
3
+ * appear in code: a file-path comment + doc comment, then the construct's
4
4
  * declaration (`class X extends Y { ... }`, `function f(): T`, ...) with its
5
5
  * members as signatures. Graphify relationship drill-downs appear as trailing
6
6
  * comments. File content lives in the bottom FileDrawer, not here.
@@ -11,7 +11,6 @@ import { AlignLeft, FileText, ShieldCheck } from 'lucide-react';
11
11
  import { useTheme } from '@principal-ade/industry-theme';
12
12
  import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
13
13
  import {
14
- KIND_COLOR,
15
14
  formatPurl,
16
15
  type SubsystemComponent,
17
16
  type SubsystemDeclTokenKind,
@@ -19,6 +18,7 @@ import {
19
18
  import type { SubsystemOpenFileOptions } from './declarationRef';
20
19
  import { parseSourceLocation } from './declarationRef';
21
20
  import { tokenizeComponent } from './tokenizeComponent';
21
+ import { constructColorsFromPierreTheme } from '../pierre/constructColors';
22
22
 
23
23
  /** Live / result state for the declaration-panel Verify control. */
24
24
  export type ComponentVerificationPhase =
@@ -49,7 +49,7 @@ export interface ComponentVerificationState {
49
49
  source_location?: string;
50
50
  candidates?: Array<{ nodeId: string; label: string; source_file?: string }>;
51
51
  };
52
- kind?: {
52
+ construct?: {
53
53
  claimed: string;
54
54
  inferred: string;
55
55
  match: boolean;
@@ -163,11 +163,11 @@ function verificationSummary(
163
163
  ? ` · declaration ${declFresh}${v.declaration?.ref ? ` L${v.declaration.ref.startLine}` : ''}`
164
164
  : '';
165
165
  const anchorBit = `exact → ${v.anchor?.label ?? v.anchor?.nodeId ?? 'node'}${loc}${declBit}`;
166
- if (v.kind && !v.kind.match) {
166
+ if (v.construct && !v.construct.match) {
167
167
  const why =
168
- v.kind.inferred === 'unknown'
169
- ? `kind unknown (claimed ${v.kind.claimed})`
170
- : `kind mismatch: claimed ${v.kind.claimed}, inferred ${v.kind.inferred}`;
168
+ v.construct.inferred === 'unknown'
169
+ ? `construct unknown (claimed ${v.construct.claimed})`
170
+ : `construct mismatch: claimed ${v.construct.claimed}, inferred ${v.construct.inferred}`;
171
171
  return {
172
172
  text: [...bits, anchorBit, why].join(' · '),
173
173
  color: '#e5534b',
@@ -182,7 +182,7 @@ function verificationSummary(
182
182
  text: [
183
183
  ...bits,
184
184
  anchorBit,
185
- v.kind?.match ? `kind ${v.kind.inferred}` : undefined,
185
+ v.construct?.match ? `construct ${v.construct.inferred}` : undefined,
186
186
  `signature mismatch: params [${cParams}]≠[${iParams}] return [${cRet}]≠[${iRet}]`,
187
187
  ]
188
188
  .filter(Boolean)
@@ -190,8 +190,8 @@ function verificationSummary(
190
190
  color: '#e5534b',
191
191
  };
192
192
  }
193
- const kindBit =
194
- v.kind?.match ? `kind ${v.kind.inferred}` : undefined;
193
+ const constructBit =
194
+ v.construct?.match ? `construct ${v.construct.inferred}` : undefined;
195
195
  const inlineBit =
196
196
  v.signature?.inlineParameters
197
197
  ? `${v.signature.inlineParameters} inline param${v.signature.inlineParameters === 1 ? '' : 's'} verified`
@@ -210,7 +210,7 @@ function verificationSummary(
210
210
  text: [
211
211
  ...bits,
212
212
  anchorBit,
213
- ...(kindBit ? [kindBit] : []),
213
+ ...(constructBit ? [constructBit] : []),
214
214
  ...(inlineBit ? [inlineBit] : []),
215
215
  v.signature.skipCode
216
216
  ? `params/return not fully checked — ${skipPhrase[v.signature.skipCode] ?? v.signature.skipCode}`
@@ -222,7 +222,7 @@ function verificationSummary(
222
222
  const sigBit =
223
223
  v.signature && v.signature.match ? 'params/return ok' : undefined;
224
224
  return {
225
- text: [...bits, anchorBit, ...(kindBit ? [kindBit] : []), ...(sigBit ? [sigBit] : [])].join(' · '),
225
+ text: [...bits, anchorBit, ...(constructBit ? [constructBit] : []), ...(sigBit ? [sigBit] : [])].join(' · '),
226
226
  color: ok,
227
227
  };
228
228
  }
@@ -263,7 +263,7 @@ export function ComponentDeclaration({
263
263
  const [showFile, setShowFile] = useState(false);
264
264
  const [showPurpose, setShowPurpose] = useState(false);
265
265
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
266
- const color = KIND_COLOR[component.kind] ?? '#888';
266
+ const color = constructColorsFromPierreTheme(pierreSyntaxTheme)[component.construct];
267
267
  const okColor = '#3d9a5f';
268
268
  const warnColor = theme.colors.textSecondary;
269
269
 
@@ -181,11 +181,19 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
181
181
  measuredDimsRef.current = new Map();
182
182
  prevMeasuredSigRef.current = '';
183
183
  const doc = { components: componentsRef.current, edges: edgesRef.current };
184
- void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels }).then(({ nodes, edges: e }) => {
185
- if (!alive) return;
186
- setBuilt({ nodes, edges: e as Edge[] });
187
- setLayoutReady(false);
188
- });
184
+ void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels })
185
+ .then(({ nodes, edges: e }) => {
186
+ if (!alive) return;
187
+ setBuilt({ nodes, edges: e as Edge[] });
188
+ setLayoutReady(false);
189
+ })
190
+ .catch((err) => {
191
+ console.warn('[subsystem-graph] initial layout failed:', err);
192
+ if (!alive) return;
193
+ // Reveal the cover even on failure so the UI is not stuck forever.
194
+ setBuilt({ nodes: [], edges: [] });
195
+ setLayoutReady(true);
196
+ });
189
197
  return () => { alive = false; };
190
198
  }, [layoutKey, maxNodeWidth, showEdgeLabels]);
191
199
 
@@ -426,6 +434,18 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
426
434
  [onFileSelect],
427
435
  );
428
436
 
437
+ // Filename-badge clicks on nodes open the drawer through the same path as
438
+ // the declaration panel's file link (toggle + tree sync, no start line).
439
+ useEffect(() => {
440
+ SUBSYSTEM_CALLBACKS.onOpenFile = (componentId: string) => {
441
+ const comp = components.find((c) => c.id === componentId);
442
+ if (comp?.file) onOpenDeclarationFile(comp.file);
443
+ };
444
+ return () => {
445
+ SUBSYSTEM_CALLBACKS.onOpenFile = undefined;
446
+ };
447
+ }, [components, onOpenDeclarationFile]);
448
+
429
449
  // Detail-panel links: related-name clicks select the matching component —
430
450
  // resolved by id, name, or symbol (call labels may carry a trailing `()`).
431
451
  const resolveRelatedComponent = useCallback(
@@ -7,32 +7,46 @@
7
7
  * handles all formatting.
8
8
  */
9
9
 
10
- import type { SubsystemComponent } from './model';
10
+ import type { SubsystemComponent, SubsystemComponentConstruct } from './model';
11
11
  import type { GraphifyComponentDetail } from '../graphify';
12
12
 
13
+ const TYPE_FAMILY_CONSTRUCTS: ReadonlySet<string> = new Set([
14
+ 'interface',
15
+ 'type_alias',
16
+ 'enum',
17
+ ]);
18
+
13
19
  export function generateDeclarationString(component: SubsystemComponent): string {
14
20
  const detail = component.detail;
15
- const kind = detail?.kind ?? component.kind;
21
+ // Type-family constructs own their rendering even when the detail payload
22
+ // is the shared `type` shape — the construct says which keyword is honest.
23
+ const construct = TYPE_FAMILY_CONSTRUCTS.has(component.construct)
24
+ ? component.construct
25
+ : detail?.kind ?? component.construct;
16
26
  const rawName = component.symbol || component.name || 'untitled';
17
27
  // Strip class/object prefix from dotted symbols (e.g. "SessionReader.normalize" → "normalize").
18
28
  const name = rawName.includes('.') ? rawName.split('.').pop()! : rawName;
19
29
 
20
- switch (kind) {
30
+ switch (construct) {
21
31
  case 'class':
22
32
  return generateClass(name, detail);
23
33
  case 'function':
24
34
  return generateFunction(name, detail);
25
35
  case 'method':
26
36
  return generateMethod(name, detail);
27
- case 'type':
28
- return generateType(name, detail);
37
+ case 'interface':
38
+ case 'type_alias':
39
+ case 'enum':
40
+ return generateType(name, component.construct, detail);
29
41
  case 'module':
30
42
  return generateModule(detail);
43
+ case 'store':
44
+ return generateStore(name, detail);
31
45
  case 'external':
32
46
  // Not valid TypeScript — caller should handle formatting.
33
47
  return `external '${detail?.kind === 'external' ? detail.label : name}'`;
34
48
  default:
35
- return `${kind} ${name}`;
49
+ return `${construct} ${name}`;
36
50
  }
37
51
  }
38
52
 
@@ -48,7 +62,7 @@ function formatParams(params: { name?: string; type: string }[]): string {
48
62
  }
49
63
 
50
64
  // ---------------------------------------------------------------------------
51
- // Per-kind generators
65
+ // Per-construct generators
52
66
  // ---------------------------------------------------------------------------
53
67
 
54
68
  function generateClass(name: string, detail?: GraphifyComponentDetail): string {
@@ -98,16 +112,31 @@ function generateMethod(name: string, detail?: GraphifyComponentDetail): string
98
112
  return `class ${hostClass} {\n ${name}(${params})${ret};\n}`;
99
113
  }
100
114
 
101
- function generateType(name: string, detail?: GraphifyComponentDetail): string {
115
+ function generateType(
116
+ name: string,
117
+ construct: SubsystemComponentConstruct,
118
+ detail?: GraphifyComponentDetail,
119
+ ): string {
120
+ // The type-family constructs render their declaration keyword honestly —
121
+ // the construct itself says interface / type (alias) / enum / variable.
102
122
  const tpe = detail?.kind === 'type' ? detail : undefined;
103
123
  const props = (tpe?.properties ?? [])
104
124
  .map((p) => ` ${p.name}${p.type ? `: ${p.type}` : ''};`)
105
125
  .join('\n');
106
126
 
107
- if (props) {
108
- return `interface ${name} {\n${props}\n}`;
127
+ switch (construct) {
128
+ case 'enum':
129
+ return `enum ${name} { ${(tpe?.properties ?? []).map((p) => p.name).join(', ')} }`;
130
+ case 'type_alias':
131
+ return props
132
+ ? `type ${name} = {\n${props}\n};`
133
+ : `type ${name} = unknown;`;
134
+ default:
135
+ if (props) {
136
+ return `interface ${name} {\n${props}\n}`;
137
+ }
138
+ return `interface ${name} {}`;
109
139
  }
110
- return `interface ${name} {}`;
111
140
  }
112
141
 
113
142
  function generateModule(detail?: GraphifyComponentDetail): string {
@@ -126,3 +155,20 @@ function generateModule(detail?: GraphifyComponentDetail): string {
126
155
 
127
156
  return parts.join('\n') || `module {}`;
128
157
  }
158
+
159
+ /**
160
+ * A store renders as its retained state — ambient `declare const` lines for
161
+ * the state members, never a class/method stub. The node's name labels the
162
+ * block; the access mechanism lives in separate accessor nodes.
163
+ */
164
+ function generateStore(name: string, detail?: GraphifyComponentDetail): string {
165
+ const store = detail?.kind === 'store' ? detail : undefined;
166
+ const props = (store?.properties ?? [])
167
+ .map((p) => `declare const ${p.name}${p.type ? `: ${p.type}` : ''};`)
168
+ .join('\n');
169
+
170
+ if (!props) {
171
+ return `// store: ${name} — no captured state members`;
172
+ }
173
+ return `// store: ${name}\n${props}`;
174
+ }
@@ -36,11 +36,30 @@ describe('subsystem graph model', () => {
36
36
  expect(converted[0].target).toBe('reader');
37
37
  });
38
38
 
39
+ test('buildSubsystemGraph tolerates external components with no file', async () => {
40
+ const withExternal: SubsystemComponent[] = [
41
+ ...comps,
42
+ {
43
+ id: "proposed-watcher",
44
+ name: "watchDir",
45
+ kind: "external",
46
+ // Intentionally omit file/purl — agents often leave these off for externals.
47
+ file: undefined as unknown as string,
48
+ purl: undefined as unknown as string,
49
+ },
50
+ ];
51
+ const { nodes } = await buildSubsystemGraph({
52
+ components: withExternal,
53
+ edges: [{ id: 'e3', from: 'reader', to: 'proposed-watcher', mechanism: 'feeds' }],
54
+ });
55
+ expect(nodes.find((n) => n.id === 'proposed-watcher')).toBeDefined();
56
+ });
57
+
39
58
  test('buildSubsystemGraph creates external stub nodes for non-component targets', async () => {
40
59
  const { nodes, edges: gEdges } = await buildSubsystemGraph({ components: comps, edges });
41
60
  const external = nodes.find((n) => n.id === 'external:host');
42
61
  expect(external).toBeDefined();
43
- expect(external!.data.component.kind).toBe('external');
62
+ expect(external!.data.component.construct).toBe('external');
44
63
  const crossEdge = gEdges.find((e) => e.target === 'external:host');
45
64
  expect(crossEdge).toBeDefined();
46
65
  });
@@ -62,14 +81,27 @@ describe('subsystem graph model', () => {
62
81
  });
63
82
 
64
83
  test('deriveNameFromSymbol is consistent per kind', () => {
65
- // class/type/module/function use the symbol as-is.
66
- expect(deriveNameFromSymbol('SessionReader', 'class')).toBe('SessionReader');
67
- expect(deriveNameFromSymbol('SessionRecord', 'type')).toBe('SessionRecord');
68
- // falls back to existing name when no symbol.
69
- expect(deriveNameFromSymbol(undefined, 'class', 'SessionReader')).toBe('SessionReader');
84
+ // brace-bodied constructs wear {}; module uses the symbol as-is.
85
+ expect(deriveNameFromSymbol('SessionReader', 'class')).toBe('SessionReader {}');
86
+ expect(deriveNameFromSymbol('SessionRecord', 'type_alias')).toBe('SessionRecord {}');
87
+ expect(deriveNameFromSymbol('transcript', 'module')).toBe('transcript');
88
+ // falls back to existing name when no symbol (still brace-decorated).
89
+ expect(deriveNameFromSymbol(undefined, 'class', 'SessionReader')).toBe('SessionReader {}');
70
90
  expect(deriveNameFromSymbol('', 'external', 'trail-viewer-host')).toBe('trail-viewer-host');
71
91
  });
72
92
 
93
+ test('executable constructs wear () on the node', () => {
94
+ expect(deriveNameFromSymbol('createSubsystemGraph', 'function')).toBe('createSubsystemGraph()');
95
+ // methods keep the dotted ownership symbol and wear the parens
96
+ expect(deriveNameFromSymbol('SessionCache.put', 'method')).toBe('SessionCache.put()');
97
+ // already-parenthesized labels don't double up
98
+ expect(deriveNameFromSymbol('run()', 'function')).toBe('run()');
99
+ // data-shaped constructs stay bare
100
+ expect(deriveNameFromSymbol('ROOT', 'store')).toBe('ROOT');
101
+ // brace bodies don't double up
102
+ expect(deriveNameFromSymbol('Foo {}', 'class')).toBe('Foo {}');
103
+ });
104
+
73
105
  test('deriveNameFromSymbol falls back to file basename for modules', () => {
74
106
  expect(deriveNameFromSymbol(undefined, 'module', undefined, 'transcript.ts')).toBe('transcript');
75
107
  expect(deriveNameFromSymbol(undefined, 'module', undefined, 'src/event-processors/index.ts')).toBe('index');