@principal-ai/subsystems-react 0.24.3 → 0.24.5

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 (39) hide show
  1. package/dist/components/session-events/SessionEventFeed.d.ts +2 -2
  2. package/dist/components/session-events/SessionEventFeed.js +9 -9
  3. package/dist/components/session-events/SessionEventFeed.js.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
  9. package/dist/pierre/PierreSnippetView.js +21 -1
  10. package/dist/pierre/PierreSnippetView.js.map +1 -1
  11. package/dist/pierre/scrollAnchor.d.ts.map +1 -1
  12. package/dist/pierre/scrollAnchor.js +9 -1
  13. package/dist/pierre/scrollAnchor.js.map +1 -1
  14. package/dist/subsystem/ComponentDeclaration.d.ts +5 -2
  15. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  16. package/dist/subsystem/ComponentDeclaration.js +18 -3
  17. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  18. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  19. package/dist/subsystem/ConstructsCatalog.js +32 -11
  20. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  21. package/dist/subsystem/IssueList.d.ts +4 -2
  22. package/dist/subsystem/IssueList.d.ts.map +1 -1
  23. package/dist/subsystem/IssueList.js +105 -31
  24. package/dist/subsystem/IssueList.js.map +1 -1
  25. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  26. package/dist/subsystem/SubsystemComponentGraph.js +3 -1
  27. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  28. package/dist/subsystem/declarationRef.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/src/components/session-events/SessionEventFeed.tsx +9 -9
  31. package/src/index.ts +1 -0
  32. package/src/pierre/PierreSnippetView.tsx +25 -1
  33. package/src/pierre/scrollAnchor.ts +7 -1
  34. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +74 -5
  35. package/src/subsystem/ComponentDeclaration.tsx +30 -3
  36. package/src/subsystem/ConstructsCatalog.tsx +52 -28
  37. package/src/subsystem/IssueList.tsx +117 -31
  38. package/src/subsystem/SubsystemComponentGraph.tsx +5 -0
  39. package/src/subsystem/declarationRef.ts +1 -1
@@ -66,7 +66,7 @@ export interface ComponentVerificationState {
66
66
  inlineParameters?: number;
67
67
  };
