@principal-ai/subsystems-react 0.24.2 → 0.24.4

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.
@@ -7,6 +7,11 @@
7
7
 
8
8
  import { useCallback, useEffect, useMemo, useState } from 'react';
9
9
  import { File } from '@pierre/diffs/react';
10
+ import {
11
+ getFiletypeFromFileName,
12
+ getHighlighterOptions,
13
+ preloadHighlighter,
14
+ } from '@pierre/diffs';
10
15
  import { useTheme } from '@principal-ade/industry-theme';
11
16
  import { buildPierreOptions, PIERRE_FILE_STYLE } from './pierreBackground';
12
17
  import { pierreLangForPath } from './pierreFileLang';
@@ -33,6 +38,12 @@ export function PierreFileView({
33
38
  const { theme } = useTheme();
34
39
  const [contents, setContents] = useState<string | null>(null);
35
40
  const [error, setError] = useState<string | null>(null);
41
+ // Pierre highlights on the main thread with no worker pool: a cold first
42
+ // render paints an empty <pre> and never re-renders once Shiki resolves.
43
+ // Warm the shared highlighter for this file before mounting <File> so the
44
+ // very first render produces content.
45
+ const [highlighterReady, setHighlighterReady] = useState(false);
46
+ const lang = pierreLangForPath(filePath) ?? getFiletypeFromFileName(fileName);
36
47
 
37
48
  const fileObject = useMemo(
38
49
  () =>
@@ -60,6 +71,21 @@ export function PierreFileView({
60
71
  };
61
72
  }, [filePath, readFile]);
62
73
 
74
+ useEffect(() => {
75
+ let cancelled = false;
76
+ setHighlighterReady(false);
77
+ void preloadHighlighter(getHighlighterOptions(lang, {}))
78
+ .catch(() => {
79
+ // Fall through: let <File> attempt its own (plain-text) render.
80
+ })
81
+ .then(() => {
82
+ if (!cancelled) setHighlighterReady(true);
83
+ });
84
+ return () => {
85
+ cancelled = true;
86
+ };
87
+ }, [lang]);
88
+
63
89
  const onPostRender = useCallback(
64
90
  (fileContainer: HTMLElement) => {
65
91
  if (focusLine == null) return;
@@ -83,7 +109,7 @@ export function PierreFileView({
83
109
  </div>
84
110
  );
85
111
  }
86
- if (fileObject === null) {
112
+ if (fileObject === null || !highlighterReady) {
87
113
  return (
88
114
  <div style={{ padding: 16, color: theme.colors.textSecondary }}>
89
115
  Loading…
@@ -10,6 +10,11 @@
10
10
 
11
11
  import { useCallback, useEffect, useMemo, useState } from 'react';
12
12
  import { File } from '@pierre/diffs/react';
13
+ import {
14
+ getFiletypeFromFileName,
15
+ getHighlighterOptions,
16
+ preloadHighlighter,
17
+ } from '@pierre/diffs';
13
18
  import { useTheme } from '@principal-ade/industry-theme';
14
19
  import { buildPierreOptions, PIERRE_FILE_STYLE } from './pierreBackground';
15
20
  import { pierreLangForPath } from './pierreFileLang';
@@ -47,6 +52,25 @@ export function PierreSnippetView({
47
52
  const { theme } = useTheme();
48
53
  const [contents, setContents] = useState<string | null>(null);
49
54
  const [error, setError] = useState<string | null>(null);
55
+ // Warm the shared highlighter before mounting <File>; a cold first render
56
+ // paints an empty <pre> that never re-renders (no worker pool).
57
+ const [highlighterReady, setHighlighterReady] = useState(false);
58
+ const lang = pierreLangForPath(filePath) ?? getFiletypeFromFileName(fileName);
59
+
60
+ useEffect(() => {
61
+ let cancelled = false;
62
+ setHighlighterReady(false);
63
+ void preloadHighlighter(getHighlighterOptions(lang, {}))
64
+ .catch(() => {
65
+ // Fall through: let <File> attempt its own (plain-text) render.
66
+ })
67
+ .then(() => {
68
+ if (!cancelled) setHighlighterReady(true);
69
+ });
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, [lang]);
50
74
 
51
75
  useEffect(() => {
52
76
  let cancelled = false;
@@ -116,7 +140,7 @@ export function PierreSnippetView({
116
140
  </div>
117
141
  );
118
142
  }
119
- if (!fileObject || !slice) {
143
+ if (!fileObject || !slice || !highlighterReady) {
120
144
  return (
121
145
  <div style={{ padding: 16, color: theme.colors.textSecondary }}>
122
146
  Loading…
@@ -70,10 +70,68 @@ const ISSUES: SubsystemIssue[] = [
70
70
  },
71
71
  {
72
72
  id: 'i7',
73
- severity: 'warn',
73
+ severity: 'info',
74
74
  kind: 'boundary_process_nest_disagree',
75
- message: 'Component is nested in process "api" but Graphify places it outside.',
76
- target: { kind: 'module', id: 'api', label: 'api' },
75
+ message:
76
+ 'module "src/host/main.ts" members claim different processes: "host", "renderer"',
77
+ target: { kind: 'module', id: 'src/host/main.ts', label: 'src/host/main.ts' },
78
+ },
79
+ {
80
+ id: 'i8',
81
+ severity: 'warn',
82
+ kind: 'unverified_symbol',
83
+ message: 'Symbol persistSession unverified — Graphify anchor file-only: persistSession',
84
+ target: { kind: 'component', id: 'session-store', label: 'SessionStore' },
85
+ },
86
+ {
87
+ id: 'i9',
88
+ severity: 'error',
89
+ kind: 'stale_declaration',
90
+ message: 'Declaration moved since capture — recorded start line 42, now 57.',
91
+ target: { kind: 'component', id: 'session-store', label: 'SessionStore' },
92
+ fix: { label: 'Re-pin declaration' },
93
+ },
94
+ {
95
+ id: 'i10',
96
+ severity: 'info',
97
+ kind: 'construct_unconfirmed',
98
+ message: 'Graphify anchored the node but left the construct unclassified (interface vs type alias).',
99
+ target: { kind: 'component', id: 'host-info', label: 'HostInfo' },
100
+ },
101
+ {
102
+ id: 'i11',
103
+ severity: 'error',
104
+ kind: 'signature_mismatch',
105
+ message: 'Claimed (req: HostInfo) => Session; Graphify has (string) => void.',
106
+ target: { kind: 'component', id: 'entry', label: 'checkoutApi' },
107
+ },
108
+ {
109
+ id: 'i13',
110
+ severity: 'info',
111
+ kind: 'unresolved',
112
+ message: 'Repo root for pkg:github/you/your-app is unresolved — source checks skipped.',
113
+ target: { kind: 'component', id: 'stripe', label: 'Stripe' },
114
+ },
115
+ {
116
+ id: 'i14',
117
+ severity: 'info',
118
+ kind: 'topology_import_unconfirmed',
119
+ message: 'No import edge from checkoutApi to cartStore found in Graphify.',
120
+ target: { kind: 'relation', id: 'r1', label: 'checkoutApi → cartStore', detail: 'imports' },
121
+ },
122
+ {
123
+ id: 'i15',
124
+ severity: 'info',
125
+ kind: 'topology_relation_unconfirmed',
126
+ message: 'Graphify found no inheritance evidence for FileSessionStore → SessionStore.',
127
+ target: { kind: 'relation', id: 'r2', label: 'FileSessionStore → SessionStore', detail: 'extends' },
128
+ },
129
+ {
130
+ id: 'i16',
131
+ severity: 'error',
132
+ kind: 'boundary_module_without_file',
133
+ message: 'module "src/checkout" set but file is empty — nowhere to ground the frame.',
134
+ target: { kind: 'module', id: 'src/checkout', label: 'src/checkout' },
77
135
  },
78
136
  ];
79
137
 
@@ -120,7 +178,11 @@ export const GraphWithIssues: Story = {
120
178
  walkthroughs={graphEdges.walkthroughs}
121
179
  title="Checkout"
122
180
  description="A small e-commerce checkout subsystem."
123
- diagnostic={{ status: 'issues', issueCount: 5, onOpen: () => {} }}
181
+ diagnostic={{
182
+ status: 'issues',
183
+ issueCount: ISSUES.filter((i) => i.severity === 'error' || i.severity === 'warn').length,
184
+ onOpen: () => {},
185
+ }}
124
186
  issues={ISSUES}
125
187
  onSelectIssue={() => {}}
126
188
  onApplyIssueFix={() => {}}
@@ -60,13 +60,12 @@ export const SUBSYSTEM_ISSUE_CATEGORY_LABEL: Record<
60
60
  /** Default `kind` → layer. `boundary_*` splits by module (static) vs process (runtime). */
61
61
  const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
62
62
  missing_file: 'construct',
63
- missing_symbol: 'construct',
63
+ unverified_symbol: 'construct',
64
64
  stale_declaration: 'construct',
65
65
  construct_mismatch: 'construct',
66
66
  construct_unconfirmed: 'construct',
67
67
  signature_mismatch: 'construct',
68
68
  signature_unconfirmed: 'construct',
69
- anchor: 'construct',
70
69
  unresolved: 'construct',
71
70
  topology_broken_endpoint: 'static-topology',
72
71
  topology_import_unconfirmed: 'static-topology',
@@ -126,25 +125,24 @@ export function humanizeIssueKind(kind: string): string {
126
125
  /**
127
126
  * Curated short labels, trimmed of prefixes the grouping already implies
128
127
  * (`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.
128
+ * `construct_mismatch` reads "Construct mismatch" rather than a bare "Mismatch"
129
+ * that would collide with `signature_mismatch`. Explicit `issue.kindLabel` wins.
131
130
  */
132
131
  const KIND_LABEL: Record<string, string> = {
133
- missing_file: 'Missing file',
134
- missing_symbol: 'Missing symbol',
132
+ missing_file: 'Declaration file missing',
133
+ unverified_symbol: 'Unverified symbol',
135
134
  stale_declaration: 'Stale declaration',
136
- construct_mismatch: 'Wrong construct',
135
+ construct_mismatch: 'Construct mismatch',
137
136
  construct_unconfirmed: 'Unconfirmed construct',
138
137
  signature_mismatch: 'Signature mismatch',
139
138
  signature_unconfirmed: 'Signature unconfirmed',
140
- anchor: 'Anchor',
141
139
  unresolved: 'Unresolved',
142
- topology_broken_endpoint: 'Broken endpoint',
140
+ topology_broken_endpoint: 'Construct relation broken',
143
141
  topology_import_unconfirmed: 'Import unconfirmed',
144
142
  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',
143
+ boundary_module_without_file: 'Declaration file undefined',
144
+ boundary_module_file_mismatch: 'Declaration file outside module',
145
+ boundary_process_nest_disagree: 'Module spans multiple process contexts',
148
146
  walkthrough: 'Step issue',
149
147
  };
150
148