@principal-ai/subsystems-react 0.38.0 → 0.39.0

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 (54) hide show
  1. package/dist/graphify/consolidated.d.ts +8 -0
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/construct.d.ts +18 -0
  4. package/dist/graphify/construct.d.ts.map +1 -1
  5. package/dist/graphify/construct.js +23 -0
  6. package/dist/graphify/construct.js.map +1 -1
  7. package/dist/graphify/index.d.ts +2 -2
  8. package/dist/graphify/index.d.ts.map +1 -1
  9. package/dist/graphify/index.js +1 -1
  10. package/dist/graphify/index.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/subsystem/IssueList.d.ts +49 -3
  16. package/dist/subsystem/IssueList.d.ts.map +1 -1
  17. package/dist/subsystem/IssueList.js +180 -79
  18. package/dist/subsystem/IssueList.js.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.d.ts +9 -1
  20. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  21. package/dist/subsystem/SubsystemComponentGraph.js +461 -12
  22. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  23. package/dist/subsystem/formatDeclaration.js +19 -5
  24. package/dist/subsystem/formatDeclaration.js.map +1 -1
  25. package/dist/subsystem/model.d.ts +38 -0
  26. package/dist/subsystem/model.d.ts.map +1 -1
  27. package/dist/subsystem/model.js.map +1 -1
  28. package/dist/subsystem/nodes.d.ts.map +1 -1
  29. package/dist/subsystem/nodes.js +124 -67
  30. package/dist/subsystem/nodes.js.map +1 -1
  31. package/dist/subsystem/symbolRefs.d.ts.map +1 -1
  32. package/dist/subsystem/symbolRefs.js +2 -0
  33. package/dist/subsystem/symbolRefs.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/graphify/consolidated.ts +8 -0
  36. package/src/graphify/construct.test.ts +61 -1
  37. package/src/graphify/construct.ts +33 -0
  38. package/src/graphify/index.ts +6 -1
  39. package/src/index.ts +1 -0
  40. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +625 -0
  41. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +7 -1
  42. package/src/stories/Subsystem/ComponentGraph/NodeAnatomy.stories.tsx +40 -73
  43. package/src/stories/Subsystem/ComponentGraph/StoreValueType.stories.tsx +261 -0
  44. package/src/stories/Subsystem/StoreValueTypeDeclaration.stories.tsx +182 -0
  45. package/src/subsystem/IssueList.focus.test.tsx +113 -0
  46. package/src/subsystem/IssueList.icon.test.tsx +95 -0
  47. package/src/subsystem/IssueList.tsx +267 -111
  48. package/src/subsystem/SubsystemComponentGraph.tsx +530 -12
  49. package/src/subsystem/formatDeclaration.test.ts +64 -0
  50. package/src/subsystem/formatDeclaration.ts +20 -5
  51. package/src/subsystem/model.ts +46 -0
  52. package/src/subsystem/nodes.group.test.tsx +52 -0
  53. package/src/subsystem/nodes.tsx +102 -2
  54. package/src/subsystem/symbolRefs.ts +2 -0
@@ -1,21 +1,27 @@
1
1
  import React from 'react';
2
2
  import '@xyflow/react/dist/style.css';
3
3
  import type { Meta, StoryObj } from '@storybook/react';
4
- import { ReactFlowProvider, type NodeProps } from '@xyflow/react';
4
+ import { ReactFlowProvider, type Node, type NodeProps } from '@xyflow/react';
5
5
  import { ThemeProvider, defaultEditorTheme, useTheme } from '@principal-ade/industry-theme';
6
- import { FileX, Search, MapPin, Shapes, Sigma } from 'lucide-react';
7
6
  import { SubsystemComponentNode } from '../../../subsystem/nodes';
8
7
  import type { GraphifyComponentDetail } from '../../../graphify';