68
68
  declaration?: {
69
- freshness: 'valid' | 'stale' | 'missing' | 'unanchored' | 'unchecked';
69
+ freshness: 'fresh' | 'stale' | 'missing' | 'unanchored' | 'unchecked';
70
70
  ref?: {
71
71
  startLine: number;
72
72
  lineHash: string;
@@ -120,6 +120,9 @@ export interface ComponentDeclarationProps {
120
120
  verification?: ComponentVerificationState | null;
121
121
  /** Start with the file path row visible (catalog / source-first views). */
122
122
  defaultShowFile?: boolean;
123
+ /** True while this component's file is open in the host drawer — keeps the
124
+ * declaration visibly tied to the source being read. */
125
+ fileOpen?: boolean;
123
126
  }
124
127
 
125
128
  function verificationSummary(
@@ -259,6 +262,7 @@ export function ComponentDeclaration({
259
262
  onVerify,
260
263
  verification,
261
264
  defaultShowFile = false,
265
+ fileOpen = false,
262
266
  }: ComponentDeclarationProps) {
263
267
  const { theme, mode } = useTheme();
264
268
  const pierreSyntaxTheme = resolvePierreSyntaxThemeName(mode);
@@ -380,8 +384,31 @@ export function ComponentDeclaration({
380
384
  </button>
381
385
  );
382
386
  const verifyBusy = verification?.phase === 'checking';
387
+ const openPill = fileOpen ? (
388
+ <span
389
+ title="This declaration's file is open below"
390
+ style={{
391
+ display: 'inline-flex',
392
+ alignItems: 'center',
393
+ gap: 4,
394
+ height: 18,
395
+ padding: '0 6px',
396
+ border: `1px solid ${theme.colors.accent ?? color}`,
397
+ borderRadius: 999,
398
+ color: theme.colors.accent ?? color,
399
+ fontFamily: theme.fonts.monospace,
400
+ fontSize: theme.fontSizes[0] * 0.85,
401
+ letterSpacing: 0.4,
402
+ textTransform: 'uppercase',
403
+ whiteSpace: 'nowrap',
404
+ }}
405
+ >
406
+ file open
407
+ </span>
408
+ ) : null;
383
409
  const headerActions = (
384
- <span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 2 }}>
410
+ <span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
411
+ {openPill}
385
412
  {onVerify && (
386
413
  <button
387
414
  type="button"
@@ -458,7 +485,7 @@ export function ComponentDeclaration({
458
485
  'purl',
459
486
  ),
460
487
  );
461
- } else if (onVerify) {
488
+ } else if (onVerify || fileOpen) {
462
489
  lines.push(line(headerActions, 'actions'));
463
490
  }
464
491
 
@@ -88,6 +88,7 @@ export function ConstructsCatalog({
88
88
  const { theme, mode } = useTheme();
89
89
  const pierreTheme = resolvePierreSyntaxThemeName(mode);
90
90
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
91
+ const accentColor = theme.colors.accent ?? theme.colors.info;
91
92
  const [visibleIds, setVisibleIds] = useState<string[]>(() =>
92
93
  components[0] ? [components[0].id] : [],
93
94
  );
@@ -121,6 +122,20 @@ export function ConstructsCatalog({
121
122
  ?.scrollIntoView({ block: 'nearest' });
122
123
  }, [visibleIds]);
123
124
 
125
+ // Keep the declaration whose file is open in view, so the source and its
126
+ // signature stay legible together.
127
+ useEffect(() => {
128
+ if (!drawer) return;
129
+ const match =
130
+ components.find(
131
+ (c) => c.file === drawer.file && c.declarationRef?.startLine === drawer.startLine,
132
+ ) ?? components.find((c) => c.file === drawer.file);
133
+ if (!match) return;
134
+ document
135
+ .getElementById(`construct-signature-${match.id}`)
136
+ ?.scrollIntoView({ block: 'nearest' });
137
+ }, [components, drawer]);
138
+
124
139
  // Drop ids that left the model; if nothing remains, show the first construct.
125
140
  useEffect(() => {
126
141
  const ids = new Set(components.map((c) => c.id));
@@ -423,6 +438,7 @@ export function ConstructsCatalog({
423
438
  );
424
439
  const role = rightBadgeLabel(c);
425
440
  const roleColor = rightBadgeColor(c);
441
+ const rowOpen = drawer != null && !!c.file && c.file === drawer.file;
426
442
  return (
427
443
  <li key={c.id} role="presentation">
428
444
  <button
@@ -447,9 +463,12 @@ export function ConstructsCatalog({
447
463
  ? theme.colors.background
448
464
  : 'transparent',
449
465
  cursor: 'pointer',
450
- boxShadow: visible
451
- ? `inset 0 0 0 1px ${theme.colors.border}`
452
- : undefined,
466
+ boxShadow: [
467
+ visible ? `inset 0 0 0 1px ${theme.colors.border}` : null,
468
+ rowOpen ? `inset 2px 0 0 ${accentColor}` : null,
469
+ ]
470
+ .filter(Boolean)
471
+ .join(', ') || undefined,
453
472
  outline: focused && !visible ? `1px dotted ${muted}` : undefined,
454
473
  outlineOffset: -1,
455
474
  opacity: visible ? 1 : 0.72,
@@ -547,31 +566,36 @@ export function ConstructsCatalog({
547
566
  }}
548
567
  >
549
568
  {visibleComponents.length > 0 ? (
550
- visibleComponents.map((c) => (
551
- <div
552
- key={c.id}
553
- id={`construct-signature-${c.id}`}
554
- style={{
555
- border: `1px solid ${theme.colors.border}`,
556
- borderRadius: 8,
557
- overflow: 'hidden',
558
- flexShrink: 0,
559
- }}
560
- >
561
- <ComponentDeclaration
562
- component={c}
563
- onOpenFile={fileViewer ? onOpenFile : undefined}
564
- defaultShowFile={!!fileViewer}
565
- onRelatedSelect={onRelatedSelect}
566
- onVerify={onVerifyComponent}
567
- verification={
568
- componentVerification && focusedId === c.id
569
- ? componentVerification
570
- : undefined
571
- }
572
- />
573
- </div>
574
- ))
569
+ visibleComponents.map((c) => {
570
+ const open = drawer != null && !!c.file && c.file === drawer.file;
571
+ return (
572
+ <div
573
+ key={c.id}
574
+ id={`construct-signature-${c.id}`}
575
+ style={{
576
+ border: `1px solid ${open ? accentColor : theme.colors.border}`,
577
+ boxShadow: open ? `0 0 0 1px ${accentColor}` : undefined,
578
+ borderRadius: 8,
579
+ overflow: 'hidden',
580
+ flexShrink: 0,
581
+ }}
582
+ >
583
+ <ComponentDeclaration
584
+ component={c}
585
+ onOpenFile={fileViewer ? onOpenFile : undefined}
586
+ defaultShowFile={!!fileViewer}
587
+ onRelatedSelect={onRelatedSelect}
588
+ onVerify={onVerifyComponent}
589
+ fileOpen={open}
590
+ verification={
591
+ componentVerification && focusedId === c.id
592
+ ? componentVerification
593
+ : undefined
594
+ }
595
+ />
596
+ </div>
597
+ );
598
+ })
575
599
  ) : (
576
600
  <p
577
601
  style={{
@@ -27,6 +27,7 @@ export type SubsystemIssueTargetKind =
27
27
  | 'relation'
28
28
  | 'module'
29
29
  | 'walkthrough'
30
+ | 'repo'
30
31
  | 'graph';
31
32
 
32
33
  /**
@@ -34,6 +35,7 @@ export type SubsystemIssueTargetKind =
34
35
  * constructs → static topology → runtime topology → walkthrough.
35
36
  */
36
37
  export type SubsystemIssueCategory =
38
+ | 'repo'
37
39
  | 'construct'
38
40
  | 'static-topology'
39
41
  | 'runtime-topology'
@@ -41,6 +43,7 @@ export type SubsystemIssueCategory =
41
43
 
42
44
  /** Layer order + display names, shared by the list and its headers. */
43
45
  export const SUBSYSTEM_ISSUE_CATEGORIES: SubsystemIssueCategory[] = [
46
+ 'repo',
44
47
  'construct',
45
48
  'static-topology',
46
49
  'runtime-topology',
@@ -51,6 +54,7 @@ export const SUBSYSTEM_ISSUE_CATEGORY_LABEL: Record<
51
54
  SubsystemIssueCategory,
52
55
  string
53
56
  > = {
57
+ repo: 'Repositories',
54
58
  construct: 'Constructs',
55
59
  'static-topology': 'Static topology',
56
60
  'runtime-topology': 'Runtime topology',
@@ -60,18 +64,18 @@ export const SUBSYSTEM_ISSUE_CATEGORY_LABEL: Record<
60
64
  /** Default `kind` → layer. `boundary_*` splits by module (static) vs process (runtime). */
61
65
  const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
62
66
  missing_file: 'construct',
63
- missing_symbol: 'construct',
67
+ symbol_ambiguous: 'construct',
68
+ symbol_unmatched: 'construct',
64
69
  stale_declaration: 'construct',
65
70
  construct_mismatch: 'construct',
66
71
  construct_unconfirmed: 'construct',
67
72
  signature_mismatch: 'construct',
68
73
  signature_unconfirmed: 'construct',
69
- anchor: 'construct',
70
- unresolved: 'construct',
74
+ repo_unresolved: 'repo',
75
+ graphify_unavailable: 'repo',
71
76
  topology_broken_endpoint: 'static-topology',
72
77
  topology_import_unconfirmed: 'static-topology',
73
78
  topology_relation_unconfirmed: 'static-topology',
74
- boundary_module_without_file: 'static-topology',
75
79
  boundary_module_file_mismatch: 'static-topology',
76
80
  boundary_process_nest_disagree: 'runtime-topology',
77
81
  walkthrough: 'walkthrough',
@@ -124,27 +128,28 @@ export function humanizeIssueKind(kind: string): string {
124
128
  }
125
129
 
126
130
  /**
127
- * Curated short labels, trimmed of prefixes the grouping already implies
128
- * (`topology_`, `boundary_`) but kept unambiguous within a layer — so
129
- * `construct_mismatch` reads "Wrong construct", not a bare "Mismatch" that
130
- * would collide with `signature_mismatch`. Explicit `issue.kindLabel` wins.
131
+ * Curated labels. Construct-layer findings stay fully qualified ("Construct
132
+ * declaration file missing", "Construct type mismatch") rather than relying on
133
+ * the category header — this UI is new and the extra words read clearer.
134
+ * Topology findings drop the `topology_`/`boundary_` prefix since the layer
135
+ * already scopes them. Explicit `issue.kindLabel` wins.
131
136
  */
132
137
  const KIND_LABEL: Record<string, string> = {
133
- missing_file: 'Missing file',
134
- missing_symbol: 'Missing symbol',
135
- stale_declaration: 'Stale declaration',
136
- construct_mismatch: 'Wrong construct',
137
- construct_unconfirmed: 'Unconfirmed construct',
138
- signature_mismatch: 'Signature mismatch',
139
- signature_unconfirmed: 'Signature unconfirmed',
140
- anchor: 'Anchor',
141
- unresolved: 'Unresolved',
142
- topology_broken_endpoint: 'Broken endpoint',
138
+ missing_file: 'Construct declaration file missing',
139
+ symbol_ambiguous: 'Multiple construct symbol matches',
140
+ symbol_unmatched: 'Construct symbol unconfirmed',
141
+ stale_declaration: 'Construct declaration drift',
142
+ construct_mismatch: 'Construct type mismatch',
143
+ construct_unconfirmed: 'Construct type unconfirmed',
144
+ signature_mismatch: 'Construct signature mismatch',
145
+ signature_unconfirmed: 'Construct signature unconfirmed',
146
+ repo_unresolved: 'No repository clone',
147
+ graphify_unavailable: 'Graphify cache unavailable',
148
+ topology_broken_endpoint: 'Construct relationship broken',
143
149
  topology_import_unconfirmed: 'Import unconfirmed',
144
- topology_relation_unconfirmed: 'Relation unconfirmed',
145
- boundary_module_without_file: 'Module without file',
146
- boundary_module_file_mismatch: 'Module file mismatch',
147
- boundary_process_nest_disagree: 'Process nest disagree',
150
+ topology_relation_unconfirmed: 'Relationship unconfirmed',
151
+ boundary_module_file_mismatch: 'Declaration file outside module',
152
+ boundary_process_nest_disagree: 'Module spans multiple process contexts',
148
153
  walkthrough: 'Step issue',
149
154
  };
150
155
 
@@ -153,6 +158,44 @@ export function issueKindLabel(issue: SubsystemIssue): string {
153
158
  return issue.kindLabel ?? KIND_LABEL[issue.kind] ?? humanizeIssueKind(issue.kind);
154
159
  }
155
160
 
161
+ /**
162
+ * Display order within the list, by verification pipeline — not severity.
163
+ * Mirrors the per-component check order (file → symbol → declaration → type →
164
+ * signature), then topology (integrity → corroboration → membership) and the
165
+ * remaining layers. Within a stage, "unconfirmed" precedes "mismatch": you
166
+ * reach a verdict before you can find it wrong.
167
+ */
168
+ const KIND_ORDER: Record<string, number> = {
169
+ // Repositories — availability precondition
170
+ repo_unresolved: 0,
171
+ graphify_unavailable: 1,
172
+ // Constructs — file → symbol → declaration → type → signature
173
+ missing_file: 10,
174
+ symbol_unmatched: 11,
175
+ symbol_ambiguous: 12,
176
+ stale_declaration: 13,
177
+ construct_unconfirmed: 14,
178
+ construct_mismatch: 15,
179
+ signature_unconfirmed: 16,
180
+ signature_mismatch: 17,
181
+ // Static topology — edge integrity → corroboration → membership
182
+ topology_broken_endpoint: 20,
183
+ topology_relation_unconfirmed: 21,
184
+ topology_import_unconfirmed: 22,
185
+ boundary_module_file_mismatch: 23,
186
+ // Runtime topology
187
+ boundary_process_nest_disagree: 30,
188
+ // Walkthrough
189
+ walkthrough: 40,
190
+ };
191
+
192
+ const KIND_ORDER_FALLBACK = 100;
193
+
194
+ /** Sort key for a kind; unknown kinds sort last. */
195
+ export function issueKindOrder(kind: string): number {
196
+ return KIND_ORDER[kind] ?? KIND_ORDER_FALLBACK;
197
+ }
198
+
156
199
  export function severityRank(severity: SubsystemIssueSeverity): number {
157
200
  return SEVERITY_RANK[severity] ?? SEVERITY_RANK.info;
158
201
  }
@@ -246,7 +289,12 @@ export function groupIssuesByCategory(
246
289
  }
247
290
  return SUBSYSTEM_ISSUE_CATEGORIES.map((category) => {
248
291
  const grouped = byCategory.get(category) ?? [];
249
- grouped.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
292
+ // Logical pipeline order, then by target label (stable for the rest).
293
+ grouped.sort((a, b) => {
294
+ const byKind = issueKindOrder(a.kind) - issueKindOrder(b.kind);
295
+ if (byKind !== 0) return byKind;
296
+ return (a.target?.label ?? '').localeCompare(b.target?.label ?? '');
297
+ });
250
298
  return {
251
299
  category,
252
300
  label: SUBSYSTEM_ISSUE_CATEGORY_LABEL[category],
@@ -269,9 +317,10 @@ function severityColor(
269
317
 
270
318
  const TARGET_KIND_LABEL: Record<SubsystemIssueTargetKind, string> = {
271
319
  component: 'component',
272
- relation: 'relation',
320
+ relation: 'relationship',
273
321
  module: 'module',
274
322
  walkthrough: 'flow',
323
+ repo: 'repo',
275
324
  graph: 'model',
276
325
  };
277
326
 
@@ -445,6 +494,19 @@ export function SubsystemIssueList({
445
494
  const { theme } = useTheme();
446
495
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
447
496
  const categories = groupIssuesByCategory(issues);
497
+ // Collapsed categories (by id). Everything starts expanded.
498
+ const [collapsed, setCollapsed] = useState<Set<SubsystemIssueCategory>>(
499
+ () => new Set(),
500
+ );
501
+ const [hovered, setHovered] = useState<SubsystemIssueCategory | null>(null);
502
+ const toggleCollapsed = (category: SubsystemIssueCategory) => {
503
+ setCollapsed((prev) => {
504
+ const next = new Set(prev);
505
+ if (next.has(category)) next.delete(category);
506
+ else next.add(category);
507
+ return next;
508
+ });
509
+ };
448
510
 
449
511
  return (
450
512
  <div style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
@@ -455,19 +517,39 @@ export function SubsystemIssueList({
455
517
  const accent = category.severity
456
518
  ? severityColor(category.severity, theme.colors, muted)
457
519
  : theme.colors.success;
520
+ const isCollapsed = collapsed.has(category.category);
521
+ const isHovered = hovered === category.category;
458
522
  return (
459
523
  <div key={category.category}>
460
- <div
524
+ <button
525
+ type="button"
526
+ aria-expanded={!isCollapsed}
527
+ onClick={() => toggleCollapsed(category.category)}
528
+ onMouseEnter={() => setHovered(category.category)}
529
+ onMouseLeave={() => setHovered(null)}
461
530
  style={{
462
531
  position: 'sticky',
463
532
  top: 0,
464
533
  zIndex: 1,
534
+ width: '100%',
535
+ boxSizing: 'border-box',
465
536
  display: 'flex',
466
537
  alignItems: 'center',
467
- gap: 7,
468
- padding: '7px 12px',
469
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
538
+ gap: 8,
539
+ padding: '9px 12px',
540
+ background: isHovered
541
+ ? (theme.colors.backgroundHover ??
542
+ theme.colors.backgroundSecondary ??
543
+ theme.colors.background)
544
+ : (theme.colors.backgroundTertiary ??
545
+ theme.colors.backgroundSecondary ??
546
+ theme.colors.background),
547
+ border: 'none',
548
+ borderTop: `1px solid ${theme.colors.border}`,
470
549
  borderBottom: `1px solid ${theme.colors.border}`,
550
+ cursor: 'pointer',
551
+ textAlign: 'left',
552
+ fontFamily: theme.fonts.body,
471
553
  }}
472
554
  >
473
555
  <span
@@ -475,9 +557,9 @@ export function SubsystemIssueList({
475
557
  flex: 1,
476
558
  minWidth: 0,
477
559
  fontSize: theme.fontSizes[1],
478
- fontWeight: 600,
560
+ fontWeight: 700,
561
+ letterSpacing: 0.2,
479
562
  color: clean ? muted : theme.colors.text,
480
- fontFamily: theme.fonts.body,
481
563
  whiteSpace: 'nowrap',
482
564
  overflow: 'hidden',
483
565
  textOverflow: 'ellipsis',
@@ -500,8 +582,12 @@ export function SubsystemIssueList({
500
582
  {category.count}
501
583
  </span>
502
584
  )}
503
- </div>
585
+ <span style={{ display: 'inline-flex', color: muted, flexShrink: 0 }}>
586
+ {isCollapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
587
+ </span>
588
+ </button>
504
589
  {!clean &&
590
+ !isCollapsed &&
505
591
  category.issues.map((issue) => (
506
592
  <SubsystemIssueCard
507
593
  key={issue.id}
@@ -1993,6 +1993,11 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
1993
1993
  onRelatedSelect={resolveRelatedComponent}
1994
1994
  onVerify={onVerifyComponent}
1995
1995
  verification={componentVerification}
1996
+ fileOpen={
1997
+ drawerTarget?.kind === 'file' &&
1998
+ !!selected.file &&
1999
+ selected.file === drawerTarget.file
2000
+ }
1996
2001
  />
1997
2002
  </div>
1998
2003
  )}
@@ -26,7 +26,7 @@ export interface SubsystemDeclarationRef {
26
26
  }
27
27
 
28
28
  export type DeclarationFreshness =
29
- | 'valid'
29
+ | 'fresh'
30
30
  | 'stale'
31
31
  | 'missing'
32
32
  | 'unanchored'