@principal-ai/subsystems-react 0.24.4 → 0.24.6

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 (35) 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/scrollAnchor.d.ts.map +1 -1
  9. package/dist/pierre/scrollAnchor.js +9 -1
  10. package/dist/pierre/scrollAnchor.js.map +1 -1
  11. package/dist/subsystem/ComponentDeclaration.d.ts +5 -2
  12. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  13. package/dist/subsystem/ComponentDeclaration.js +16 -2
  14. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  15. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  16. package/dist/subsystem/ConstructsCatalog.js +42 -11
  17. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  18. package/dist/subsystem/IssueList.d.ts +4 -2
  19. package/dist/subsystem/IssueList.d.ts.map +1 -1
  20. package/dist/subsystem/IssueList.js +103 -27
  21. package/dist/subsystem/IssueList.js.map +1 -1
  22. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  23. package/dist/subsystem/SubsystemComponentGraph.js +3 -1
  24. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  25. package/dist/subsystem/declarationRef.d.ts +1 -1
  26. package/package.json +1 -1
  27. package/src/components/session-events/SessionEventFeed.tsx +9 -9
  28. package/src/index.ts +1 -0
  29. package/src/pierre/scrollAnchor.ts +7 -1
  30. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +21 -14
  31. package/src/subsystem/ComponentDeclaration.tsx +27 -1
  32. package/src/subsystem/ConstructsCatalog.tsx +64 -28
  33. package/src/subsystem/IssueList.tsx +115 -27
  34. package/src/subsystem/SubsystemComponentGraph.tsx +5 -0
  35. package/src/subsystem/declarationRef.ts +1 -1