9
- import type { SubsystemComponent, SubsystemGraphNode } from '../../../subsystem/model';
8
+ import type {
9
+ SubsystemComponent,
10
+ SubsystemGraphNodeData,
11
+ SubsystemNodeIssue,
12
+ } from '../../../subsystem/model';
10
13
 
11
14
  /**
12
15
  * NodeAnatomy — a dissection of the component node's visual language.
13
16
  *
14
17
  * Section 1 lays out every channel the node already spends (border, corners,
15
18
  * left badge, right badge cluster, fill) so an added channel can be chosen
16
- * against it. Section 2 shows the *proposed* diagnostics layer (severity ring +
17
- * earliest-rung corner chip) composed over real nodes. Nothing here is wired
18
- * into the production node yet — this is the reference to design against.
19
+ * against it. Section 2 shows the diagnostics layer (dotted border +
20
+ * earliest-rung corner chip) composed over real nodes — now wired into the
21
+ * production node as `data.issue` (see `NodeIssueOverlay` in `nodes.tsx` and
22
+ * `issueBadgeByAlias` in `SubsystemComponentGraph`); this story stays the
23
+ * visual reference. Sections 3–4 (verification ladder, diagnostic hover) are
24
+ * still proposals.
19
25
  */
20
26
  const meta = {
21
27
  title: 'Subsystem/ComponentGraph/NodeAnatomy',
@@ -42,14 +48,19 @@ const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
42
48
 
43
49
  function nodeProps(
44
50
  component: SubsystemComponent,
45
- opts?: { selected?: boolean; width?: number; height?: number },
46
- ): NodeProps<SubsystemGraphNode> {
51
+ opts?: {
52
+ selected?: boolean;
53
+ width?: number;
54
+ height?: number;
55
+ issue?: SubsystemNodeIssue;
56
+ },
57
+ ): NodeProps<Node<SubsystemGraphNodeData, 'subsystem-component'>> {
47
58
  return {
48
- data: { component },
59
+ data: { component, ...(opts?.issue ? { issue: opts.issue } : {}) },
49
60
  selected: opts?.selected ?? false,
50
61
  width: opts?.width ?? NODE_W,
51
62
  height: opts?.height ?? NODE_H,
52
- } as unknown as NodeProps<SubsystemGraphNode>;
63
+ } as unknown as NodeProps<Node<SubsystemGraphNodeData, 'subsystem-component'>>;
53
64
  }
54
65
 
55
66
  // ---------------------------------------------------------------------------
