@principal-ai/principal-view-react 0.16.35 → 0.16.37

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 (42) hide show
  1. package/dist/graphify/consolidated.d.ts +17 -3
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/index.d.ts +2 -0
  4. package/dist/graphify/index.d.ts.map +1 -1
  5. package/dist/graphify/index.js +1 -1
  6. package/dist/graphify/index.js.map +1 -1
  7. package/dist/graphify/resolve.d.ts +48 -0
  8. package/dist/graphify/resolve.d.ts.map +1 -0
  9. package/dist/graphify/resolve.js +83 -0
  10. package/dist/graphify/resolve.js.map +1 -0
  11. package/dist/index.d.ts +5 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/subsystem/ComponentDetail.d.ts +16 -7
  16. package/dist/subsystem/ComponentDetail.d.ts.map +1 -1
  17. package/dist/subsystem/ComponentDetail.js +124 -82
  18. package/dist/subsystem/ComponentDetail.js.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  20. package/dist/subsystem/SubsystemComponentGraph.js +47 -10
  21. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  22. package/dist/subsystem/model.d.ts +14 -0
  23. package/dist/subsystem/model.d.ts.map +1 -1
  24. package/dist/subsystem/model.js +26 -1
  25. package/dist/subsystem/model.js.map +1 -1
  26. package/dist/subsystem/paths.d.ts +38 -0
  27. package/dist/subsystem/paths.d.ts.map +1 -0
  28. package/dist/subsystem/paths.js +52 -0
  29. package/dist/subsystem/paths.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/graphify/consolidated.ts +17 -3
  32. package/src/graphify/index.ts +9 -0
  33. package/src/graphify/resolve.test.ts +89 -0
  34. package/src/graphify/resolve.ts +115 -0
  35. package/src/index.ts +19 -0
  36. package/src/stories/SubsystemComponentGraph.stories.tsx +331 -11
  37. package/src/subsystem/ComponentDetail.tsx +355 -156
  38. package/src/subsystem/SubsystemComponentGraph.tsx +76 -15
  39. package/src/subsystem/model.test.ts +11 -0
  40. package/src/subsystem/model.ts +25 -1
  41. package/src/subsystem/paths.test.ts +62 -0
  42. package/src/subsystem/paths.ts +69 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Resolve graphify type-name references to their defining nodes.