@@ -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,12 @@ export function ConstructsCatalog({
423
438
  );
424
439
  const role = rightBadgeLabel(c);
425
440
  const roleColor = rightBadgeColor(c);
441
+ const rowOpen =
442
+ drawer != null &&
443
+ drawer.startLine != null &&
444
+ !!c.file &&
445
+ c.file === drawer.file &&
446
+ c.declarationRef?.startLine === drawer.startLine;
426
447
  return (
427
448
  <li key={c.id} role="presentation">
428
449
  <button
@@ -447,9 +468,12 @@ export function ConstructsCatalog({
447
468
  ? theme.colors.background
448
469
  : 'transparent',
449
470
  cursor: 'pointer',
450
- boxShadow: visible
451
- ? `inset 0 0 0 1px ${theme.colors.border}`
452
- : undefined,
471
+ boxShadow: [
472
+ visible ? `inset 0 0 0 1px ${theme.colors.border}` : null,
473
+ rowOpen ? `inset 2px 0 0 ${accentColor}` : null,
474
+ ]
475
+ .filter(Boolean)
476
+ .join(', ') || undefined,
453
477
  outline: focused && !visible ? `1px dotted ${muted}` : undefined,
454
478
  outlineOffset: -1,
455
479
  opacity: visible ? 1 : 0.72,
@@ -547,31 +571,43 @@ export function ConstructsCatalog({
547
571
  }}
548
572
  >
549
573
  {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
- ))
574
+ visibleComponents.map((c) => {
575
+ // File-level: every declaration sharing the open file shows the
576
+ // indicator. Line-level: only the declaration actually being
577
+ // highlighted gets the accent border.
578
+ const fileOpen = drawer != null && !!c.file && c.file === drawer.file;
579
+ const lineOpen =
580
+ fileOpen &&
581
+ drawer.startLine != null &&
582
+ c.declarationRef?.startLine === drawer.startLine;
583
+ return (
584
+ <div
585
+ key={c.id}
586
+ id={`construct-signature-${c.id}`}
587
+ style={{
588
+ border: `1px solid ${lineOpen ? accentColor : theme.colors.border}`,
589
+ boxShadow: lineOpen ? `0 0 0 1px ${accentColor}` : undefined,
590
+ borderRadius: 8,
591
+ overflow: 'hidden',
592
+ flexShrink: 0,
593
+ }}
594
+ >
595
+ <ComponentDeclaration
596
+ component={c}
597
+ onOpenFile={fileViewer ? onOpenFile : undefined}
598
+ defaultShowFile={!!fileViewer}
599
+ onRelatedSelect={onRelatedSelect}
600
+ onVerify={onVerifyComponent}
601
+ fileOpen={fileOpen}
602
+ verification={
603
+ componentVerification && focusedId === c.id
604
+ ? componentVerification
605
+ : undefined
606
+ }
607
+ />
608
+ </div>
609
+ );
610
+ })
575
611
  ) : (
576
612
  <p
577
613
  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,17 +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
- unverified_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
- unresolved: 'construct',
74
+ repo_unresolved: 'repo',
75
+ graphify_unavailable: 'repo',
70
76
  topology_broken_endpoint: 'static-topology',
71
77
  topology_import_unconfirmed: 'static-topology',
72
78
  topology_relation_unconfirmed: 'static-topology',
73
- boundary_module_without_file: 'static-topology',
74
79
  boundary_module_file_mismatch: 'static-topology',
75
80
  boundary_process_nest_disagree: 'runtime-topology',
76
81
  walkthrough: 'walkthrough',
@@ -123,24 +128,26 @@ export function humanizeIssueKind(kind: string): string {
123
128
  }
124
129
 
125
130
  /**
126
- * Curated short labels, trimmed of prefixes the grouping already implies
127
- * (`topology_`, `boundary_`) but kept unambiguous within a layer — so
128
- * `construct_mismatch` reads "Construct mismatch" rather than a bare "Mismatch"
129
- * that 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.
130
136
  */
131
137
  const KIND_LABEL: Record<string, string> = {
132
- missing_file: 'Declaration file missing',
133
- unverified_symbol: 'Unverified symbol',
134
- stale_declaration: 'Stale declaration',
135
- construct_mismatch: 'Construct mismatch',
136
- construct_unconfirmed: 'Unconfirmed construct',
137
- signature_mismatch: 'Signature mismatch',
138
- signature_unconfirmed: 'Signature unconfirmed',
139
- unresolved: 'Unresolved',
140
- topology_broken_endpoint: 'Construct relation broken',
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',
141
149
  topology_import_unconfirmed: 'Import unconfirmed',
142
- topology_relation_unconfirmed: 'Relation unconfirmed',
143
- boundary_module_without_file: 'Declaration file undefined',
150
+ topology_relation_unconfirmed: 'Relationship unconfirmed',
144
151
  boundary_module_file_mismatch: 'Declaration file outside module',
145
152
  boundary_process_nest_disagree: 'Module spans multiple process contexts',
146
153
  walkthrough: 'Step issue',
@@ -151,6 +158,44 @@ export function issueKindLabel(issue: SubsystemIssue): string {
151
158
  return issue.kindLabel ?? KIND_LABEL[issue.kind] ?? humanizeIssueKind(issue.kind);
152
159
  }
153
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
+
154
199
  export function severityRank(severity: SubsystemIssueSeverity): number {
155
200
  return SEVERITY_RANK[severity] ?? SEVERITY_RANK.info;
156
201
  }
@@ -244,7 +289,12 @@ export function groupIssuesByCategory(
244
289
  }
245
290
  return SUBSYSTEM_ISSUE_CATEGORIES.map((category) => {
246
291
  const grouped = byCategory.get(category) ?? [];
247
- 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
+ });
248
298
  return {
249
299
  category,
250
300
  label: SUBSYSTEM_ISSUE_CATEGORY_LABEL[category],
@@ -267,9 +317,10 @@ function severityColor(
267
317
 
268
318
  const TARGET_KIND_LABEL: Record<SubsystemIssueTargetKind, string> = {
269
319
  component: 'component',
270
- relation: 'relation',
320
+ relation: 'relationship',
271
321
  module: 'module',
272
322
  walkthrough: 'flow',
323
+ repo: 'repo',
273
324
  graph: 'model',
274
325
  };
275
326
 
@@ -443,6 +494,19 @@ export function SubsystemIssueList({
443
494
  const { theme } = useTheme();
444
495
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
445
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
+ };
446
510
 
447
511
  return (
448
512
  <div style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
@@ -453,19 +517,39 @@ export function SubsystemIssueList({
453
517
  const accent = category.severity
454
518
  ? severityColor(category.severity, theme.colors, muted)
455
519
  : theme.colors.success;
520
+ const isCollapsed = collapsed.has(category.category);
521
+ const isHovered = hovered === category.category;
456
522
  return (
457
523
  <div key={category.category}>
458
- <div
524
+ <button
525
+ type="button"
526
+ aria-expanded={!isCollapsed}
527
+ onClick={() => toggleCollapsed(category.category)}
528
+ onMouseEnter={() => setHovered(category.category)}
529
+ onMouseLeave={() => setHovered(null)}
459
530
  style={{
460
531
  position: 'sticky',
461
532
  top: 0,
462
533
  zIndex: 1,
534
+ width: '100%',
535
+ boxSizing: 'border-box',
463
536
  display: 'flex',
464
537
  alignItems: 'center',
465
- gap: 7,
466
- padding: '7px 12px',
467
- 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}`,
468
549
  borderBottom: `1px solid ${theme.colors.border}`,
550
+ cursor: 'pointer',
551
+ textAlign: 'left',
552
+ fontFamily: theme.fonts.body,
469
553
  }}
470
554
  >
471
555
  <span
@@ -473,9 +557,9 @@ export function SubsystemIssueList({
473
557
  flex: 1,
474
558
  minWidth: 0,
475
559
  fontSize: theme.fontSizes[1],
476
- fontWeight: 600,
560
+ fontWeight: 700,
561
+ letterSpacing: 0.2,
477
562
  color: clean ? muted : theme.colors.text,
478
- fontFamily: theme.fonts.body,
479
563
  whiteSpace: 'nowrap',
480
564
  overflow: 'hidden',
481
565
  textOverflow: 'ellipsis',
@@ -498,8 +582,12 @@ export function SubsystemIssueList({
498
582
  {category.count}
499
583
  </span>
500
584
  )}
501
- </div>
585
+ <span style={{ display: 'inline-flex', color: muted, flexShrink: 0 }}>
586
+ {isCollapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
587
+ </span>
588
+ </button>
502
589
  {!clean &&
590
+ !isCollapsed &&
503
591
  category.issues.map((issue) => (
504
592
  <SubsystemIssueCard
505
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'