@principal-ai/subsystems-react 0.33.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +7 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +169 -5
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +9 -1
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +9 -2
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +4 -2
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +6 -6
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +13 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +10 -3
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.d.ts +31 -0
- package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -0
- package/dist/subsystem/SymbolInspectionCard.js +247 -0
- package/dist/subsystem/SymbolInspectionCard.js.map +1 -0
- package/dist/subsystem/symbolRefs.d.ts +95 -0
- package/dist/subsystem/symbolRefs.d.ts.map +1 -0
- package/dist/subsystem/symbolRefs.js +167 -0
- package/dist/subsystem/symbolRefs.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +14 -0
- package/src/stories/Subsystem/ComponentDeclarationSymbols.stories.tsx +269 -0
- package/src/subsystem/ComponentDeclaration.tsx +239 -9
- package/src/subsystem/ConstructsCatalog.tsx +20 -0
- package/src/subsystem/IssueList.tsx +10 -8
- package/src/subsystem/SubsystemComponentGraph.tsx +26 -1
- package/src/subsystem/SymbolInspectionCard.tsx +433 -0
- package/src/subsystem/symbolRefs.test.ts +127 -0
- package/src/subsystem/symbolRefs.ts +256 -0
|
@@ -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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import type { SubsystemOpenFileOptions } from './declarationRef';
|
|
23
23
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
24
24
|
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
25
|
+
import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
|
|
25
26
|
import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './DiagnosticToggle';
|
|
26
27
|
import { FileDrawer } from './FileDrawer';
|
|
27
28
|
import { componentColor } from '../pierre/constructColors';
|
|
@@ -40,6 +41,13 @@ export interface ConstructsCatalogProps {
|
|
|
40
41
|
renderFileView?: (component: SubsystemComponent) => ReactNode;
|
|
41
42
|
onFileSelect?: (file: string) => void;
|
|
42
43
|
componentVerification?: ComponentVerificationState | null;
|
|
44
|
+
/** Referenced-symbol click → host graphify lookup (purl/file from the card). */
|
|
45
|
+
onInspectSymbol?: (req: {
|
|
46
|
+
purl: string;
|
|
47
|
+
file: string;
|
|
48
|
+
symbol: string;
|
|
49
|
+
ref: DeclarationSymbolRef;
|
|
50
|
+
}) => Promise<SymbolInspection | null> | SymbolInspection | null;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
const FileDrawerContent = memo(function FileDrawerContent({
|
|
@@ -145,6 +153,7 @@ export function ConstructsCatalog({
|
|
|
145
153
|
renderFileView,
|
|
146
154
|
onFileSelect,
|
|
147
155
|
componentVerification,
|
|
156
|
+
onInspectSymbol,
|
|
148
157
|
}: ConstructsCatalogProps) {
|
|
149
158
|
const { theme, mode } = useTheme();
|
|
150
159
|
const pierreTheme = resolvePierreSyntaxThemeName(mode);
|
|
@@ -948,6 +957,17 @@ export function ConstructsCatalog({
|
|
|
948
957
|
onOpenFile={fileViewer ? onOpenFile : undefined}
|
|
949
958
|
defaultShowFile={!!fileViewer}
|
|
950
959
|
onRelatedSelect={onRelatedSelect}
|
|
960
|
+
onInspectSymbol={
|
|
961
|
+
onInspectSymbol
|
|
962
|
+
? (symbol, ref) =>
|
|
963
|
+
onInspectSymbol({
|
|
964
|
+
purl: c.purl,
|
|
965
|
+
file: c.file,
|
|
966
|
+
symbol,
|
|
967
|
+
ref,
|
|
968
|
+
})
|
|
969
|
+
: undefined
|
|
970
|
+
}
|
|
951
971
|
fileOpen={fileOpen}
|
|
952
972
|
declarationOpen={lineOpen}
|
|
953
973
|
showRepoIdentity={false}
|
|
@@ -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 →
|
|
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
|
-
| '
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
80
|
+
'dynamic-topology': 'Dynamic topology',
|
|
79
81
|
walkthrough: 'Walkthrough',
|
|
80
82
|
};
|
|
81
83
|
|
|
82
|
-
/** Default `kind` → layer. `boundary_*`
|
|
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: '
|
|
98
|
-
boundary_process_nest_disagree: '
|
|
99
|
+
boundary_module_file_mismatch: 'dynamic-topology',
|
|
100
|
+
boundary_process_nest_disagree: 'dynamic-topology',
|
|
99
101
|
walkthrough: 'walkthrough',
|
|
100
102
|
};
|
|
101
103
|
|
|
@@ -65,6 +65,7 @@ import { GraphLayoutCover } from './GraphLayoutCover';
|
|
|
65
65
|
import { GRAPH_NAV_PROPS, GraphChrome } from './graphChrome';
|
|
66
66
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
67
67
|
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
68
|
+
import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
|
|
68
69
|
import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
|
|
69
70
|
import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
|
|
70
71
|
import { WalkthroughsPanel, WALKTHROUGH_PLAY_PAUSE_MS } from './WalkthroughsPanel';
|
|
@@ -287,6 +288,18 @@ export interface SubsystemComponentGraphProps {
|
|
|
287
288
|
onFileSelect?: (file: string) => void;
|
|
288
289
|
/** Live verification status for the selected component. */
|
|
289
290
|
componentVerification?: ComponentVerificationState | null;
|
|
291
|
+
/**
|
|
292
|
+
* Referenced-symbol click in the declaration panel → resolve it against the
|
|
293
|
+
* host's graphify cache. The graph supplies the selected component's
|
|
294
|
+
* purl/file so the host can pick the right repo graph; the host returns what
|
|
295
|
+
* graphify knows (declaration / source / candidates).
|
|
296
|
+
*/
|
|
297
|
+
onInspectSymbol?: (req: {
|
|
298
|
+
purl: string;
|
|
299
|
+
file: string;
|
|
300
|
+
symbol: string;
|
|
301
|
+
ref: DeclarationSymbolRef;
|
|
302
|
+
}) => Promise<SymbolInspection | null> | SymbolInspection | null;
|
|
290
303
|
/**
|
|
291
304
|
* When set, the graph's walkthrough working set — expanded flows and the
|
|
292
305
|
* selected flow/step — is persisted to `localStorage` under this key and
|
|
@@ -383,7 +396,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
383
396
|
measured: { w: number; h: number } | null;
|
|
384
397
|
}
|
|
385
398
|
|
|
386
|
-
function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, persistKey }: InnerProps) {
|
|
399
|
+
function Inner({ components, relations, walkthroughs, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, persistKey }: InnerProps) {
|
|
387
400
|
const { theme } = useTheme();
|
|
388
401
|
const { fitView } = useReactFlow();
|
|
389
402
|
const viewport = useViewport();
|
|
@@ -2136,6 +2149,17 @@ function Inner({ components, relations, walkthroughs, initialWalkthroughId, onRe
|
|
|
2136
2149
|
component={selected}
|
|
2137
2150
|
onOpenFile={onOpenDeclarationFile}
|
|
2138
2151
|
onRelatedSelect={resolveRelatedComponent}
|
|
2152
|
+
onInspectSymbol={
|
|
2153
|
+
onInspectSymbol
|
|
2154
|
+
? (symbol, ref) =>
|
|
2155
|
+
onInspectSymbol({
|
|
2156
|
+
purl: selected.purl,
|
|
2157
|
+
file: selected.file,
|
|
2158
|
+
symbol,
|
|
2159
|
+
ref,
|
|
2160
|
+
})
|
|
2161
|
+
: undefined
|
|
2162
|
+
}
|
|
2139
2163
|
verification={componentVerification}
|
|
2140
2164
|
fileOpen={
|
|
2141
2165
|
drawerTarget?.kind === 'file' &&
|
|
@@ -2365,6 +2389,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
2365
2389
|
renderFileView={props.renderFileView}
|
|
2366
2390
|
onFileSelect={props.onFileSelect}
|
|
2367
2391
|
componentVerification={props.componentVerification}
|
|
2392
|
+
onInspectSymbol={props.onInspectSymbol}
|
|
2368
2393
|
/>
|
|
2369
2394
|
) : (
|
|
2370
2395
|
<ReactFlowProvider>
|