@principal-ai/principal-view-react 0.16.47 → 0.16.50
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/graphify/anchor.d.ts +28 -0
- package/dist/graphify/anchor.d.ts.map +1 -0
- package/dist/graphify/anchor.js +132 -0
- package/dist/graphify/anchor.js.map +1 -0
- package/dist/graphify/consolidated.d.ts +12 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/ids.d.ts +19 -0
- package/dist/graphify/ids.d.ts.map +1 -0
- package/dist/graphify/ids.js +46 -0
- package/dist/graphify/ids.js.map +1 -0
- package/dist/graphify/index.d.ts +7 -0
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js +4 -0
- package/dist/graphify/index.js.map +1 -1
- package/dist/graphify/kind.d.ts +23 -0
- package/dist/graphify/kind.d.ts.map +1 -0
- package/dist/graphify/kind.js +71 -0
- package/dist/graphify/kind.js.map +1 -0
- package/dist/graphify/signature.d.ts +87 -0
- package/dist/graphify/signature.d.ts.map +1 -0
- package/dist/graphify/signature.js +276 -0
- package/dist/graphify/signature.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +58 -3
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +186 -53
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +5 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +2 -2
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +12 -1
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +1 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +1 -0
- package/dist/subsystem/model.js.map +1 -1
- package/package.json +1 -1
- package/src/graphify/anchor.test.ts +124 -0
- package/src/graphify/anchor.ts +160 -0
- package/src/graphify/consolidated.ts +13 -0
- package/src/graphify/ids.ts +48 -0
- package/src/graphify/index.ts +26 -0
- package/src/graphify/kind.test.ts +108 -0
- package/src/graphify/kind.ts +114 -0
- package/src/graphify/signature.test.ts +278 -0
- package/src/graphify/signature.ts +354 -0
- package/src/index.ts +26 -0
- package/src/stories/ComponentDeclarationAudit.stories.tsx +32 -0
- package/src/subsystem/ComponentDeclaration.tsx +306 -56
- package/src/subsystem/SubsystemComponentGraph.tsx +8 -1
- package/src/subsystem/formatDeclaration.ts +13 -1
- package/src/subsystem/model.ts +2 -0
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* comments. File content lives in the bottom FileDrawer, not here.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import { AlignLeft, FileText } from 'lucide-react';
|
|
9
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
10
|
+
import { AlignLeft, FileText, ShieldCheck } from 'lucide-react';
|
|
11
11
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
12
12
|
import {
|
|
13
13
|
KIND_COLOR,
|
|
@@ -17,6 +17,53 @@ import {
|
|
|
17
17
|
} from './model';
|
|
18
18
|
import { tokenizeComponent } from './tokenizeComponent';
|
|
19
19
|
|
|
20
|
+
/** Live / result state for the declaration-panel Verify control. */
|
|
21
|
+
export type ComponentVerificationPhase =
|
|
22
|
+
| 'idle'
|
|
23
|
+
| 'checking'
|
|
24
|
+
| 'done'
|
|
25
|
+
| 'error';
|
|
26
|
+
|
|
27
|
+
export interface ComponentVerificationState {
|
|
28
|
+
phase: ComponentVerificationPhase;
|
|
29
|
+
message?: string;
|
|
30
|
+
/** Mirrors host `ok` when a structured verify result is available. */
|
|
31
|
+
ok?: boolean;
|
|
32
|
+
code?: string;
|
|
33
|
+
file?: {
|
|
34
|
+
exists: boolean;
|
|
35
|
+
symbolDeclared?: boolean | null;
|
|
36
|
+
};
|
|
37
|
+
cache?: {
|
|
38
|
+
status: 'ready' | 'missing' | 'unavailable';
|
|
39
|
+
purl: string;
|
|
40
|
+
};
|
|
41
|
+
anchor?: {
|
|
42
|
+
resolution: 'exact' | 'file-only' | 'ambiguous' | 'missing';
|
|
43
|
+
nodeId?: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
source_file?: string;
|
|
46
|
+
source_location?: string;
|
|
47
|
+
candidates?: Array<{ nodeId: string; label: string; source_file?: string }>;
|
|
48
|
+
};
|
|
49
|
+
kind?: {
|
|
50
|
+
claimed: string;
|
|
51
|
+
inferred: string;
|
|
52
|
+
match: boolean;
|
|
53
|
+
evidence?: string[];
|
|
54
|
+
};
|
|
55
|
+
signature?: {
|
|
56
|
+
match: boolean;
|
|
57
|
+
skipped: boolean;
|
|
58
|
+
skipCode?: string;
|
|
59
|
+
reason?: string;
|
|
60
|
+
claimed: { parameterTypes: string[]; returnTypes: string[] };
|
|
61
|
+
inferred: { parameterTypes: string[]; returnTypes: string[] };
|
|
62
|
+
/** Graphify `inline_parameter` marker count (anonymous eager args). */
|
|
63
|
+
inlineParameters?: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
/** Shared affordance for clickable text pieces. */
|
|
21
68
|
const clickableStyle = {
|
|
22
69
|
cursor: 'pointer',
|
|
@@ -46,8 +93,8 @@ function DetailLink({ children, onClick }: { children: ReactNode; onClick?: () =
|
|
|
46
93
|
);
|
|
47
94
|
}
|
|
48
95
|
|
|
49
|
-
/** Panel props.
|
|
50
|
-
*
|
|
96
|
+
/** Panel props. Callbacks are wired by the graph; standalone usage without
|
|
97
|
+
* them renders the same panel with nothing clickable. */
|
|
51
98
|
export interface ComponentDeclarationProps {
|
|
52
99
|
component: SubsystemComponent;
|
|
53
100
|
/** File-path click → open that file in the bottom drawer. */
|
|
@@ -57,20 +104,153 @@ export interface ComponentDeclarationProps {
|
|
|
57
104
|
onRelatedSelect?: (ref: string) => void;
|
|
58
105
|
/** Max width of the declaration panel (CSS value). Defaults to none. */
|
|
59
106
|
maxWidth?: string | number;
|
|
107
|
+
/** When set, shows a Verify control that calls back with the component id. */
|
|
108
|
+
onVerify?: (componentId: string) => void;
|
|
109
|
+
/** Live verification status for the selected component. */
|
|
110
|
+
verification?: ComponentVerificationState | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function verificationSummary(
|
|
114
|
+
v: ComponentVerificationState,
|
|
115
|
+
muted: string,
|
|
116
|
+
ok: string,
|
|
117
|
+
warn: string,
|
|
118
|
+
): { text: string; color: string } {
|
|
119
|
+
if (v.phase === 'checking') {
|
|
120
|
+
return { text: v.message ?? 'Verifying…', color: muted };
|
|
121
|
+
}
|
|
122
|
+
if (v.phase === 'error') {
|
|
123
|
+
return { text: v.message ?? 'Verification failed', color: '#e5534b' };
|
|
124
|
+
}
|
|
125
|
+
if (v.phase !== 'done') {
|
|
126
|
+
return { text: '', color: muted };
|
|
127
|
+
}
|
|
128
|
+
const bits: string[] = [];
|
|
129
|
+
if (v.file) {
|
|
130
|
+
if (!v.file.exists) bits.push('file missing');
|
|
131
|
+
else if (v.file.symbolDeclared === false) bits.push('symbol not declared in file');
|
|
132
|
+
else if (v.file.symbolDeclared === true) bits.push('file+symbol ok');
|
|
133
|
+
else bits.push('file exists');
|
|
134
|
+
}
|
|
135
|
+
if (v.cache?.status === 'missing') {
|
|
136
|
+
return {
|
|
137
|
+
text: [...bits, 'graphify cache not ready — run graphify from Subsystems list'].join(' · '),
|
|
138
|
+
color: warn,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (v.cache?.status === 'unavailable') {
|
|
142
|
+
return {
|
|
143
|
+
text: [...bits, 'no local checkout for this purl'].join(' · '),
|
|
144
|
+
color: muted,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const res = v.anchor?.resolution;
|
|
148
|
+
if (res === 'exact') {
|
|
149
|
+
const loc = v.anchor?.source_location ? ` @ ${v.anchor.source_location}` : '';
|
|
150
|
+
const anchorBit = `exact → ${v.anchor?.label ?? v.anchor?.nodeId ?? 'node'}${loc}`;
|
|
151
|
+
if (v.kind && !v.kind.match) {
|
|
152
|
+
const why =
|
|
153
|
+
v.kind.inferred === 'unknown'
|
|
154
|
+
? `kind unknown (claimed ${v.kind.claimed})`
|
|
155
|
+
: `kind mismatch: claimed ${v.kind.claimed}, inferred ${v.kind.inferred}`;
|
|
156
|
+
return {
|
|
157
|
+
text: [...bits, anchorBit, why].join(' · '),
|
|
158
|
+
color: '#e5534b',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (v.signature && !v.signature.skipped && !v.signature.match) {
|
|
162
|
+
const cParams = v.signature.claimed.parameterTypes.join(', ') || '∅';
|
|
163
|
+
const iParams = v.signature.inferred.parameterTypes.join(', ') || '∅';
|
|
164
|
+
const cRet = v.signature.claimed.returnTypes.join(', ') || '∅';
|
|
165
|
+
const iRet = v.signature.inferred.returnTypes.join(', ') || '∅';
|
|
166
|
+
return {
|
|
167
|
+
text: [
|
|
168
|
+
...bits,
|
|
169
|
+
anchorBit,
|
|
170
|
+
v.kind?.match ? `kind ${v.kind.inferred}` : undefined,
|
|
171
|
+
`signature mismatch: params [${cParams}]≠[${iParams}] return [${cRet}]≠[${iRet}]`,
|
|
172
|
+
]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(' · '),
|
|
175
|
+
color: '#e5534b',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const kindBit =
|
|
179
|
+
v.kind?.match ? `kind ${v.kind.inferred}` : undefined;
|
|
180
|
+
const inlineBit =
|
|
181
|
+
v.signature?.inlineParameters
|
|
182
|
+
? `${v.signature.inlineParameters} inline param${v.signature.inlineParameters === 1 ? '' : 's'} verified`
|
|
183
|
+
: undefined;
|
|
184
|
+
if (v.signature?.skipped) {
|
|
185
|
+
const skipPhrase: Record<string, string> = {
|
|
186
|
+
no_claimed_types: 'no claimable named types (primitives/inline only)',
|
|
187
|
+
generic_arg_only:
|
|
188
|
+
'return only as generic_arg — wrapper types (Promise/Omit/Map/Array) not read as return edges',
|
|
189
|
+
partially_generic_arg:
|
|
190
|
+
'claimed types resolve only via generic_arg; rest unresolved',
|
|
191
|
+
unresolved_claimed_types:
|
|
192
|
+
'claimed return type has no graphify edge (npm/global/DOM type)',
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
text: [
|
|
196
|
+
...bits,
|
|
197
|
+
anchorBit,
|
|
198
|
+
...(kindBit ? [kindBit] : []),
|
|
199
|
+
...(inlineBit ? [inlineBit] : []),
|
|
200
|
+
v.signature.skipCode
|
|
201
|
+
? `params/return not fully checked — ${skipPhrase[v.signature.skipCode] ?? v.signature.skipCode}`
|
|
202
|
+
: 'params/return not checked',
|
|
203
|
+
].join(' · '),
|
|
204
|
+
color: warn,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const sigBit =
|
|
208
|
+
v.signature && v.signature.match ? 'params/return ok' : undefined;
|
|
209
|
+
return {
|
|
210
|
+
text: [...bits, anchorBit, ...(kindBit ? [kindBit] : []), ...(sigBit ? [sigBit] : [])].join(' · '),
|
|
211
|
+
color: ok,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (res === 'file-only') {
|
|
215
|
+
return {
|
|
216
|
+
text: [...bits, 'file in graph, symbol not uniquely matched'].join(' · '),
|
|
217
|
+
color: warn,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (res === 'ambiguous') {
|
|
221
|
+
const n = v.anchor?.candidates?.length ?? 0;
|
|
222
|
+
return {
|
|
223
|
+
text: [...bits, `ambiguous (${n} candidates)`].join(' · '),
|
|
224
|
+
color: warn,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (res === 'missing') {
|
|
228
|
+
return {
|
|
229
|
+
text: [...bits, 'no matching graphify node'].join(' · '),
|
|
230
|
+
color: '#e5534b',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { text: bits.join(' · ') || 'done', color: muted };
|
|
60
234
|
}
|
|
61
235
|
|
|
62
236
|
/** Declaration panel for the selected component — its definition, code-style. */
|
|
63
|
-
export function ComponentDeclaration({
|
|
237
|
+
export function ComponentDeclaration({
|
|
238
|
+
component,
|
|
239
|
+
onOpenFile,
|
|
240
|
+
onRelatedSelect: _onRelatedSelect,
|
|
241
|
+
maxWidth,
|
|
242
|
+
onVerify,
|
|
243
|
+
verification,
|
|
244
|
+
}: ComponentDeclarationProps) {
|
|
64
245
|
const { theme } = useTheme();
|
|
65
246
|
const [fileHovered, setFileHovered] = useState(false);
|
|
66
|
-
// Header visibility toggles — off by default so the panel opens as pure
|
|
67
|
-
// code; the repo-row icons bring the path and description back.
|
|
68
247
|
const [showFile, setShowFile] = useState(false);
|
|
69
248
|
const [showPurpose, setShowPurpose] = useState(false);
|
|
70
249
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
71
250
|
const color = KIND_COLOR[component.kind] ?? '#888';
|
|
251
|
+
const okColor = '#3d9a5f';
|
|
252
|
+
const warnColor = theme.colors.textSecondary;
|
|
72
253
|
|
|
73
|
-
// Token-kind → theme-color map (used by the token iterator below).
|
|
74
254
|
const tokenColor: Record<SubsystemDeclTokenKind, string> = {
|
|
75
255
|
keyword: theme.colors.secondary,
|
|
76
256
|
name: color,
|
|
@@ -78,7 +258,7 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
78
258
|
type: theme.colors.accent,
|
|
79
259
|
punctuation: muted,
|
|
80
260
|
string: theme.colors.success,
|
|
81
|
-
newline: '',
|
|
261
|
+
newline: '',
|
|
82
262
|
};
|
|
83
263
|
|
|
84
264
|
const line = (children: ReactNode, key?: string, indent?: boolean | number) => {
|
|
@@ -98,9 +278,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
98
278
|
key,
|
|
99
279
|
indent,
|
|
100
280
|
);
|
|
101
|
-
// Purpose is the panel's actual prose, not code — body font, readable
|
|
102
|
-
// size/contrast, capped measure. The `//` metadata lines above it stay
|
|
103
|
-
// muted mono on purpose.
|
|
104
281
|
const purposeStyle = {
|
|
105
282
|
color: theme.colors.text,
|
|
106
283
|
fontFamily: theme.fonts.body,
|
|
@@ -108,17 +285,9 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
108
285
|
lineHeight: 1.5,
|
|
109
286
|
maxWidth: '52ch',
|
|
110
287
|
} as const;
|
|
111
|
-
|
|
112
|
-
// `// implemented by:`, …) are deliberately not rendered — the graph's
|
|
113
|
-
// edges carry interactions, and duplicated name lists read as clutter. The
|
|
114
|
-
// data stays on the Graphify detail types for other surfaces.
|
|
288
|
+
|
|
115
289
|
const lines: ReactNode[] = [];
|
|
116
|
-
// Repo identity — owner avatar + name for GitHub-hosted purls; anything
|
|
117
|
-
// else falls back to the formatted purl as a quiet comment. The avatar
|
|
118
|
-
// hides itself on load failure (offline / blocked hosts).
|
|
119
290
|
const ghMatch = /^pkg:github\/([^/]+)\/([^/#?]+)/.exec(component.purl ?? '');
|
|
120
|
-
// Small ghost icon button for the header toggles — accent when its section
|
|
121
|
-
// is shown, muted when hidden.
|
|
122
291
|
const toggleBtn = (on: boolean, onClick: () => void, title: string, Icon: typeof FileText) => (
|
|
123
292
|
<button
|
|
124
293
|
type="button"
|
|
@@ -142,6 +311,44 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
142
311
|
<Icon size={13} />
|
|
143
312
|
</button>
|
|
144
313
|
);
|
|
314
|
+
const verifyBusy = verification?.phase === 'checking';
|
|
315
|
+
const headerActions = (
|
|
316
|
+
<span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
317
|
+
{onVerify && (
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
title="Verify against graphify"
|
|
321
|
+
aria-label="Verify against graphify"
|
|
322
|
+
disabled={verifyBusy}
|
|
323
|
+
onClick={(e) => {
|
|
324
|
+
e.stopPropagation();
|
|
325
|
+
onVerify(component.id);
|
|
326
|
+
}}
|
|
327
|
+
style={{
|
|
328
|
+
display: 'flex',
|
|
329
|
+
alignItems: 'center',
|
|
330
|
+
gap: 4,
|
|
331
|
+
height: 20,
|
|
332
|
+
padding: '0 6px',
|
|
333
|
+
border: `1px solid ${theme.colors.border}`,
|
|
334
|
+
borderRadius: 4,
|
|
335
|
+
background: 'transparent',
|
|
336
|
+
color: verifyBusy ? muted : theme.colors.textSecondary,
|
|
337
|
+
cursor: verifyBusy ? 'wait' : 'pointer',
|
|
338
|
+
fontSize: theme.fontSizes[0],
|
|
339
|
+
fontFamily: theme.fonts.body,
|
|
340
|
+
opacity: verifyBusy ? 0.7 : 1,
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<ShieldCheck size={12} />
|
|
344
|
+
{verifyBusy ? '…' : 'Verify'}
|
|
345
|
+
</button>
|
|
346
|
+
)}
|
|
347
|
+
{toggleBtn(showFile, () => setShowFile((v) => !v), 'Toggle file path', FileText)}
|
|
348
|
+
{toggleBtn(showPurpose, () => setShowPurpose((v) => !v), 'Toggle description', AlignLeft)}
|
|
349
|
+
</span>
|
|
350
|
+
);
|
|
351
|
+
|
|
145
352
|
if (ghMatch) {
|
|
146
353
|
const [, ghOwner, ghRepo] = ghMatch;
|
|
147
354
|
lines.push(
|
|
@@ -166,17 +373,68 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
166
373
|
>
|
|
167
374
|
{ghRepo}
|
|
168
375
|
</span>
|
|
169
|
-
|
|
170
|
-
{toggleBtn(showFile, () => setShowFile((v) => !v), 'Toggle file path', FileText)}
|
|
171
|
-
{toggleBtn(showPurpose, () => setShowPurpose((v) => !v), 'Toggle description', AlignLeft)}
|
|
172
|
-
</span>
|
|
376
|
+
{headerActions}
|
|
173
377
|
</span>,
|
|
174
378
|
'repo',
|
|
175
379
|
),
|
|
176
380
|
);
|
|
177
381
|
} else if (component.purl) {
|
|
178
|
-
lines.push(
|
|
382
|
+
lines.push(
|
|
383
|
+
line(
|
|
384
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6, width: '100%' }}>
|
|
385
|
+
<span style={commentStyle}>{`// ${formatPurl(component.purl)}`}</span>
|
|
386
|
+
{headerActions}
|
|
387
|
+
</span>,
|
|
388
|
+
'purl',
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
} else if (onVerify) {
|
|
392
|
+
lines.push(line(headerActions, 'actions'));
|
|
179
393
|
}
|
|
394
|
+
|
|
395
|
+
if (verification && verification.phase !== 'idle') {
|
|
396
|
+
const summary = verificationSummary(verification, muted, okColor, warnColor);
|
|
397
|
+
if (summary.text) {
|
|
398
|
+
lines.push(
|
|
399
|
+
line(
|
|
400
|
+
<span
|
|
401
|
+
style={{
|
|
402
|
+
color: summary.color,
|
|
403
|
+
fontFamily: theme.fonts.body,
|
|
404
|
+
fontSize: theme.fontSizes[0],
|
|
405
|
+
whiteSpace: 'normal',
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{summary.text}
|
|
409
|
+
</span>,
|
|
410
|
+
'verify-status',
|
|
411
|
+
),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
verification.anchor?.resolution === 'ambiguous' &&
|
|
416
|
+
verification.anchor.candidates &&
|
|
417
|
+
verification.anchor.candidates.length > 0
|
|
418
|
+
) {
|
|
419
|
+
for (const c of verification.anchor.candidates.slice(0, 5)) {
|
|
420
|
+
lines.push(
|
|
421
|
+
commentLine(
|
|
422
|
+
`// ${c.label}${c.source_file ? ` — ${c.source_file}` : ''}`,
|
|
423
|
+
`cand-${c.nodeId}`,
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (verification.signature?.skipped && verification.signature.reason) {
|
|
429
|
+
lines.push(
|
|
430
|
+
commentLine(
|
|
431
|
+
`// why: ${verification.signature.reason}`,
|
|
432
|
+
'verify-skip-reason',
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
180
438
|
if (showFile && component.file)
|
|
181
439
|
lines.push(
|
|
182
440
|
line(
|
|
@@ -185,8 +443,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
185
443
|
onMouseEnter={() => setFileHovered(true)}
|
|
186
444
|
onMouseLeave={() => setFileHovered(false)}
|
|
187
445
|
style={{
|
|
188
|
-
// Path as UI text, not a code comment — matches the repo-name
|
|
189
|
-
// row. Quiet until hover; the underline only appears then.
|
|
190
446
|
color: theme.colors.textSecondary ?? muted,
|
|
191
447
|
fontFamily: theme.fonts.body,
|
|
192
448
|
fontSize: theme.fontSizes[1],
|
|
@@ -201,38 +457,33 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
201
457
|
),
|
|
202
458
|
);
|
|
203
459
|
|
|
204
|
-
// --- Container width measurement --------------------------------------
|
|
205
|
-
// Approximate character count from the container's clientWidth so Prettier
|
|
206
|
-
// wraps at the same column the code block will actually use.
|
|
207
460
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
208
461
|
const [printWidth, setPrintWidth] = useState(80);
|
|
209
462
|
|
|
210
|
-
|
|
463
|
+
useEffect(() => {
|
|
211
464
|
const el = containerRef.current;
|
|
212
465
|
if (!el) return;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
466
|
+
|
|
467
|
+
function measureWidth() {
|
|
468
|
+
const probe = document.createElement('div');
|
|
469
|
+
probe.style.cssText =
|
|
470
|
+
'position:absolute;visibility:hidden;white-space:pre;' +
|
|
471
|
+
`font-family:${getComputedStyle(el!).fontFamily};` +
|
|
472
|
+
`font-size:${getComputedStyle(el!).fontSize};` +
|
|
473
|
+
'line-height:1;width:auto';
|
|
474
|
+
probe.textContent = 'x';
|
|
475
|
+
document.body.appendChild(probe);
|
|
476
|
+
const ch = probe.getBoundingClientRect().width || 8.4;
|
|
477
|
+
document.body.removeChild(probe);
|
|
478
|
+
setPrintWidth(Math.max(40, Math.floor(el!.clientWidth / ch)));
|
|
221
479
|
}
|
|
222
|
-
}, []);
|
|
223
480
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const el = containerRef.current;
|
|
227
|
-
if (!el) return;
|
|
228
|
-
const ro = new ResizeObserver(measure);
|
|
481
|
+
measureWidth();
|
|
482
|
+
const ro = new ResizeObserver(measureWidth);
|
|
229
483
|
ro.observe(el);
|
|
230
484
|
return () => ro.disconnect();
|
|
231
|
-
}, [
|
|
485
|
+
}, []);
|
|
232
486
|
|
|
233
|
-
// --- Declaration tokens ------------------------------------------------
|
|
234
|
-
// Tokenize asynchronously via Prettier + highlight.js. Pre-tokenized
|
|
235
|
-
// tokens from the wire skip the pipeline entirely.
|
|
236
487
|
const [tokens, setTokens] = useState(component.tokens ?? []);
|
|
237
488
|
useEffect(() => {
|
|
238
489
|
if (component.tokens) {
|
|
@@ -243,7 +494,9 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
243
494
|
tokenizeComponent(component, printWidth).then((t) => {
|
|
244
495
|
if (!cancelled) setTokens(t);
|
|
245
496
|
});
|
|
246
|
-
return () => {
|
|
497
|
+
return () => {
|
|
498
|
+
cancelled = true;
|
|
499
|
+
};
|
|
247
500
|
}, [component, printWidth]);
|
|
248
501
|
const declLines: ReactNode[][] = [[]];
|
|
249
502
|
let di = 0;
|
|
@@ -262,18 +515,15 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
262
515
|
for (let li = 0; li < declLines.length; li++) {
|
|
263
516
|
if (declLines[li].length === 0) continue;
|
|
264
517
|
lines.push(
|
|
265
|
-
<div key={`decl-${li}`}
|
|
518
|
+
<div key={`decl-${li}`} style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre', minHeight: 18 }}>
|
|
266
519
|
{declLines[li]}
|
|
267
520
|
</div>,
|
|
268
521
|
);
|
|
269
522
|
}
|
|
270
523
|
|
|
271
|
-
// Purpose prose follows the declaration — identity and syntax first, the
|
|
272
|
-
// explanation underneath. Newlines split into separate prose lines so a
|
|
273
|
-
// two-thought purpose reads as a short paragraph instead of one blob.
|
|
274
524
|
if (showPurpose && component.purpose) {
|
|
275
525
|
lines.push(line(<div style={{ height: 6 }} />, 'purpose-gap'));
|
|
276
|
-
component.purpose.split(
|
|
526
|
+
component.purpose.split('\n').forEach((purposeLine, i) =>
|
|
277
527
|
lines.push(
|
|
278
528
|
line(
|
|
279
529
|
<span style={{ ...purposeStyle, display: 'block' }} key={`purpose${i}`}>
|
|
@@ -45,6 +45,7 @@ import { SubsystemComponentNode, SubsystemEdge, SUBSYSTEM_CALLBACKS } from './no
|
|
|
45
45
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
46
46
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
47
47
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
48
|
+
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
48
49
|
import { FileDrawer } from './FileDrawer';
|
|
49
50
|
import { EdgeLegendModal, MECHANISM_DESCRIPTIONS } from './EdgeLegendModal';
|
|
50
51
|
import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
|
|
@@ -88,6 +89,10 @@ export interface SubsystemComponentGraphProps {
|
|
|
88
89
|
* path). The tree is derived from the components' `file` values.
|
|
89
90
|
*/
|
|
90
91
|
onFileSelect?: (file: string) => void;
|
|
92
|
+
/** Verify the selected component against graphify (declaration panel). */
|
|
93
|
+
onVerifyComponent?: (componentId: string) => void;
|
|
94
|
+
/** Live verification status for the selected component. */
|
|
95
|
+
componentVerification?: ComponentVerificationState | null;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
const nodeTypes: NodeTypes = {
|
|
@@ -117,7 +122,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
117
122
|
measured: { w: number; h: number } | null;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect }: InnerProps) {
|
|
125
|
+
function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
121
126
|
const { theme } = useTheme();
|
|
122
127
|
const { fitView } = useReactFlow();
|
|
123
128
|
const viewport = useViewport();
|
|
@@ -699,6 +704,8 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
699
704
|
component={selected}
|
|
700
705
|
onOpenFile={onTreeSelectFile}
|
|
701
706
|
onRelatedSelect={resolveRelatedComponent}
|
|
707
|
+
onVerify={onVerifyComponent}
|
|
708
|
+
verification={componentVerification}
|
|
702
709
|
/>
|
|
703
710
|
</div>
|
|
704
711
|
)}
|
|
@@ -13,13 +13,17 @@ import type { GraphifyComponentDetail } from '../graphify';
|
|
|
13
13
|
export function generateDeclarationString(component: SubsystemComponent): string {
|
|
14
14
|
const detail = component.detail;
|
|
15
15
|
const kind = detail?.kind ?? component.kind;
|
|
16
|
-
const
|
|
16
|
+
const rawName = component.symbol || component.name || 'untitled';
|
|
17
|
+
// Strip class/object prefix from dotted symbols (e.g. "SessionReader.normalize" → "normalize").
|
|
18
|
+
const name = rawName.includes('.') ? rawName.split('.').pop()! : rawName;
|
|
17
19
|
|
|
18
20
|
switch (kind) {
|
|
19
21
|
case 'class':
|
|
20
22
|
return generateClass(name, detail);
|
|
21
23
|
case 'function':
|
|
22
24
|
return generateFunction(name, detail);
|
|
25
|
+
case 'method':
|
|
26
|
+
return generateMethod(name, detail);
|
|
23
27
|
case 'type':
|
|
24
28
|
return generateType(name, detail);
|
|
25
29
|
case 'module':
|
|
@@ -86,6 +90,14 @@ function generateFunction(name: string, detail?: GraphifyComponentDetail): strin
|
|
|
86
90
|
return `function ${name}(${params})${ret};`;
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
function generateMethod(name: string, detail?: GraphifyComponentDetail): string {
|
|
94
|
+
const m = detail?.kind === 'method' ? detail : undefined;
|
|
95
|
+
const hostClass = m?.hostClass ?? 'Host';
|
|
96
|
+
const params = formatParams(m?.parameters ?? []);
|
|
97
|
+
const ret = m?.returnType ? `: ${m.returnType}` : '';
|
|
98
|
+
return `class ${hostClass} {\n ${name}(${params})${ret};\n}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
function generateType(name: string, detail?: GraphifyComponentDetail): string {
|
|
90
102
|
const tpe = detail?.kind === 'type' ? detail : undefined;
|
|
91
103
|
const props = (tpe?.properties ?? [])
|
package/src/subsystem/model.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { GraphifyComponentDetail } from '../graphify';
|
|
|
23
23
|
export type SubsystemComponentKind =
|
|
24
24
|
| 'class'
|
|
25
25
|
| 'function'
|
|
26
|
+
| 'method'
|
|
26
27
|
| 'type'
|
|
27
28
|
| 'module'
|
|
28
29
|
| 'external';
|
|
@@ -269,6 +270,7 @@ export function packageColor(name: string): string {
|
|
|
269
270
|
export const KIND_COLOR: Record<SubsystemComponentKind, string> = {
|
|
270
271
|
class: '#0893d2', // blue
|
|
271
272
|
function: '#6c5ce7', // indigo
|
|
273
|
+
method: '#d4a03c', // gold
|
|
272
274
|
type: '#e07a5f', // terracotta
|
|
273
275
|
module: '#4ec9b0', // teal
|
|
274
276
|
external: '#b48ead', // purple
|