@principal-ai/subsystems-react 0.33.0 → 0.34.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.
@@ -0,0 +1,269 @@
1
+ import React, { useState } from 'react';
2
+ import '@xyflow/react/dist/style.css';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
5
+ import { ComponentDeclaration } from '../../subsystem/ComponentDeclaration';
6
+ import type { SubsystemComponent } from '../../subsystem/model';
7
+ import type { GraphifyComponentDetail } from '../../graphify';
8
+ import type { SymbolInspection } from '../../subsystem/symbolRefs';
9
+
10
+ const meta = {
11
+ title: 'Subsystem/ComponentDeclarationSymbols',
12
+ component: ComponentDeclaration,
13
+ parameters: { layout: 'padded' },
14
+ tags: ['autodocs'],
15
+ decorators: [
16
+ (Story) => (
17
+ <ThemeProvider theme={defaultEditorTheme}>
18
+ <div style={{ maxWidth: '80ch', margin: '0 auto' }}>
19
+ <Story />
20
+ </div>
21
+ </ThemeProvider>
22
+ ),
23
+ ],
24
+ } satisfies Meta<typeof ComponentDeclaration>;
25
+ export default meta;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Fixture graphify lookups — one per information level. Names absent from the
29
+ // map resolve as `missing`, mirroring "nothing in the cached graph".
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const INSPECTIONS: Record<string, SymbolInspection> = {
33
+ RawEvent: {
34
+ symbol: 'RawEvent',
35
+ purl: 'pkg:github/principal-ai/agent-monitoring',
36
+ resolution: 'resolved',
37
+ node: {
38
+ nodeId: 'eventprocessing_types_rawevent',
39
+ label: 'RawEvent',
40
+ sourceFile: 'src/event-processing/types.ts',
41
+ sourceLocation: 'L8',
42
+ },
43
+ declaration: {
44
+ kind: 'type',
45
+ properties: [
46
+ { name: 'id', type: 'string' },
47
+ { name: 'at', type: 'number' },
48
+ { name: 'payload', type: 'unknown' },
49
+ ],
50
+ usedBy: [],
51
+ implementors: [],
52
+ },
53
+ },
54
+ BaseProcessor: {
55
+ symbol: 'BaseProcessor',
56
+ purl: 'pkg:github/principal-ai/agent-monitoring',
57
+ resolution: 'resolved',
58
+ node: {
59
+ nodeId: 'eventprocessing_baseprocessor_baseprocessor',
60
+ label: 'BaseProcessor',
61
+ sourceFile: 'src/event-processing/BaseProcessor.ts',
62
+ sourceLocation: 'L3',
63
+ },
64
+ declaration: {
65
+ kind: 'class',
66
+ methods: [{ nodeId: 'm-run', name: 'run', parameters: [], returnType: 'void' }],
67
+ properties: [],
68
+ extends: [],
69
+ implements: [],
70
+ instantiations: [],
71
+ references: [],
72
+ },
73
+ },
74
+ // Limited: graphify resolved the location but couldn't reconstruct the
75
+ // shape — the card falls back to the source declaration, still offering
76
+ // "Add to references" because the model has no structured declaration.
77
+ ProcessingOptions: {
78
+ symbol: 'ProcessingOptions',
79
+ purl: 'pkg:github/principal-ai/agent-monitoring',
80
+ resolution: 'resolved',
81
+ node: {
82
+ nodeId: 'eventprocessing_types_processingoptions',
83
+ label: 'ProcessingOptions',
84
+ sourceFile: 'src/event-processing/types.ts',
85
+ sourceLocation: 'L21',
86
+ },
87
+ source: {
88
+ file: 'src/event-processing/types.ts',
89
+ startLine: 21,
90
+ text: 'interface ProcessingOptions {\n retries: number;\n timeoutMs: number;\n}',
91
+ },
92
+ },
93
+ // Limited: multiple same-label definitions.
94
+ ProcessedEvent: {
95
+ symbol: 'ProcessedEvent',
96
+ resolution: 'ambiguous',
97
+ candidates: [
98
+ { nodeId: 'a1', label: 'ProcessedEvent', sourceFile: 'src/event-processing/types.ts' },
99
+ { nodeId: 'a2', label: 'ProcessedEvent', sourceFile: 'src/legacy/types.ts' },
100
+ ],
101
+ },
102
+ // Limited: referenced but not defined in this corpus (external/ambient type).
103
+ Disposable: {
104
+ symbol: 'Disposable',
105
+ resolution: 'unresolved',
106
+ reason: 'referenced but not defined in this corpus',
107
+ },
108
+ // Limited: nothing in the corpus.
109
+ EventEmitterLike: {
110
+ symbol: 'EventEmitterLike',
111
+ resolution: 'missing',
112
+ reason: 'no matching definition in the cached graph',
113
+ },
114
+ SessionRecord: {
115
+ symbol: 'SessionRecord',
116
+ resolution: 'resolved',
117
+ node: {
118
+ nodeId: 'session_transcript_sessionrecord',
119
+ label: 'SessionRecord',
120
+ sourceFile: 'src/session/transcript.ts',
121
+ sourceLocation: 'L12',
122
+ },
123
+ declaration: {
124
+ kind: 'type',
125
+ properties: [
126
+ { name: 'id', type: 'string' },
127
+ { name: 'admittedSeq', type: 'number' },
128
+ { name: 'events', type: 'SessionEvent[]' },
129
+ ],
130
+ usedBy: [],
131
+ implementors: [],
132
+ },
133
+ },
134
+ 'capture-session': {
135
+ symbol: 'capture-session',
136
+ resolution: 'resolved',
137
+ node: {
138
+ nodeId: 'session_capture_capture_session',
139
+ label: 'capture-session()',
140
+ sourceFile: 'src/session/capture.ts',
141
+ sourceLocation: 'L40',
142
+ },
143
+ declaration: {
144
+ kind: 'function',
145
+ parameters: [{ name: 'session', type: 'SessionRecord' }],
146
+ returnType: 'Promise<void>',
147
+ callers: [],
148
+ callees: [],
149
+ },
150
+ },
151
+ };
152
+
153
+ const inspect = (symbol: string): Promise<SymbolInspection> =>
154
+ new Promise((resolve) =>
155
+ setTimeout(
156
+ () =>
157
+ resolve(
158
+ INSPECTIONS[symbol] ?? {
159
+ symbol,
160
+ resolution: 'missing',
161
+ reason: 'no matching definition in the cached graph',
162
+ },
163
+ ),
164
+ 220,
165
+ ),
166
+ );
167
+
168
+ function Harness({ component }: { component: SubsystemComponent }) {
169
+ const [log, setLog] = useState<string[]>([]);
170
+ return (
171
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
172
+ <div style={{ fontFamily: 'monospace', fontSize: 11, color: '#888' }}>
173
+ Click a dotted-underlined symbol to inspect what graphify has on it.
174
+ </div>
175
+ <ComponentDeclaration
176
+ component={component}
177
+ onInspectSymbol={inspect}
178
+ onAddToModel={(ref, info) =>
179
+ setLog((l) => [`add ${ref.name} (${info?.resolution ?? 'no info'})`, ...l])
180
+ }
181
+ onOpenFile={(file, opts) =>
182
+ setLog((l) => [
183
+ `open ${file}${opts?.startLine ? `:${opts.startLine}` : ''}`,
184
+ ...l,
185
+ ])
186
+ }
187
+ />
188
+ <div
189
+ data-symbol-action-log
190
+ style={{ fontFamily: 'monospace', fontSize: 11, color: '#6a9955', minHeight: 16 }}
191
+ >
192
+ {log.length ? `actions: ${log.join(' · ')}` : 'actions: —'}
193
+ </div>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // A class whose declaration references a range of symbols — resolved
200
+ // (type/class), thin (unknown construct → "Add to references"), ambiguous,
201
+ // unresolved, and missing.
202
+ // ---------------------------------------------------------------------------
203
+
204
+ const classComponent: SubsystemComponent = {
205
+ alias: 'class-event-processor',
206
+ name: 'EventProcessor',
207
+ construct: 'class',
208
+ file: 'src/event-processing/EventProcessor.ts',
209
+ purl: 'pkg:github/principal-ai/agent-monitoring',
210
+ symbol: 'EventProcessor',
211
+ purpose: 'Consumes raw events and emits processed ones.',
212
+ declaration: {
213
+ kind: 'class',
214
+ methods: [
215
+ {
216
+ nodeId: 'm-process',
217
+ name: 'process',
218
+ parameters: [
219
+ { name: 'event', type: 'RawEvent' },
220
+ { name: 'options', type: 'ProcessingOptions' },
221
+ ],
222
+ returnType: 'ProcessedEvent',
223
+ },
224
+ {
225
+ nodeId: 'm-batch',
226
+ name: 'batch',
227
+ parameters: [{ name: 'events', type: 'RawEvent[]' }],
228
+ returnType: 'Promise<ProcessedEvent[]>',
229
+ },
230
+ { nodeId: 'm-dispose', name: 'dispose' },
231
+ ],
232
+ properties: [
233
+ { name: 'queue', type: 'RawEvent[]' },
234
+ { name: 'options', type: 'Required<ProcessingOptions>' },
235
+ ],
236
+ extends: ['BaseProcessor'],
237
+ implements: ['Disposable', 'EventEmitterLike'],
238
+ instantiations: [],
239
+ references: [{ nodeId: 'n-emitter', name: 'EventEmitterLike', context: 'type' }],
240
+ } satisfies GraphifyComponentDetail,
241
+ };
242
+
243
+ export const InspectableSymbols: StoryObj<Record<string, never>> = {
244
+ render: () => <Harness component={classComponent} />,
245
+ };
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // A function — callers/callees are clickable too, alongside param/return types.
249
+ // ---------------------------------------------------------------------------
250
+
251
+ const functionComponent: SubsystemComponent = {
252
+ alias: 'fn-normalize',
253
+ name: 'normalize',
254
+ construct: 'function',
255
+ file: 'src/session/SessionReader.ts',
256
+ purl: 'pkg:github/principal-ai/agent-monitoring',
257
+ symbol: 'SessionReader.normalize',
258
+ declaration: {
259
+ kind: 'function',
260
+ parameters: [{ name: 'session', type: 'SessionRecord' }],
261
+ returnType: 'SessionEvent[]',
262
+ callers: [{ nodeId: 'c1', name: 'capture-session', source_location: 'L120' }],
263
+ callees: [{ nodeId: 'c2', name: 'toUniversalEvents', source_location: 'L64' }],
264
+ } satisfies GraphifyComponentDetail,
265
+ };
266
+
267
+ export const CallableRelated: StoryObj<Record<string, never>> = {
268
+ render: () => <Harness component={functionComponent} />,
269
+ };
@@ -6,7 +6,7 @@
6
6
  * comments. File content lives in the bottom FileDrawer, not here.
7
7
  */
8
8
 
9
- import { useEffect, useRef, useState, type ReactNode } from 'react';
9
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
10
10
  import { AlignLeft, FileText } from 'lucide-react';
11
11
  import { useTheme } from '@principal-ade/industry-theme';
12
12
  import { resolvePierreSyntaxThemeName } from '../pierre/pierreSyntaxTheme';
@@ -19,6 +19,38 @@ import type { SubsystemOpenFileOptions } from './declarationRef';
19
19
  import { parseSourceLocation } from './declarationRef';
20
20
  import { tokenizeComponent } from './tokenizeComponent';
21
21
  import { componentColor } from '../pierre/constructColors';
22
+ import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
23
+ import { extractDeclarationSymbolRefs } from './symbolRefs';
24
+ import { SymbolInspectionCard } from './SymbolInspectionCard';
25
+
26
+ /** One clickable span in the token stream, mapped to a referenced symbol. */
27
+ interface SymbolClickRange {
28
+ start: number;
29
+ end: number;
30
+ ref: DeclarationSymbolRef;
31
+ }
32
+
33
+ /** Whole-word occurrences of `name` in `text` (identifier boundaries only). */
34
+ function findSymbolOccurrences(
35
+ text: string,
36
+ name: string,
37
+ ): { start: number; end: number }[] {
38
+ const out: { start: number; end: number }[] = [];
39
+ if (!name) return out;
40
+ const isWordChar = (ch: string) => /[\w$]/.test(ch);
41
+ let from = 0;
42
+ for (;;) {
43
+ const idx = text.indexOf(name, from);
44
+ if (idx < 0) break;
45
+ const before = idx > 0 ? text[idx - 1]! : '';
46
+ const after = text[idx + name.length] ?? '';
47
+ if ((!before || !isWordChar(before)) && (!after || !isWordChar(after))) {
48
+ out.push({ start: idx, end: idx + name.length });
49
+ }
50
+ from = idx + name.length;
51
+ }
52
+ return out;
53
+ }
22
54
 
23
55
  /** Live / result state for the declaration-panel Verify control. */
24
56
  export type ComponentVerificationPhase =
@@ -113,6 +145,14 @@ export interface ComponentDeclarationProps {
113
145
  /** Related-name click (types, callers/callees, implementors, …) → select
114
146
  * that component if one matches; unmatched refs no-op. */
115
147
  onRelatedSelect?: (ref: string) => void;
148
+ /** Referenced-symbol click → resolve it against graphify. When absent,
149
+ * symbols are not clickable. Host-injected; stories pass a fixture. */
150
+ onInspectSymbol?: (
151
+ symbol: string,
152
+ ref: DeclarationSymbolRef,
153
+ ) => Promise<SymbolInspection | null> | SymbolInspection | null;
154
+ /** "Add to references" from the inspection card. */
155
+ onAddToModel?: (ref: DeclarationSymbolRef, info?: SymbolInspection) => void;
116
156
  /** Max width of the declaration panel (CSS value). Defaults to none. */
117
157
  maxWidth?: string | number;
118
158
  /** Live verification status for the selected component. */
@@ -267,6 +307,8 @@ export function ComponentDeclaration({
267
307
  component,
268
308
  onOpenFile,
269
309
  onRelatedSelect: _onRelatedSelect,
310
+ onInspectSymbol,
311
+ onAddToModel,
270
312
  maxWidth,
271
313
  verification,
272
314
  defaultShowFile = false,
@@ -300,6 +342,53 @@ export function ComponentDeclaration({
300
342
  /** Fallback when wire tokens omit Shiki colors. */
301
343
  const declarationTextColor = muted;
302
344
 
345
+ // Symbols the declaration references — made clickable for a graphify lookup.
346
+ const symbolRefs = useMemo(
347
+ () => extractDeclarationSymbolRefs(component.declaration),
348
+ [component.declaration],
349
+ );
350
+ const [inspection, setInspection] = useState<
351
+ | {
352
+ ref: DeclarationSymbolRef;
353
+ loading: boolean;
354
+ info?: SymbolInspection;
355
+ error?: string;
356
+ x: number;
357
+ y: number;
358
+ }
359
+ | null
360
+ >(null);
361
+ // Symbol name currently hovered, so every occurrence of the same symbol can
362
+ // highlight together (and stay lit while its popover is open).
363
+ const [hoveredSymbol, setHoveredSymbol] = useState<string | null>(null);
364
+ const activeSymbol = inspection?.ref.name ?? null;
365
+
366
+ // Dismiss the inspection card on outside click / Escape.
367
+ useEffect(() => {
368
+ if (!inspection) return;
369
+ const onDown = (e: MouseEvent) => {
370
+ const t = e.target as HTMLElement | null;
371
+ // The popover, and the symbol spans themselves (they toggle on click),
372
+ // must not be treated as outside.
373
+ if (t?.closest('[data-symbol-inspection]')) return;
374
+ if (t?.closest('[data-symbol-ref]')) return;
375
+ setInspection(null);
376
+ };
377
+ const onKey = (e: KeyboardEvent) => {
378
+ if (e.key === 'Escape') {
379
+ // Drop focus from the symbol span so no focus ring lingers after close.
380
+ (document.activeElement as HTMLElement | null)?.blur?.();
381
+ setInspection(null);
382
+ }
383
+ };
384
+ document.addEventListener('mousedown', onDown);
385
+ document.addEventListener('keydown', onKey);
386
+ return () => {
387
+ document.removeEventListener('mousedown', onDown);
388
+ document.removeEventListener('keydown', onKey);
389
+ };
390
+ }, [inspection]);
391
+
303
392
  const declarationStartLine =
304
393
  component.declarationRef?.startLine ??
305
394
  verification?.declaration?.ref?.startLine ??
@@ -729,22 +818,140 @@ export function ComponentDeclaration({
729
818
  cancelled = true;
730
819
  };
731
820
  }, [component, printWidth, pierreSyntaxTheme]);
821
+
822
+ // Map character offsets in the concatenated token text to referenced symbols,
823
+ // so inline occurrences render as clickable spans. `-1` = not a symbol.
824
+ const clickIndex = useMemo(() => {
825
+ const full = tokens.map((t) => t.text).join('');
826
+ const rangeAt = new Int32Array(full.length).fill(-1);
827
+ if (!onInspectSymbol || symbolRefs.length === 0) {
828
+ return { ranges: [] as SymbolClickRange[], rangeAt };
829
+ }
830
+ const ranges: SymbolClickRange[] = [];
831
+ for (const ref of symbolRefs) {
832
+ for (const occ of findSymbolOccurrences(full, ref.name)) {
833
+ ranges.push({ start: occ.start, end: occ.end, ref });
834
+ }
835
+ }
836
+ ranges.sort((a, b) => a.start - b.start);
837
+ ranges.forEach((r, i) => {
838
+ for (let k = r.start; k < r.end && k < rangeAt.length; k++) {
839
+ if (rangeAt[k] === -1) rangeAt[k] = i;
840
+ }
841
+ });
842
+ return { ranges, rangeAt };
843
+ }, [onInspectSymbol, symbolRefs, tokens]);
844
+
845
+ const handleInspect = useCallback(
846
+ async (ref: DeclarationSymbolRef, el: HTMLElement) => {
847
+ if (!onInspectSymbol) return;
848
+ // Toggle: clicking the symbol whose popover is already open closes it.
849
+ if (inspection?.ref.name === ref.name) {
850
+ setInspection(null);
851
+ return;
852
+ }
853
+ let x = 0;
854
+ let y = 0;
855
+ const cont = containerRef.current;
856
+ if (cont) {
857
+ const crect = cont.getBoundingClientRect();
858
+ const r = el.getBoundingClientRect();
859
+ x = r.left - crect.left;
860
+ y = r.bottom - crect.top + 4;
861
+ }
862
+ setInspection({ ref, loading: true, x, y });
863
+ try {
864
+ const info = await onInspectSymbol(ref.name, ref);
865
+ setInspection((cur) =>
866
+ cur?.ref.name === ref.name
867
+ ? { ref, loading: false, info: info ?? undefined, x, y }
868
+ : cur,
869
+ );
870
+ } catch (err) {
871
+ setInspection((cur) =>
872
+ cur?.ref.name === ref.name
873
+ ? {
874
+ ref,
875
+ loading: false,
876
+ error: err instanceof Error ? err.message : String(err),
877
+ x,
878
+ y,
879
+ }
880
+ : cur,
881
+ );
882
+ }
883
+ },
884
+ [onInspectSymbol, inspection],
885
+ );
886
+
732
887
  const declLines: ReactNode[][] = [[]];
733
888
  let di = 0;
889
+ let offset = 0;
734
890
  for (const tok of tokens) {
735
891
  if (tok.kind === 'newline') {
736
892
  declLines.push([]);
737
893
  di++;
738
894
  continue;
739
895
  }
740
- declLines[di].push(
741
- <span
742
- key={`${di}-${declLines[di].length}`}
743
- style={{ color: tok.color ?? tokenColor[tok.kind] ?? declarationTextColor }}
744
- >
745
- {tok.text}
746
- </span>,
747
- );
896
+ const color = tok.color ?? tokenColor[tok.kind] ?? declarationTextColor;
897
+ let i = 0;
898
+ while (i < tok.text.length) {
899
+ const rIdx = clickIndex.rangeAt[offset + i] ?? -1;
900
+ let j = i + 1;
901
+ while (j < tok.text.length && (clickIndex.rangeAt[offset + j] ?? -1) === rIdx) {
902
+ j++;
903
+ }
904
+ const piece = tok.text.slice(i, j);
905
+ const key = `${di}-${declLines[di].length}`;
906
+ const range = rIdx >= 0 ? clickIndex.ranges[rIdx] : undefined;
907
+ if (range) {
908
+ const ref = range.ref;
909
+ const hovered = hoveredSymbol === ref.name;
910
+ // Active (popover open for this symbol) is the only state that gets a
911
+ // solid underline; hover just shifts the color.
912
+ const active = activeSymbol === ref.name;
913
+ const lit = hovered || active;
914
+ declLines[di].push(
915
+ <span
916
+ key={key}
917
+ role="button"
918
+ tabIndex={0}
919
+ data-symbol-ref={ref.name}
920
+ // Don't take focus on mouse click — otherwise closing the popover
921
+ // (Escape) leaves a focus ring around the clicked symbol.
922
+ onMouseDown={(e) => e.preventDefault()}
923
+ onMouseEnter={() => setHoveredSymbol(ref.name)}
924
+ onMouseLeave={(e) => {
925
+ // Keep it lit when moving straight to another occurrence of the
926
+ // same symbol; otherwise clear on unhover.
927
+ const to = e.relatedTarget as HTMLElement | null;
928
+ const next = to?.closest('[data-symbol-ref]')?.getAttribute('data-symbol-ref');
929
+ if (next === ref.name) return;
930
+ setHoveredSymbol((s) => (s === ref.name ? null : s));
931
+ }}
932
+ onClick={(e) => handleInspect(ref, e.currentTarget)}
933
+ style={{
934
+ // Lit occurrences shift to the accent color (no background fill).
935
+ color: lit ? theme.colors.accent ?? theme.colors.secondary : color,
936
+ cursor: 'pointer',
937
+ textDecorationLine: 'underline',
938
+ textDecorationStyle: active ? 'solid' : 'dotted',
939
+ textUnderlineOffset: 2,
940
+ }}
941
+ >
942
+ {piece}
943
+ </span>,
944
+ );
945
+ } else {
946
+ declLines[di].push(
947
+ <span key={key} style={{ color }}>
948
+ {piece}
949
+ </span>,
950
+ );
951
+ }
952
+ i = j;
953
+ }
954
+ offset += tok.text.length;
748
955
  }
749
956
  for (let li = 0; li < declLines.length; li++) {
750
957
  if (declLines[li].length === 0) continue;
@@ -783,10 +990,22 @@ export function ComponentDeclaration({
783
990
  );
784
991
  }
785
992
 
993
+ const inspectionLeft = inspection
994
+ ? Math.max(
995
+ 0,
996
+ Math.min(
997
+ inspection.x,
998
+ Math.max(0, (containerRef.current?.clientWidth ?? 320) - 320),
999
+ ),
1000
+ )
1001
+ : 0;
1002
+
786
1003
  return (
787
1004
  <div
788
1005
  ref={containerRef}
1006
+ onMouseLeave={() => setHoveredSymbol(null)}
789
1007
  style={{
1008
+ position: 'relative',
790
1009
  width: '100%',
791
1010
  minWidth: 0,
792
1011
  boxSizing: 'border-box',
@@ -800,6 +1019,17 @@ export function ComponentDeclaration({
800
1019
  }}
801
1020
  >
802
1021
  {lines}
1022
+ {inspection && !inspection.loading && (
1023
+ <SymbolInspectionCard
1024
+ symbolRef={inspection.ref}
1025
+ inspection={inspection.info}
1026
+ error={inspection.error}
1027
+ onClose={() => setInspection(null)}
1028
+ onAddToModel={onAddToModel}
1029
+ onOpenFile={onOpenFile}
1030
+ style={{ left: inspectionLeft, top: inspection.y }}
1031
+ />
1032
+ )}
803
1033
  </div>
804
1034
  );
805
1035
  }
@@ -38,13 +38,15 @@ export type SubsystemIssueTargetKind =
38
38
 
39
39
  /**
40
40
  * The four verification layers, in order. Mirrors the docs' progression:
41
- * constructs → static topology → runtime topology → walkthrough.
41
+ * constructs → static topology → dynamic topology → walkthrough.
42
+ * Static topology = relations; dynamic topology = process (runtime) +
43
+ * package/module (containment).
42
44
  */
43
45
  export type SubsystemIssueCategory =
44
46
  | 'repo'
45
47
  | 'construct'
46
48
  | 'static-topology'
47
- | 'runtime-topology'
49
+ | 'dynamic-topology'
48
50
  | 'walkthrough';
49
51
 
50
52
  /** Layer order + display names, shared by the list and its headers. */
@@ -52,7 +54,7 @@ export const SUBSYSTEM_ISSUE_CATEGORIES: SubsystemIssueCategory[] = [
52
54
  'repo',
53
55
  'construct',
54
56
  'static-topology',
55
- 'runtime-topology',
57
+ 'dynamic-topology',
56
58
  'walkthrough',
57
59
  ];
58
60
 
@@ -64,7 +66,7 @@ export const SUBSYSTEM_ISSUE_CATEGORY_ICON: Record<
64
66
  repo: FolderGit2,
65
67
  construct: Component,
66
68
  'static-topology': Network,
67
- 'runtime-topology': Server,
69
+ 'dynamic-topology': Server,
68
70
  walkthrough: Route,
69
71
  };
70
72
 
@@ -75,11 +77,11 @@ export const SUBSYSTEM_ISSUE_CATEGORY_LABEL: Record<
75
77
  repo: 'Repositories',
76
78
  construct: 'Constructs',
77
79
  'static-topology': 'Static topology',
78
- 'runtime-topology': 'Runtime topology',
80
+ 'dynamic-topology': 'Dynamic topology',
79
81
  walkthrough: 'Walkthrough',
80
82
  };
81
83
 
82
- /** Default `kind` → layer. `boundary_*` splits by module (static) vs process (runtime). */
84
+ /** Default `kind` → layer. `boundary_*` (module containment + process) is dynamic. */
83
85
  const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
84
86
  missing_file: 'construct',
85
87
  symbol_ambiguous: 'construct',
@@ -94,8 +96,8 @@ const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
94
96
  topology_broken_endpoint: 'static-topology',
95
97
  topology_import_unconfirmed: 'static-topology',
96
98
  topology_relation_unconfirmed: 'static-topology',
97
- boundary_module_file_mismatch: 'static-topology',
98
- boundary_process_nest_disagree: 'runtime-topology',
99
+ boundary_module_file_mismatch: 'dynamic-topology',
100
+ boundary_process_nest_disagree: 'dynamic-topology',
99
101
  walkthrough: 'walkthrough',
100
102
  };
101
103