3
+ *
4
+ * A `references` edge (`parameter_type`, `return_type`, `field`,
5
+ * `generic_arg`) targets either the type's real definition node or a
6
+ * *sourceless stub* graphify left behind when the name could not be uniquely
7
+ * resolved at extraction time (mirrors `_rewire_unique_stub_nodes` there:
8
+ * ambiguous same-label definitions and never-defined names stay stubs).
9
+ *
10
+ * This module applies the same conservative matching from the consumer side so
11
+ * UI click targets can distinguish "jump to definition" from "N candidates"
12
+ * from "not in this corpus".
13
+ */
14
+
15
+ import type { GraphifyNode } from './types';
16
+ import type { GraphifyReferenceInfo } from './consolidated';
17
+
18
+ /** Outcome of resolving one type reference. */
19
+ export type GraphifyTypeRefStatus =
20
+ /** Target node is a real definition — jump to its `source_file`. */
21
+ | 'resolved'
22
+ /** Target was a stub and multiple same-label definitions exist. */
23
+ | 'ambiguous'
24
+ /** Target was a stub and nothing in the corpus defines the name. */
25
+ | 'unresolved'
26
+ /** No nodeId on the ref, or the id is not in the graph. */
27
+ | 'missing';
28
+
29
+ export interface GraphifyTypeRefResolution {
30
+ status: GraphifyTypeRefStatus;
31
+ /** The definition node when resolved; the stub itself otherwise. */
32
+ node: GraphifyNode | null;
33
+ /** Same-label definitions when the target was an unresolved stub. */
34
+ candidates: GraphifyNode[];
35
+ }
36
+
37
+ /**
38
+ * Mirror graphify's `normalise_callable_label`: trim, strip surrounding
39
+ * parens/leading dots, lowercase. `SessionReader()` and `sessionreader`
40
+ * collapse to the same key.
41
+ */
42
+ export function normalizeGraphifyLabel(label: string): string {
43
+ return label
44
+ .trim()
45
+ .replace(/^[()]+|[()]+$/g, '')
46
+ .replace(/^\.+/, '')
47
+ .toLowerCase();
48
+ }
49
+
50
+ const FILE_SUFFIX_RE =
51
+ /\.(py|js|jsx|ts|tsx|mjs|cjs|java|go|rs|rb|php|cs|cpp|cc|c|h|hpp|pas|pp|dpr|swift|kt|scala|dart|lua|pl|pm|ex|exs|zig|vue|svelte)$/i;
52
+
53
+ function isDefinition(node: GraphifyNode): boolean {
54
+ if (node.file_type !== 'code') return false;
55
+ const file = typeof node.source_file === 'string' ? node.source_file : '';
56
+ if (!file) return false;
57
+ const label = typeof node.label === 'string' ? node.label.trim() : '';
58
+ return !!label && !FILE_SUFFIX_RE.test(label);
59
+ }
60
+
61
+ /** Prebuilt lookup state — build once per graph, resolve many refs. */
62
+ export interface GraphifyTypeResolver {
63
+ resolve(nodeId: string | undefined, fallbackLabel?: string): GraphifyTypeRefResolution;
64
+ }
65
+
66
+ export function createGraphifyTypeResolver(nodes: readonly GraphifyNode[]): GraphifyTypeResolver {
67
+ const byId = new Map<string, GraphifyNode>();
68
+ const defsByExactLabel = new Map<string, GraphifyNode[]>();
69
+ const defsByFoldedLabel = new Map<string, GraphifyNode[]>();
70
+
71
+ for (const node of nodes) {
72
+ byId.set(String(node.id), node);
73
+ if (!isDefinition(node)) continue;
74
+ const label = String(node.label).trim();
75
+ (defsByExactLabel.get(label) ?? defsByExactLabel.set(label, []).get(label)!).push(node);
76
+ const folded = normalizeGraphifyLabel(label);
77
+ if (!folded) continue;
78
+ (defsByFoldedLabel.get(folded) ?? defsByFoldedLabel.set(folded, []).get(folded)!).push(node);
79
+ }
80
+
81
+ function resolve(nodeId: string | undefined, fallbackLabel?: string): GraphifyTypeRefResolution {
82
+ if (!nodeId) return { status: 'missing', node: null, candidates: [] };
83
+ const target = byId.get(nodeId);
84
+ if (!target) return { status: 'missing', node: null, candidates: [] };
85
+ // Real definition (graphify rewired the reference onto it, or it lived in
86
+ // the referencing file all along).
87
+ if (isDefinition(target)) return { status: 'resolved', node: target, candidates: [] };
88
+
89
+ // Sourceless stub — try the rewire tiers: exact label, then folded.
90
+ const label = typeof target.label === 'string' && target.label.trim() ? target.label.trim() : (fallbackLabel ?? '');
91
+ let candidates = defsByExactLabel.get(label) ?? [];
92
+ if (candidates.length !== 1) {
93
+ const folded = normalizeGraphifyLabel(label);
94
+ candidates = folded ? (defsByFoldedLabel.get(folded) ?? []) : [];
95
+ }
96
+ if (candidates.length === 1) {
97
+ // Unique match — same fold `_rewire_unique_stub_nodes` performs at
98
+ // extraction time; graphs built before that pass land here instead.
99
+ return { status: 'resolved', node: candidates[0]!, candidates };
100
+ }
101
+ if (candidates.length > 1) return { status: 'ambiguous', node: target, candidates };
102
+ return { status: 'unresolved', node: target, candidates: [] };
103
+ }
104
+
105
+ return { resolve };
106
+ }
107
+
108
+ /** One-shot convenience over {@link createGraphifyTypeResolver}. */
109
+ export function resolveGraphifyTypeRef(
110
+ nodes: readonly GraphifyNode[],
111
+ ref: Pick<GraphifyReferenceInfo, 'nodeId'> | { nodeId?: string } | undefined,
112
+ fallbackLabel?: string,
113
+ ): GraphifyTypeRefResolution {
114
+ return createGraphifyTypeResolver(nodes).resolve(ref?.nodeId, fallbackLabel);
115
+ }
package/src/index.ts CHANGED
@@ -242,10 +242,29 @@ export type {
242
242
  GraphifyComponentDetail,
243
243
  GraphifyEdgeRef,
244
244
  } from './graphify';