@@ -219,7 +230,7 @@ function ChannelCell({ channel }: { channel: Channel }) {
219
230
 
220
231
  const LEGEND: Array<[string, string]> = [
221
232
  ['border color', 'construct kind (Pierre palette) — or primary when a file is open (spotlight)'],
222
- ['border style', 'solid normally; dashed goldenrod when `proposed`'],
233
+ ['border style', 'solid normally; dashed goldenrod when `proposed`; dotted when the node has findings'],
223
234
  ['border width', '2px; 4px when selected or file-matched'],
224
235
  ['corners', '8px rounded; square for `external`'],
225
236
  ['left badge (top edge)', 'construct / framework stereotype / entityKind'],
@@ -290,14 +301,6 @@ function Legend() {
290
301
  type Severity = 'error' | 'info';
291
302
  type Rung = 'file' | 'symbol' | 'declaration' | 'type' | 'signature';
292
303
 
293
- const RUNG_ICON = {
294
- file: FileX,
295
- symbol: Search,
296
- declaration: MapPin,
297
- type: Shapes,
298
- signature: Sigma,
299
- } as const;
300
-
301
304
  const RUNG_LABEL: Record<Rung, string> = {
302
305
  file: 'file',
303
306
  symbol: 'symbol',
@@ -317,51 +320,13 @@ function IssueOverlayNode({
317
320
  rung: Rung;
318
321
  count?: number;
319
322
  }) {
320
- const { theme } = useTheme();
321
- const color =
322
- severity === 'error'
323
- ? (theme.colors.error ?? '#e5534b')
324
- : (theme.colors.info ?? '#0893d2');
325
- // Gaps (info) read hollow/dashed — "couldn't confirm"; errors solid.
326
- const dashed = severity === 'info';
327
- const Icon = RUNG_ICON[rung];
328
- const badgeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
323
+ // The diagnostics layer is wired into the production node — the border turns
324
+ // dotted (construct color kept) and the chip carries severity + rung + count.
329
325
  return (
330
326
  <div style={{ position: 'relative', width: NODE_W, height: NODE_H }}>
331
- <SubsystemComponentNode {...nodeProps(component)} />
332
- {/* severity ring — additive, outside the construct-colored border */}
333
- <div
334
- style={{
335
- position: 'absolute',
336
- inset: -5,
337
- borderRadius: 11,
338
- border: `2px ${dashed ? 'dashed' : 'solid'} ${color}`,
339
- pointerEvents: 'none',
340
- }}
327
+ <SubsystemComponentNode
328
+ {...nodeProps(component, { issue: { severity, rung, count } })}
341
329
  />
342
- {/* earliest-rung corner chip + count */}
343
- <div
344
- style={{
345
- position: 'absolute',
346
- right: -8,
347
- bottom: -7,
348
- display: 'inline-flex',
349
- alignItems: 'center',
350
- gap: 3,
351
- padding: '1px 6px',
352
- borderRadius: 999,
353
- border: `1.5px solid ${color}`,
354
- background: badgeBg,
355
- color,
356
- fontFamily: MONO,
357
- fontSize: 10,
358
- fontWeight: 600,
359
- lineHeight: 1.4,
360
- }}
361
- >
362
- <Icon size={11} color={color} />
363
- {count > 1 ? <span>{count}</span> : null}
364
- </div>
365
330
  </div>
366
331
  );
367
332
  }
@@ -454,7 +419,7 @@ const ISSUE_EXAMPLES: Array<{
454
419
  },
455
420
  ];
456
421
 
457
- /** Proposed diagnostics layer over real nodes — ring by severity, chip = earliest rung. */
422
+ /** Diagnostics layer over real nodes — dotted border + earliest-rung chip. */
458
423
  export const IssueOverlayProposal: Story = {
459
424
  render: () => {
460
425
  return (
@@ -470,20 +435,22 @@ export const IssueOverlayProposal: Story = {
470
435
  marginBottom: 10,
471
436
  }}
472
437
  >
473
- Proposed · not wired yet
438
+ Wired · data.issue
474
439
  </div>
475
440
  <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12, lineHeight: 1.7, color: '#b0b0b0' }}>
476
441
  <li>
477
- <b>Ring</b> (outside the construct border): <b>solid</b> = error (contradicts
478
- source), <b>dashed</b> = info/gap (couldn&apos;t confirm). Color = severity.
442
+ <b>Dotted border</b> (keeps the construct color) = the node has findings — a sibling of
443
+ <b> proposed</b>&apos;s dashed, kept distinct so the two don&apos;t read alike.
479
444
  </li>
480
445
  <li>
481
- <b>Corner chip</b> = the <i>earliest failing rung</i> by verification order (file →
482
- symbol → declaration → type → signature), with a count when a node has more than one.
446
+ <b>Corner chip</b> carries severity (<b>red</b> = error / contradicts source,{' '}
447
+ <b>amber</b> = info/gap / couldn&apos;t confirm) and the <i>earliest failing rung</i> by
448
+ verification order (file → symbol → declaration → type → signature), with a count when a
449
+ node has more than one.
483
450
  </li>
484
451
  <li>
485
- First cell is the clean node for contrast — no ring, no chip. Compose with selection,
486
- file-open spotlight, and dimming without touching the border.
452
+ First cell is the clean node for contrast — solid border, no chip. Severity rides on the
453
+ chip so it never competes with the construct palette.
487
454
  </li>
488
455
  </ul>
489
456
  </div>
@@ -526,7 +493,7 @@ function VerificationSquares({ statuses }: { statuses: RungStatus[] }) {
526
493
  const { theme } = useTheme();
527
494
  const success = theme.colors.success ?? '#2da44e';
528
495
  const error = theme.colors.error ?? '#e5534b';
529
- const info = theme.colors.info ?? '#0893d2';
496
+ const warning = theme.colors.warning ?? '#d4a017';
530
497
  const dim = theme.colors.border ?? '#444';
531
498
  const nodeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
532
499
  return (
@@ -549,7 +516,7 @@ function VerificationSquares({ statuses }: { statuses: RungStatus[] }) {
549
516
  if (status === 'failed') return <span key={i} style={{ ...base, background: error }} />;
550
517
  if (status === 'unconfirmed')
551
518
  return (
552
- <span key={i} style={{ ...base, boxShadow: `inset 0 0 0 1.5px ${info}` }} />
519
+ <span key={i} style={{ ...base, boxShadow: `inset 0 0 0 1.5px ${warning}` }} />
553
520
  );
554
521
  // not reached / skipped — a filled neutral so it stays legible on the node
555
522
  return <span key={i} style={{ ...base, background: dim }} />;
@@ -653,7 +620,7 @@ export const VerificationSteps: Story = {
653
620
  </div>
654
621
  <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12, lineHeight: 1.7, color: '#b0b0b0' }}>
655
622
  <li>
656
- <b>green</b> = verified, <b>red</b> = failed, <b>hollow blue</b> = unconfirmed (info),
623
+ <b>green</b> = verified, <b>red</b> = failed, <b>hollow amber</b> = unconfirmed (info),
657
624
  <b> grey</b> = skipped / not reached.
658
625
  </li>
659
626
  <li>
@@ -694,7 +661,7 @@ function IssueHoverBadge({ severity, label }: { severity: Severity; label: strin
694
661
  const color =
695
662
  severity === 'error'
696
663
  ? (theme.colors.error ?? '#e5534b')
697
- : (theme.colors.info ?? '#0893d2');
664
+ : (theme.colors.warning ?? '#d4a017');
698
665
  return (
699
666
  <div
700
667
  style={{
@@ -0,0 +1,261 @@
1
+ import '@xyflow/react/dist/style.css';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ReactFlowProvider, type NodeProps } from '@xyflow/react';
4
+ import { ThemeProvider, defaultEditorTheme, useTheme } from '@principal-ade/industry-theme';
5
+ import { SubsystemComponentNode } from '../../../subsystem/nodes';
6
+ import type { GraphifyComponentDetail } from '../../../graphify';
7
+ import type { SubsystemComponent, SubsystemGraphNode } from '../../../subsystem/model';
8
+
9
+ /**
10
+ * StoreValueType — proposal sketch (not wired yet).
11
+ *
12
+ * A store is a state declaration, so it should name and type itself like every
13
+ * other declaration. `SubsystemStoreDeclaration` has no field for a store's value
14
+ * type today; the value type lives in the **declaration panel** (see the
15
+ * `Subsystem/StoreValueTypeDeclaration` story), shown when the node is clicked —
16
+ * NOT as a node subtitle.
17
+ *
18
+ * title = the state declaration's name (the variable name) — already shipped
19
+ * badge = storage backing (memory / disk / db) — already shipped
20
+ * valueType = declaration panel only (no node channel)
21
+ *
22
+ * Cases also contrast what a wrong anchor renders: a store whose anchor is the
23
+ * accessor shows the accessor's name instead of the state's.
24
+ */
25
+ const meta = {
26
+ title: 'Subsystem/ComponentGraph/StoreValueType',
27
+ parameters: { layout: 'fullscreen' },
28
+ tags: ['autodocs'],
29
+ decorators: [
30
+ (Story) => (
31
+ <ThemeProvider theme={defaultEditorTheme}>
32
+ <ReactFlowProvider>
33
+ <Story />
34
+ </ReactFlowProvider>
35
+ </ThemeProvider>
36
+ ),
37
+ ],
38
+ } satisfies Meta;
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ const PURL = 'pkg:github/principal-ai/subsystem-modeling';
44
+ const NODE_W = 230;
45
+ const NODE_H = 92;
46
+ const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
47
+
48
+ /**
49
+ * A store component with the *proposed* `valueType` field. The field is not in
50
+ * the schema yet, so it is carried here as `declaration.valueType` (the shape we
51
+ * intend to land) and read directly by this story graph.
52
+ */
53
+ type ProposedStoreComponent = SubsystemComponent & {
54
+ declaration: GraphifyComponentDetail & { valueType?: string };
55
+ };
56
+
57
+ function nodeProps(
58
+ component: SubsystemComponent,
59
+ opts?: { selected?: boolean; width?: number; height?: number },
60
+ ): NodeProps<SubsystemGraphNode> {
61
+ return {
62
+ data: { component },
63
+ selected: opts?.selected ?? false,
64
+ width: opts?.width ?? NODE_W,
65
+ height: opts?.height ?? NODE_H,
66
+ } as unknown as NodeProps<SubsystemGraphNode>;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Fixtures — the 23-store audit shapes, rendered with the proposed channels.
71
+ // ---------------------------------------------------------------------------
72
+
73
+ const CASES: Array<{ label: string; note: string; component: ProposedStoreComponent }> = [
74
+ {
75
+ label: 'memory · declares its value type',
76
+ note: 'title = state name · subtitle = value type · badge = memory.',
77
+ component: {
78
+ alias: 'feeds',
79
+ name: 'feeds',
80
+ construct: 'store',
81
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
82
+ purl: PURL,
83
+ symbol: 'feeds',
84
+ declaration: {
85
+ kind: 'store',
86
+ storage: 'memory',
87
+ properties: [],
88
+ valueType: 'Map<string, OpencodeLiveFeedState>',
89
+ } as GraphifyComponentDetail & { valueType: string },
90
+ },
91
+ },
92
+ {
93
+ label: 'memory · closure-local (no addressable symbol)',
94
+ note: 'state is a factory-local Map; anchor is non-exact → agent-confirmed store.',
95
+ component: {
96
+ alias: 'landed',
97
+ name: 'landed',
98
+ construct: 'store',
99
+ file: 'packages/subsystems-studio/src/bun/maintainer-probe.ts',
100
+ purl: PURL,
101
+ symbol: 'landed',
102
+ declaration: {
103
+ kind: 'store',
104
+ storage: 'memory',
105
+ properties: [],
106
+ valueType: 'Map<string, number>',
107
+ } as GraphifyComponentDetail & { valueType: string },
108
+ },
109
+ },
110
+ {
111
+ label: 'memory · no value type (the gap)',
112
+ note: 'subtitle shows a muted `type: ?` — the completeness finding made visible.',
113
+ component: {
114
+ alias: 'tabs',
115
+ name: 'tabs',
116
+ construct: 'store',
117
+ file: 'packages/subsystems-studio/src/bun/index.ts',
118
+ purl: PURL,
119
+ symbol: 'tabs',
120
+ declaration: {
121
+ kind: 'store',
122
+ storage: 'memory',
123
+ properties: [],
124
+ } as GraphifyComponentDetail,
125
+ },
126
+ },
127
+ {
128
+ label: 'disk · value type',
129
+ note: 'run log on the filesystem — badge reads `disk`.',
130
+ component: {
131
+ alias: 'runs',
132
+ name: 'runs',
133
+ construct: 'store',
134
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-runs.ts',
135
+ purl: PURL,
136
+ symbol: 'runs',
137
+ declaration: {
138
+ kind: 'store',
139
+ storage: 'disk',
140
+ properties: [],
141
+ valueType: 'SubsystemModelRun[]',
142
+ } as GraphifyComponentDetail & { valueType: string },
143
+ },
144
+ },
145
+ {
146
+ label: 'db · row / members shape',
147
+ note: 'table name as title; value type is the row shape (members[]).',
148
+ component: {
149
+ alias: 'session_v2',
150
+ name: 'session_v2',
151
+ construct: 'store',
152
+ file: 'packages/subsystems-studio/src/bun/maintain-sessions.ts',
153
+ purl: PURL,
154
+ symbol: 'session_v2',
155
+ declaration: {
156
+ kind: 'store',
157
+ storage: 'external',
158
+ properties: [
159
+ { name: 'id', type: 'text' },
160
+ { name: 'title', type: 'text' },
161
+ { name: 'agent', type: 'text' },
162
+ { name: 'time_created', type: 'integer' },
163
+ ],
164
+ } as GraphifyComponentDetail,
165
+ },
166
+ },
167
+ ];
168
+
169
+ /**
170
+ * Renders the component node as it should appear: title = the state
171
+ * declaration's name, badge = storage backing. The value type is intentionally
172
+ * NOT shown here — it belongs to the declaration panel (see
173
+ * `Subsystem/StoreValueTypeDeclaration`), shown when the node is clicked.
174
+ */
175
+ function StoreNode({ component }: { component: ProposedStoreComponent }) {
176
+ return (
177
+ <div style={{ position: 'relative', width: NODE_W, height: NODE_H }}>
178
+ <SubsystemComponentNode {...nodeProps(component)} />
179
+ </div>
180
+ );
181
+ }
182
+
183
+ function Cell({
184
+ label,
185
+ note,
186
+ component,
187
+ }: {
188
+ label: string;
189
+ note: string;
190
+ component: ProposedStoreComponent;
191
+ }) {
192
+ const { theme } = useTheme();
193
+ const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
194
+ const valueType = component.declaration?.valueType;
195
+ return (
196
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 280 }}>
197
+ <div style={{ fontFamily: MONO, fontSize: 11, color: muted }}>{label}</div>
198
+ <div style={{ position: 'relative', width: NODE_W, height: NODE_H, margin: '14px 0 16px' }}>
199
+ <StoreNode component={component} />
200
+ </div>
201
+ <div style={{ fontSize: 10, fontFamily: MONO, color: muted, lineHeight: 1.5 }}>
202
+ {note}
203
+ <br />
204
+ panel: {valueType ?? '(no value type — gap)'}
205
+ </div>
206
+ </div>
207
+ );
208
+ }
209
+
210
+ /** Store nodes: title = state name, badge = storage. Value type lives in the panel. */
211
+ export const StoreValueType: Story = {
212
+ render: () => (
213
+ <div style={{ padding: 32, minHeight: '100vh', boxSizing: 'border-box' }}>
214
+ <Header
215
+ title="Store nodes"
216
+ lines={[
217
+ 'A store is a state declaration, so its node names it: title = the state declaration (the variable name). The storage badge (memory / disk / db) shows how it persists.',
218
+ 'The value type is not a node channel — it belongs to the declaration panel, shown when the node is clicked (see Subsystem/StoreValueTypeDeclaration).',
219
+ 'The caption under each node shows what the declaration panel renders for it.',
220
+ ]}
221
+ />
222
+ <div
223
+ style={{
224
+ display: 'flex',
225
+ flexWrap: 'wrap',
226
+ gap: '40px 28px',
227
+ alignItems: 'flex-start',
228
+ marginTop: 28,
229
+ }}
230
+ >
231
+ {CASES.map((c) => (
232
+ <Cell key={c.component.alias} label={c.label} note={c.note} component={c.component} />
233
+ ))}
234
+ </div>
235
+ </div>
236
+ ),
237
+ };
238
+
239
+ function Header({ title, lines }: { title: string; lines: string[] }) {
240
+ return (
241
+ <div style={{ maxWidth: 760 }}>
242
+ <div
243
+ style={{
244
+ fontSize: 12,
245
+ fontFamily: MONO,
246
+ textTransform: 'uppercase',
247
+ letterSpacing: 0.4,
248
+ color: '#a0a0a0',
249
+ marginBottom: 10,
250
+ }}
251
+ >
252
+ {title}
253
+ </div>
254
+ <ul style={{ margin: 0, paddingLeft: 18, fontSize: 12, lineHeight: 1.7, color: '#b0b0b0' }}>
255
+ {lines.map((l) => (
256
+ <li key={l}>{l}</li>
257
+ ))}
258
+ </ul>
259
+ </div>
260
+ );
261
+ }
@@ -0,0 +1,182 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
3
+ import { generateDeclarationString } from '../../subsystem/formatDeclaration';
4
+ import type { SubsystemComponent } from '../../subsystem/model';
5
+
6
+ /**
7
+ * StoreValueType — how a store's declared value type renders in the
8
+ * **declaration panel** (the click panel), not on the node.
9
+ *
10
+ * A store is a state declaration, so it declares its type the way every other
11
+ * declaration does: `declaration.valueType` (`Map<string, FeedState>` for a state
12
+ * block, a row type for a table), orthogonal to `declaration.storage`. The
13
+ * rendered string comes from the real `generateStore` in
14
+ * `formatDeclaration.ts` — not a sketch.
15
+ *
16
+ * The node itself is unchanged: title = the state declaration's name, badge =
17
+ * storage backing. The value type is panel-only.
18
+ */
19
+ const meta = {
20
+ title: 'Subsystem/StoreValueTypeDeclaration',
21
+ parameters: { layout: 'padded' },
22
+ tags: ['autodocs'],
23
+ decorators: [
24
+ (Story) => (
25
+ <ThemeProvider theme={defaultEditorTheme}>
26
+ <div style={{ maxWidth: '80ch', margin: '0 auto' }}>
27
+ <Story />
28
+ </div>
29
+ </ThemeProvider>
30
+ ),
31
+ ],
32
+ } satisfies Meta;
33
+ export default meta;
34
+
35
+ type Case = {
36
+ label: string;
37
+ note: string;
38
+ component: SubsystemComponent;
39
+ };
40
+
41
+ const CASES: Case[] = [
42
+ {
43
+ label: 'memory · value type, no named members',
44
+ note: 'The store declares what it holds; nothing else to render.',
45
+ component: {
46
+ id: 'feeds',
47
+ name: 'feeds',
48
+ construct: 'store',
49
+ symbol: 'feeds',
50
+ file: 'packages/subsystems-studio/src/bun/opencode-v2-live.ts',
51
+ purl: 'pkg:github/principal-ai/subsystem-modeling',
52
+ declaration: {
53
+ kind: 'store',
54
+ storage: 'memory',
55
+ properties: [],
56
+ valueType: 'Map<string, OpencodeLiveFeedState>',
57
+ },
58
+ },
59
+ },
60
+ {
61
+ label: 'memory · closure-local state',
62
+ note: 'State created inside a factory, reached through an accessor. Still a declaration.',
63
+ component: {
64
+ id: 'landed',
65
+ name: 'landed',
66
+ construct: 'store',
67
+ symbol: 'landed',
68
+ file: 'packages/subsystems-studio/src/bun/maintainer-probe.ts',
69
+ purl: 'pkg:github/principal-ai/subsystem-modeling',
70
+ declaration: {
71
+ kind: 'store',
72
+ storage: 'memory',
73
+ properties: [],
74
+ valueType: 'Map<string, number>',
75
+ },
76
+ },
77
+ },
78
+ {
79
+ label: 'disk · value type + named members',
80
+ note: 'Members render as a typed body under the value type.',
81
+ component: {
82
+ id: 'runs',
83
+ name: 'runs',
84
+ construct: 'store',
85
+ symbol: 'runs',
86
+ file: 'packages/subsystems-studio/src/bun/subsystem-model-runs.ts',
87
+ purl: 'pkg:github/principal-ai/subsystem-modeling',
88
+ declaration: {
89
+ kind: 'store',
90
+ storage: 'disk',
91
+ valueType: 'SubsystemModelRun[]',
92
+ properties: [{ name: 'runs', type: 'SubsystemModelRun[]' }],
93
+ },
94
+ },
95
+ },
96
+ {
97
+ label: 'memory · no value type → the gap',
98
+ note: 'Says so explicitly — the completeness finding reads this back.',
99
+ component: {
100
+ id: 'tabs',
101
+ name: 'tabs',
102
+ construct: 'store',
103
+ symbol: 'tabs',
104
+ file: 'packages/subsystems-studio/src/bun/index.ts',
105
+ purl: 'pkg:github/principal-ai/subsystem-modeling',
106
+ declaration: { kind: 'store', storage: 'memory', properties: [] },
107
+ },
108
+ },
109
+ {
110
+ label: 'db · row shape, no scalar value type',
111
+ note: 'A table names its row/record type; columns stay as members.',
112
+ component: {
113
+ id: 'session_v2',
114
+ name: 'session_v2',
115
+ construct: 'store',
116
+ symbol: 'session_v2',
117
+ file: 'packages/subsystems-studio/src/bun/maintain-sessions.ts',
118
+ purl: 'pkg:github/principal-ai/subsystem-modeling',
119
+ declaration: {
120
+ kind: 'store',
121
+ storage: 'external',
122
+ properties: [
123
+ { name: 'id', type: 'text' },
124
+ { name: 'title', type: 'text' },
125
+ { name: 'agent', type: 'text' },
126
+ { name: 'time_created', type: 'integer' },
127
+ ],
128
+ },
129
+ },
130
+ },
131
+ ];
132
+
133
+ function Panel({ component }: { component: SubsystemComponent }) {
134
+ return (
135
+ <pre
136
+ style={{
137
+ margin: 0,
138
+ padding: '10px 12px',
139
+ borderRadius: 6,
140
+ background: 'rgba(0,0,0,0.35)',
141
+ border: '1px solid rgba(255,255,255,0.08)',
142
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
143
+ fontSize: 12,
144
+ lineHeight: 1.5,
145
+ color: '#e5e7eb',
146
+ whiteSpace: 'pre-wrap',
147
+ overflowX: 'auto',
148
+ }}
149
+ >
150
+ {generateDeclarationString(component)}
151
+ </pre>
152
+ );
153
+ }
154
+
155
+ export const StoreDeclaration: StoryObj = {
156
+ render: () => (
157
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 26 }}>
158
+ <div style={{ fontSize: 12, color: '#b0b0b0', lineHeight: 1.7 }}>
159
+ <strong>Rendered by the real <code>generateStore</code>.</strong> A store declares
160
+ its type the way every other declaration does — <code>declaration.valueType</code> —
161
+ and the value type is panel-only, not a node channel. The node stays{' '}
162
+ <code>name → title</code>, <code>storage → badge</code>.
163
+ </div>
164
+ {CASES.map((c) => (
165
+ <div key={c.component.id}>
166
+ <div
167
+ style={{
168
+ fontFamily: 'ui-monospace, monospace',
169
+ fontSize: 12,
170
+ color: '#f3f4f6',
171
+ marginBottom: 6,
172
+ }}
173
+ >
174
+ {c.label}
175
+ </div>
176
+ <div style={{ fontSize: 11, color: '#9ca3af', marginBottom: 8 }}>{c.note}</div>
177
+ <Panel component={c.component} />
178
+ </div>
179
+ ))}
180
+ </div>
181
+ ),
182
+ };