@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.
Files changed (57) hide show
  1. package/dist/graphify/anchor.d.ts +28 -0
  2. package/dist/graphify/anchor.d.ts.map +1 -0
  3. package/dist/graphify/anchor.js +132 -0
  4. package/dist/graphify/anchor.js.map +1 -0
  5. package/dist/graphify/consolidated.d.ts +12 -1
  6. package/dist/graphify/consolidated.d.ts.map +1 -1
  7. package/dist/graphify/ids.d.ts +19 -0
  8. package/dist/graphify/ids.d.ts.map +1 -0
  9. package/dist/graphify/ids.js +46 -0
  10. package/dist/graphify/ids.js.map +1 -0
  11. package/dist/graphify/index.d.ts +7 -0
  12. package/dist/graphify/index.d.ts.map +1 -1
  13. package/dist/graphify/index.js +4 -0
  14. package/dist/graphify/index.js.map +1 -1
  15. package/dist/graphify/kind.d.ts +23 -0
  16. package/dist/graphify/kind.d.ts.map +1 -0
  17. package/dist/graphify/kind.js +71 -0
  18. package/dist/graphify/kind.js.map +1 -0
  19. package/dist/graphify/signature.d.ts +87 -0
  20. package/dist/graphify/signature.d.ts.map +1 -0
  21. package/dist/graphify/signature.js +276 -0
  22. package/dist/graphify/signature.js.map +1 -0
  23. package/dist/index.d.ts +4 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/subsystem/ComponentDeclaration.d.ts +58 -3
  28. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  29. package/dist/subsystem/ComponentDeclaration.js +186 -53
  30. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  31. package/dist/subsystem/SubsystemComponentGraph.d.ts +5 -0
  32. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  33. package/dist/subsystem/SubsystemComponentGraph.js +2 -2
  34. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  35. package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
  36. package/dist/subsystem/formatDeclaration.js +12 -1
  37. package/dist/subsystem/formatDeclaration.js.map +1 -1
  38. package/dist/subsystem/model.d.ts +1 -1
  39. package/dist/subsystem/model.d.ts.map +1 -1
  40. package/dist/subsystem/model.js +1 -0
  41. package/dist/subsystem/model.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/graphify/anchor.test.ts +124 -0
  44. package/src/graphify/anchor.ts +160 -0
  45. package/src/graphify/consolidated.ts +13 -0
  46. package/src/graphify/ids.ts +48 -0
  47. package/src/graphify/index.ts +26 -0
  48. package/src/graphify/kind.test.ts +108 -0
  49. package/src/graphify/kind.ts +114 -0
  50. package/src/graphify/signature.test.ts +278 -0
  51. package/src/graphify/signature.ts +354 -0
  52. package/src/index.ts +26 -0
  53. package/src/stories/ComponentDeclarationAudit.stories.tsx +32 -0
  54. package/src/subsystem/ComponentDeclaration.tsx +306 -56
  55. package/src/subsystem/SubsystemComponentGraph.tsx +8 -1
  56. package/src/subsystem/formatDeclaration.ts +13 -1
  57. 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 { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
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. The two callbacks are wired by the graph; standalone usage
50
- * without them renders the same panel with nothing clickable. */
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({ component, onOpenFile, onRelatedSelect: _onRelatedSelect, maxWidth }: ComponentDeclarationProps) {
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: '', // not rendered — drives line breaks
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
- // Trailing relationship comments (`// called by:`, `// constructed by:`,
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
- <span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 2 }}>
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(commentLine(`// ${formatPurl(component.purl)}`, 'purl'));
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
- const measure = useCallback(() => {
463
+ useEffect(() => {
211
464
  const el = containerRef.current;
212
465
  if (!el) return;
213
- const ch = el.querySelector('[data-declaration-line]');
214
- if (ch) {
215
- // Use the width of a single `ch` unit from the monospace container.
216
- const charWidth = ch.getBoundingClientRect().width || 8.4;
217
- setPrintWidth(Math.max(40, Math.floor(el.clientWidth / charWidth)));
218
- } else {
219
- // Fallback: assume ~8.4 px per char at 14px monospace.
220
- setPrintWidth(Math.max(40, Math.floor(el.clientWidth / 8.4)));
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
- useEffect(() => {
225
- measure();
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
- }, [measure]);
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 () => { cancelled = true; };
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}`} data-declaration-line style={{ display: 'flex', alignItems: 'center', whiteSpace: 'pre', minHeight: 18 }}>
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("\n").forEach((purposeLine, i) =>
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 name = component.symbol || component.name || 'untitled';
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 ?? [])
@@ -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