245
+ export type {
246
+ GraphifyTypeRefStatus,
247
+ GraphifyTypeRefResolution,
248
+ } from './graphify';
249
+ export {
250
+ normalizeGraphifyLabel,
251
+ createGraphifyTypeResolver,
252
+ resolveGraphifyTypeRef,
253
+ } from './graphify';
245
254
 
246
255
  // Subsystem component graph
247
256
  export { SubsystemComponentGraph } from './subsystem/SubsystemComponentGraph';
248
257
  export type { SubsystemComponentGraphProps } from './subsystem/SubsystemComponentGraph';
258
+ export { MECHANISM_COLOR, KIND_COLOR, MECHANISM_STYLE } from './subsystem/model';
259
+ export {
260
+ purlRepoKey,
261
+ purlOwnerName,
262
+ buildTreePathMapping,
263
+ } from './subsystem/paths';
264
+ export type {
265
+ TreeEntry,
266
+ TreePathMapping,
267
+ } from './subsystem/paths';
249
268
  export { SubsystemFileTree } from './subsystem/SubsystemFileTree';
250
269
  export type { SubsystemFileTreeProps } from './subsystem/SubsystemFileTree';
251
270
  export { GraphLayoutCover } from './subsystem/GraphLayoutCover';
@@ -77,7 +77,6 @@ function TwoNodeDemo({ showEdgeLabels = true }: { showEdgeLabels?: boolean }) {
77
77
  onSelect={(id) => setSelected(id)}
78
78
  onEdgeSelect={(e) => setSelectedEdge(e)}
79
79
  showEdgeLabels={showEdgeLabels}
80
-
81
80
  />
82
81
  <div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
83
82
  {selectedEdge
@@ -162,7 +161,7 @@ const readerDetail: GraphifyComponentDetail = {
162
161
  kind: 'class',
163
162
  methods: [
164
163
  { nodeId: 'm1', name: 'normalize', returnType: 'SessionEvent[]' },
165
- { nodeId: 'm2', name: 'readSession', parameters: ['string'], returnType: 'SessionRecord' },
164
+ { nodeId: 'm2', name: 'readSession', parameters: [{ type: 'string' }], returnType: 'SessionRecord' },
166
165
  { nodeId: 'm3', name: 'toUniversalEvents', returnType: 'UniversalEvent[]' },
167
166
  ],
168
167
  properties: [{ name: 'sessionId', type: 'string' }],
@@ -201,7 +200,6 @@ function V2ReaderDemo() {
201
200
  edges={v2ReaderEdges}
202
201
  onSelect={(id) => setSelected(id)}
203
202
  onEdgeSelect={(e) => setSelectedEdge(e)}
204
-
205
203
  />
206
204
  <div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
207
205
  {selectedEdge
@@ -325,7 +323,6 @@ export const NarrowMaxWidth: Story = {
325
323
  components={investigateOnlyComponents}
326
324
  edges={investigateOnlyEdges}
327
325
  maxNodeWidth={140}
328
-
329
326
  />
330
327
  </div>
331
328
  ),
@@ -385,6 +382,8 @@ const namingConventionComponents: SubsystemComponent[] = [
385
382
  },
386
383
  ];
387
384
 
385
+ const namingConventionEdges: SubsystemComponentEdge[] = [];
386
+
388
387
  /** Nodes with compact `maxNodeWidth` so the different conventions visibly wrap
389
388
  * at their word boundaries (camelCase, snake_case, PascalCase, acronyms). */
390
389
  export const NamingConventions: Story = {
@@ -392,9 +391,8 @@ export const NamingConventions: Story = {
392
391
  <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
393
392
  <SubsystemComponentGraph
394
393
  components={namingConventionComponents}
395
- edges={[]}
394
+ edges={namingConventionEdges}
396
395
  maxNodeWidth={180}
397
-
398
396
  />
399
397
  </div>
400
398
  ),
@@ -449,6 +447,75 @@ const detailKindComponents: SubsystemComponent[] = [
449
447
  callees: [{ nodeId: 'c2', name: 'toUniversalEvents', source_location: 'L64' }],
450
448
  } satisfies GraphifyComponentDetail,
451
449
  },
450
+ {
451
+ id: 'detail-fn-rich',
452
+ name: 'mergeSessions',
453
+ kind: 'function',
454
+ file: 'src/session/merge.ts',
455
+ purl: 'pkg:github/principal-ai/agent-monitoring',
456
+ purpose: 'multi-param signature — named, positional (graphify captures types only), union types, generic return',
457
+ symbol: 'mergeSessions',
458
+ detail: {
459
+ kind: 'function',
460
+ parameters: [
461
+ { name: 'sessions', type: 'SessionRecord[]' },
462
+ { type: 'MergeOptions' },
463
+ { name: 'strategy', type: `'append' | 'replace' | 'skip'` },
464
+ { name: 'onConflict', type: '((a: SessionRecord, b: SessionRecord) => SessionRecord)' },
465
+ ],
466
+ returnType: 'Promise<Map<string, SessionEvent[]>>',
467
+ callers: [
468
+ { nodeId: 'c1', name: 'capture-session', source_location: 'L88' },
469
+ { nodeId: 'c3', name: 'backfill', source_location: 'L210' },
470
+ ],
471
+ callees: [{ nodeId: 'c2', name: 'toUniversalEvents', source_location: 'L64' }],
472
+ } satisfies GraphifyComponentDetail,
473
+ },
474
+ {
475
+ id: 'detail-fn-void',
476
+ name: 'flush',
477
+ kind: 'function',
478
+ file: 'src/event-processing/sink.ts',
479
+ purl: 'pkg:github/principal-ai/agent-monitoring',
480
+ purpose: 'no parameters, no return type — the barest function signature',
481
+ symbol: 'flush',
482
+ detail: {
483
+ kind: 'function',
484
+ parameters: [],
485
+ callers: [{ nodeId: 'c4', name: 'EventProcessor.dispose', source_location: 'L142' }],
486
+ callees: [],
487
+ } satisfies GraphifyComponentDetail,
488
+ },
489
+ {
490
+ id: 'detail-class-rich',
491
+ name: 'EventProcessor',
492
+ kind: 'class',
493
+ file: 'src/event-processing/EventProcessor.ts',
494
+ purl: 'pkg:github/principal-ai/agent-monitoring',
495
+ purpose: 'class with fully-typed members — method params + returns, field types, extends + implements',
496
+ symbol: 'EventProcessor',
497
+ detail: {
498
+ kind: 'class',
499
+ methods: [
500
+ { nodeId: 'rm1', name: 'process', parameters: [{ type: 'RawEvent' }, { type: 'ProcessingOptions' }], returnType: 'ProcessedEvent' },
501
+ { nodeId: 'rm2', name: 'batch', parameters: [{ type: 'RawEvent[]' }], returnType: 'Promise<ProcessedEvent[]>' },
502
+ { nodeId: 'rm3', name: 'onError', parameters: [{ type: 'Error' }] },
503
+ { nodeId: 'rm4', name: 'dispose' },
504
+ ],
505
+ properties: [
506
+ { name: 'queue', type: 'RawEvent[]' },
507
+ { name: 'options', type: 'Required<ProcessingOptions>' },
508
+ { name: 'retryLimit', type: 'number' },
509
+ ],
510
+ extends: ['BaseProcessor'],
511
+ implements: ['Disposable', 'EventEmitterLike'],
512
+ instantiations: [
513
+ { nodeId: 'x1', name: 'main' },
514
+ { nodeId: 'x2', name: 'worker-pool' },
515
+ ],
516
+ references: [{ nodeId: 'x3', name: 'pipeline', context: 'type' }],
517
+ } satisfies GraphifyComponentDetail,
518
+ },
452
519
  {
453
520
  id: 'detail-type',
454
521
  name: 'SessionRecord',
@@ -501,15 +568,16 @@ const detailKindComponents: SubsystemComponent[] = [
501
568
  },
502
569
  ];
503
570
 
571
+ const detailKindEdges: SubsystemComponentEdge[] = [];
572
+
504
573
  function DetailKindsDemo() {
505
574
  const [selected, setSelected] = useState<string | null>(null);
506
575
  return (
507
576
  <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
508
577
  <SubsystemComponentGraph
509
578
  components={detailKindComponents}
510
- edges={[]}
579
+ edges={detailKindEdges}
511
580
  onSelect={(id) => setSelected(id)}
512
-
513
581
  />
514
582
  <div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
515
583
  {selected ? `selected: ${selected}` : 'click a component to see its GraphifyComponentDetail'}
@@ -522,6 +590,261 @@ export const DetailKinds: Story = {
522
590
  render: () => <DetailKindsDemo />,
523
591
  };
524
592
 
593
+ // ---------------------------------------------------------------------------
594
+ // Kind variations — one column per kind (class / function / type / module /
595
+ // external), rows going bare → detailed. Click a node to see its declaration
596
+ // panel; type names inside it are clickable when they match another node here.
597
+ // ---------------------------------------------------------------------------
598
+ const variationPurl = 'pkg:github/principal-ai/agent-monitoring';
599
+
600
+ const kindVariationComponents: SubsystemComponent[] = [
601
+ // --- class: bare → members only → relationships only
602
+ {
603
+ id: 'cls-bare',
604
+ name: 'SessionStore',
605
+ kind: 'class',
606
+ file: 'src/session/SessionStore.ts',
607
+ purl: variationPurl,
608
+ purpose: 'no drill-down — renders a plain `class SessionStore`',
609
+ symbol: 'SessionStore',
610
+ layer: 1,
611
+ },
612
+ {
613
+ id: 'cls-members',
614
+ name: 'Transcoder',
615
+ kind: 'class',
616
+ file: 'src/session/Transcoder.ts',
617
+ purl: variationPurl,
618
+ purpose: 'members only — methods with typed params + returns, a typed field',
619
+ symbol: 'Transcoder',
620
+ layer: 1,
621
+ detail: {
622
+ kind: 'class',
623
+ methods: [
624
+ { nodeId: 'tm1', name: 'encode', parameters: [{ type: 'RawFrame' }], returnType: 'Uint8Array' },
625
+ { nodeId: 'tm2', name: 'decode', parameters: [{ type: 'Uint8Array' }], returnType: 'RawFrame' },
626
+ { nodeId: 'tm3', name: 'reset' },
627
+ ],
628
+ properties: [{ name: 'bufferSize', type: 'number' }],
629
+ extends: [],
630
+ implements: [],
631
+ instantiations: [],
632
+ references: [],
633
+ } satisfies GraphifyComponentDetail,
634
+ },
635
+ {
636
+ id: 'cls-relations',
637
+ name: 'HttpTransport',
638
+ kind: 'class',
639
+ file: 'src/transport/HttpTransport.ts',
640
+ purl: variationPurl,
641
+ purpose: 'relationships only — extends + implements, constructed-by comment; empty body elides the braces',
642
+ symbol: 'HttpTransport',
643
+ layer: 1,
644
+ detail: {
645
+ kind: 'class',
646
+ methods: [],
647
+ properties: [],
648
+ extends: ['BaseTransport'],
649
+ implements: ['Transport'],
650
+ instantiations: [{ nodeId: 'x1', name: 'main' }],
651
+ references: [],
652
+ } satisfies GraphifyComponentDetail,
653
+ },
654
+
655
+ // --- function: bare → signature only → signature + call relationships
656
+ {
657
+ id: 'fn-bare',
658
+ name: 'bootstrap',
659
+ kind: 'function',
660
+ file: 'src/bootstrap.ts',
661
+ purl: variationPurl,
662
+ purpose: 'no drill-down — renders a plain `function bootstrap()`',
663
+ symbol: 'bootstrap',
664
+ layer: 2,
665
+ },
666
+ {
667
+ id: 'fn-signature',
668
+ name: 'normalizeSession',
669
+ kind: 'function',
670
+ file: 'src/event-processing/normalize.ts',
671
+ purl: variationPurl,
672
+ purpose: 'signature only — named + positional (type-only) params, array return; no callers/callees comments',
673
+ symbol: 'normalizeSession',
674
+ layer: 2,
675
+ detail: {
676
+ kind: 'function',
677
+ parameters: [
678
+ { name: 'session', type: 'SessionRecord' },
679
+ { type: 'NormalizeOptions' },
680
+ ],
681
+ returnType: 'SessionEvent[]',
682
+ callers: [],
683
+ callees: [],
684
+ } satisfies GraphifyComponentDetail,
685
+ },
686
+ {
687
+ id: 'fn-calls',
688
+ name: 'mergeSessions',
689
+ kind: 'function',
690
+ file: 'src/session/merge.ts',
691
+ purl: variationPurl,
692
+ purpose: 'signature + call relationships — union-typed param, generic return, called-by / calls trailing comments',
693
+ symbol: 'mergeSessions',
694
+ layer: 2,
695
+ detail: {
696
+ kind: 'function',
697
+ parameters: [
698
+ { name: 'sessions', type: 'SessionRecord[]' },
699
+ { name: 'strategy', type: `'append' | 'replace'` },
700
+ ],
701
+ returnType: 'Promise<SessionEvent[]>',
702
+ callers: [{ nodeId: 'c1', name: 'capture-session', source_location: 'L88' }],
703
+ callees: [{ nodeId: 'c2', name: 'toUniversalEvents', source_location: 'L64' }],
704
+ } satisfies GraphifyComponentDetail,
705
+ },
706
+
707
+ // --- type: bare → fields only → fields + implementors + used-by
708
+ {
709
+ id: 'ty-bare',
710
+ name: 'RawFrame',
711
+ kind: 'type',
712
+ file: 'src/session/RawFrame.ts',
713
+ purl: variationPurl,
714
+ purpose: 'no drill-down — renders a plain `interface RawFrame`',
715
+ symbol: 'RawFrame',
716
+ layer: 3,
717
+ },
718
+ {
719
+ id: 'ty-fields',
720
+ name: 'SessionRecord',
721
+ kind: 'type',
722
+ file: 'src/session/transcript.ts',
723
+ purl: variationPurl,
724
+ purpose: 'fields only — typed interface body, nothing else',
725
+ symbol: 'SessionRecord',
726
+ layer: 3,
727
+ detail: {
728
+ kind: 'type',
729
+ properties: [
730
+ { name: 'id', type: 'string' },
731
+ { name: 'admittedSeq', type: 'number' },
732
+ ],
733
+ usedBy: [],
734
+ implementors: [],
735
+ } satisfies GraphifyComponentDetail,
736
+ },
737
+ {
738
+ id: 'ty-full',
739
+ name: 'Transport',
740
+ kind: 'type',
741
+ file: 'src/transport/Transport.ts',
742
+ purl: variationPurl,
743
+ purpose: 'full — fields + implemented-by (clicks through to HttpTransport) + used-by comments',
744
+ symbol: 'Transport',
745
+ layer: 3,
746
+ detail: {
747
+ kind: 'type',
748
+ properties: [{ name: 'name', type: 'string' }],
749
+ usedBy: [{ nodeId: 'u1', name: 'main', context: 'parameter_type' }],
750
+ implementors: ['HttpTransport'],
751
+ } satisfies GraphifyComponentDetail,
752
+ },
753
+
754
+ // --- module: bare → exports only → exports + imports
755
+ {
756
+ id: 'mod-bare',
757
+ name: 'transcript',
758
+ kind: 'module',
759
+ file: 'src/session/transcript.ts',
760
+ purl: variationPurl,
761
+ purpose: 'no drill-down — symbol-less module named from its file basename',
762
+ symbol: '',
763
+ layer: 4,
764
+ },
765
+ {
766
+ id: 'mod-exports',
767
+ name: 'paths',
768
+ kind: 'module',
769
+ file: 'src/session/paths.ts',
770
+ purl: variationPurl,
771
+ purpose: 'exports only — export statement + defines comment',
772
+ symbol: '',
773
+ layer: 4,
774
+ detail: {
775
+ kind: 'module',
776
+ exports: ['extractToolName', 'extractFilePath'],
777
+ imports: [],
778
+ symbols: ['extractToolName', 'extractFilePath'],
779
+ } satisfies GraphifyComponentDetail,
780
+ },
781
+ {
782
+ id: 'mod-full',
783
+ name: 'index',
784
+ kind: 'module',
785
+ file: 'src/index.ts',
786
+ purl: variationPurl,
787
+ purpose: 'full — import statements + re-export statement + defines comment',
788
+ symbol: '',
789
+ layer: 4,
790
+ detail: {
791
+ kind: 'module',
792
+ exports: ['SessionReader', 'SessionStore'],
793
+ imports: [{ nodeId: 'i1', name: 'transcript', relation: 'imports_from' }],
794
+ symbols: ['SessionReader', 'SessionStore'],
795
+ } satisfies GraphifyComponentDetail,
796
+ },
797
+
798
+ // --- external: bare → labeled
799
+ {
800
+ id: 'ext-bare',
801
+ name: 'left-pad',
802
+ kind: 'external',
803
+ file: '',
804
+ purl: 'pkg:npm/left-pad',
805
+ purpose: 'no drill-down — renders a plain `external left-pad`',
806
+ symbol: '',
807
+ layer: 5,
808
+ },
809
+ {
810
+ id: 'ext-label',
811
+ name: 'trail-viewer',
812
+ kind: 'external',
813
+ file: '',
814
+ purl: 'pkg:npm/@principal-ai/trail-viewer',
815
+ purpose: 'labeled — the full purl as a quoted string literal',
816
+ symbol: '',
817
+ layer: 5,
818
+ detail: {
819
+ kind: 'external',
820
+ label: 'pkg:npm/@principal-ai/trail-viewer',
821
+ } satisfies GraphifyComponentDetail,
822
+ },
823
+ ];
824
+
825
+ const kindVariationEdges: SubsystemComponentEdge[] = [];
826
+
827
+ function KindVariationsDemo() {
828
+ const [selected, setSelected] = useState<string | null>(null);
829
+ return (
830
+ <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
831
+ <SubsystemComponentGraph
832
+ components={kindVariationComponents}
833
+ edges={kindVariationEdges}
834
+ onSelect={(id) => setSelected(id)}
835
+ />
836
+ <div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
837
+ columns: class · function · type · module · external — each goes bare → detailed
838
+ {selected ? ` · selected: ${selected}` : ''}
839
+ </div>
840
+ </div>
841
+ );
842
+ }
843
+
844
+ export const KindVariations: Story = {
845
+ render: () => <KindVariationsDemo />,
846
+ };
847
+
525
848
  // ---------------------------------------------------------------------------
526
849
  // Minimal (pre-graphify) vs Resolved (post-graphify) — the SAME subsystem,
527
850
  // showing what the LLM emits (no detail) and what graphify enrichment adds.
@@ -585,7 +908,6 @@ function MinimalVsResolvedDemo({ resolved }: { resolved: boolean }) {
585
908
  components={resolved ? resolvedComponents : minimalComponents}
586
909
  edges={sharedEdges}
587
910
  onSelect={(id) => setSelected(id)}
588
-
589
911
  />
590
912
  <div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
591
913
  {resolved
@@ -662,7 +984,6 @@ function InvestigationDemo() {
662
984
  components={investigationComponents}
663
985
  edges={investigationEdges}
664
986
  onSelect={(id) => setSelected(id)}
665
-
666
987
  />
667
988
  <div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
668
989
  read-only investigation snapshot (grok 019fd2a9) — components analyzed, not edited
@@ -754,7 +1075,6 @@ function MermaidDemo() {
754
1075
  components={mermaidComponents}
755
1076
  edges={mermaidEdges}
756
1077
  onSelect={(id) => setSelected(id)}
757
-
758
1078
  />
759
1079
  <div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
760
1080
  mermaid diagram rendering pipeline — lazy → render → zoom → modal