@principal-ai/subsystems-react 0.49.0 → 0.51.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 (97) hide show
  1. package/dist/index.d.ts +8 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreFileView.d.ts +1 -1
  6. package/dist/pierre/PierreFileView.d.ts.map +1 -1
  7. package/dist/pierre/PierreFileView.js +2 -1
  8. package/dist/pierre/PierreFileView.js.map +1 -1
  9. package/dist/pierre/PierreSnippetView.d.ts +1 -1
  10. package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
  11. package/dist/pierre/PierreSnippetView.js +2 -1
  12. package/dist/pierre/PierreSnippetView.js.map +1 -1
  13. package/dist/pierre/PierreTrailCodeView.d.ts +7 -1
  14. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  15. package/dist/pierre/PierreTrailCodeView.js +71 -18
  16. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  17. package/dist/pierre/pierreBackground.d.ts +36 -2
  18. package/dist/pierre/pierreBackground.d.ts.map +1 -1
  19. package/dist/pierre/pierreBackground.js +40 -13
  20. package/dist/pierre/pierreBackground.js.map +1 -1
  21. package/dist/pierre/visibleStep.d.ts +48 -0
  22. package/dist/pierre/visibleStep.d.ts.map +1 -0
  23. package/dist/pierre/visibleStep.js +53 -0
  24. package/dist/pierre/visibleStep.js.map +1 -0
  25. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts +95 -0
  26. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts.map +1 -0
  27. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js +340 -0
  28. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js.map +1 -0
  29. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts +4 -0
  30. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts.map +1 -1
  31. package/dist/stories/Subsystem/C4Graph/c4Fixture.js +7334 -1
  32. package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +1 -1
  33. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts +2 -0
  34. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts.map +1 -0
  35. package/dist/stories/Subsystem/C4Graph/generateFixture.js +145 -0
  36. package/dist/stories/Subsystem/C4Graph/generateFixture.js.map +1 -0
  37. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  38. package/dist/subsystem/C4Graph.js +16 -60
  39. package/dist/subsystem/C4Graph.js.map +1 -1
  40. package/dist/subsystem/C4NodeCard.d.ts +84 -0
  41. package/dist/subsystem/C4NodeCard.d.ts.map +1 -0
  42. package/dist/subsystem/C4NodeCard.js +229 -0
  43. package/dist/subsystem/C4NodeCard.js.map +1 -0
  44. package/dist/subsystem/DiagnosticToggle.d.ts +8 -1
  45. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
  46. package/dist/subsystem/DiagnosticToggle.js +40 -6
  47. package/dist/subsystem/DiagnosticToggle.js.map +1 -1
  48. package/dist/subsystem/FileDrawer.d.ts +10 -1
  49. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  50. package/dist/subsystem/FileDrawer.js +61 -40
  51. package/dist/subsystem/FileDrawer.js.map +1 -1
  52. package/dist/subsystem/SubsystemComponentGraph.d.ts +6 -0
  53. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  54. package/dist/subsystem/SubsystemComponentGraph.js +21 -10
  55. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  56. package/dist/subsystem/c4Associations.d.ts +186 -0
  57. package/dist/subsystem/c4Associations.d.ts.map +1 -0
  58. package/dist/subsystem/c4Associations.js +284 -0
  59. package/dist/subsystem/c4Associations.js.map +1 -0
  60. package/dist/subsystem/model.d.ts +25 -1
  61. package/dist/subsystem/model.d.ts.map +1 -1
  62. package/dist/subsystem/model.js +15 -0
  63. package/dist/subsystem/model.js.map +1 -1
  64. package/dist/subsystem/nodes.d.ts.map +1 -1
  65. package/dist/subsystem/nodes.js +78 -7
  66. package/dist/subsystem/nodes.js.map +1 -1
  67. package/dist/subsystem/toC4.d.ts +119 -0
  68. package/dist/subsystem/toC4.d.ts.map +1 -1
  69. package/dist/subsystem/toC4.js +95 -5
  70. package/dist/subsystem/toC4.js.map +1 -1
  71. package/package.json +2 -2
  72. package/src/index.ts +39 -0
  73. package/src/pierre/PierreFileView.tsx +2 -1
  74. package/src/pierre/PierreSnippetView.tsx +2 -1
  75. package/src/pierre/PierreTrailCodeView.tsx +81 -22
  76. package/src/pierre/pierreBackground.ts +62 -12
  77. package/src/pierre/visibleStep.test.ts +108 -0
  78. package/src/pierre/visibleStep.ts +76 -0
  79. package/src/stories/Subsystem/C4Graph/C4Associations.stories.tsx +273 -0
  80. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +325 -0
  81. package/src/stories/Subsystem/C4Graph/c4Associations.fixture.ts +344 -0
  82. package/src/stories/Subsystem/C4Graph/c4Fixture.ts +7337 -4
  83. package/src/stories/Subsystem/C4Graph/generateFixture.ts +161 -0
  84. package/src/stories/Subsystem/ComponentGraph/Deprecated.stories.tsx +358 -0
  85. package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +27 -2
  86. package/src/stories/Subsystem/ComponentGraph/{Flows.stories.tsx → Trails.stories.tsx} +10 -4
  87. package/src/subsystem/C4Graph.tsx +51 -92
  88. package/src/subsystem/C4NodeCard.test.ts +196 -0
  89. package/src/subsystem/C4NodeCard.tsx +307 -0
  90. package/src/subsystem/DiagnosticToggle.tsx +58 -4
  91. package/src/subsystem/FileDrawer.tsx +112 -63
  92. package/src/subsystem/SubsystemComponentGraph.tsx +29 -5
  93. package/src/subsystem/c4Associations.test.ts +484 -0
  94. package/src/subsystem/c4Associations.ts +398 -0
  95. package/src/subsystem/model.ts +34 -1
  96. package/src/subsystem/nodes.tsx +124 -5
  97. package/src/subsystem/toC4.ts +210 -5
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Regenerate BOTH C4 fixtures from one composition of the live store, so the
3
+ * document and its association set can never drift apart.
4
+ *
5
+ * Run from packages/subsystems-react:
6
+ * bun run src/stories/Subsystem/C4Graph/generateFixture.ts
7
+ *
8
+ * The two files it writes are paired:
9
+ * c4Fixture.ts the composed document the graph renders
10
+ * c4Associations.fixture.ts proposed associations keyed off that document
11
+ *
12
+ * They must be generated together. A document from one snapshot and an
13
+ * association set from another produces merges that silently do nothing.
14
+ */
15
+ import * as fs from 'node:fs';
16
+ import * as os from 'node:os';
17
+ import * as path from 'node:path';
18
+ import { toC4 } from '../../../subsystem/toC4';
19
+ import { buildAssociations, deriveConcerns, suggestMerges } from '../../../subsystem/c4Associations';
20
+ import type { SubsystemModelDocument, SubsystemComponent } from '../../../subsystem/model';
21
+
22
+ const REPO = 'pkg:github/principal-ai/subsystem-modeling';
23
+ const SYSTEM_LABEL = 'Subsystem Modeling';
24
+ const dir = path.join(os.homedir(), '.principal/subsystem-models');
25
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json') && f !== '_index.json');
26
+
27
+ /**
28
+ * Compose every stored model for one repo key into a single document.
29
+ *
30
+ * One canonical component per (purl#symbol); aliases rebased to `c0..cn` so
31
+ * two models cannot collide in one alias space.
32
+ */
33
+ const byId = new Map<string, SubsystemComponent>();
34
+ const steps: NonNullable<SubsystemModelDocument['trails']>[number]['steps'] = [];
35
+ let n = 0;
36
+
37
+ for (const f of files) {
38
+ const d = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) as SubsystemModelDocument;
39
+ const aliasMap = new Map<string, string>();
40
+ for (const c of d.components ?? []) {
41
+ if ((c.purl ?? '').split('#')[0] !== REPO) continue;
42
+ const key = `${c.purl!.split('#')[1] ?? ''}::${c.symbol ?? c.alias}`;
43
+ if (!byId.has(key)) byId.set(key, { ...c, alias: `c${n++}` });
44
+ aliasMap.set(c.alias, key);
45
+ }
46
+ for (const t of d.trails ?? []) {
47
+ for (const s of t.steps ?? []) {
48
+ const a = aliasMap.get(s.from);
49
+ const b = aliasMap.get(s.to);
50
+ if (!a || !b || a === b) continue;
51
+ steps.push({
52
+ from: byId.get(a)!.alias,
53
+ to: byId.get(b)!.alias,
54
+ mechanism: s.mechanism,
55
+ // Keep the site so the fixture is a faithful document, not a shape.
56
+ file: s.file,
57
+ line: s.line,
58
+ purl: s.purl,
59
+ symbol: s.symbol,
60
+ ...(s.annotation ? { annotation: s.annotation } : {}),
61
+ });
62
+ }
63
+ }
64
+ }
65
+
66
+ // The React-local document type carries components + trails only; the store
67
+ // record wraps it with title/description.
68
+ const document: SubsystemModelDocument = {
69
+ components: [...byId.values()],
70
+ trails: [{ id: 'all', title: 'all trails', steps }],
71
+ };
72
+
73
+ // --- Roll up once, then derive concerns off the same rollup -------------
74
+ const raw = toC4(document, { view: 'container', systemLabel: SYSTEM_LABEL });
75
+
76
+ const memberOf = new Map<string, SubsystemComponent>();
77
+ for (const c of byId.values()) memberOf.set(c.alias, c);
78
+
79
+ const rollup = raw.nodes
80
+ .filter((x) => x.kind === 'container')
81
+ .map((x) => {
82
+ const members = x.members.map((a) => memberOf.get(a)!).filter(Boolean);
83
+ const frameworks = [...new Set(members.map((m) => m.framework).filter(Boolean) as string[])];
84
+ return { key: x.key ?? x.id, members, frameworks };
85
+ });
86
+
87
+ const set = buildAssociations({ repoKey: REPO, rollup, author: 'c4-proposer' });
88
+ const merges = suggestMerges(rollup.map((r) => ({ key: r.key, memberCount: r.members.length })));
89
+ const concerns = rollup.flatMap((r) => deriveConcerns(r.key, r.members, { frameworks: r.frameworks }));
90
+
91
+ /** Every derived key, so a story can assert nothing was invented. */
92
+ const derivedKeys = rollup.map((r) => r.key).sort();
93
+
94
+ /** Components behind each derived key — the evidence that picks a merge survivor. */
95
+ const memberCounts = Object.fromEntries(
96
+ rollup.map((r) => [r.key, r.members.length]).sort(([a], [b]) => String(a).localeCompare(String(b))),
97
+ );
98
+
99
+ // --- Write the document ------------------------------------------------
100
+ const docSrc = `// GENERATED from live store data by generateFixture.ts — do not hand-edit.
101
+ //
102
+ // Composite of ${files.length} stored subsystem models for ${REPO},
103
+ // joined on purl#symbol (one canonical component per real declaration).
104
+ //
105
+ // Regenerate together with c4Associations.fixture.ts — the association set is
106
+ // keyed off the container keys this document derives. Generating them
107
+ // separately produces merges that silently do nothing.
108
+
109
+ import type { SubsystemModelDocument } from "../../../subsystem/model";
110
+
111
+ /** Container keys this document is expected to derive. */
112
+ export const c4DerivedKeys = ${JSON.stringify(derivedKeys, null, 2)} as const;
113
+
114
+ /** Components behind each derived key — decides which side of a merge survives. */
115
+ export const c4MemberCounts: Record<string, number> = ${JSON.stringify(memberCounts, null, 2)};
116
+
117
+ export const c4FixtureModel: SubsystemModelDocument = ${JSON.stringify(document, null, 1)};
118
+ `;
119
+ fs.writeFileSync(path.join(__dirname, 'c4Fixture.ts'), docSrc);
120
+
121
+ // --- Write the associations -------------------------------------------
122
+ const assocSrc = `// GENERATED from live store data by generateFixture.ts — do not hand-edit.
123
+ //
124
+ // Derived from the SAME composition as c4Fixture.ts: ${byId.size} components
125
+ // → ${rollup.length} container keys → ${set.associations.length} associations.
126
+ //
127
+ // Every association is state:'proposed'. None of it is accepted — accepting is
128
+ // a human's decision, and this story exists to make that decision visible.
129
+
130
+ import type { C4Association } from "../../../subsystem/toC4";
131
+
132
+ /** Container keys whose trailing role matches — candidates for a merge. */
133
+ export const C4_MERGE_CANDIDATES = ${JSON.stringify(merges, null, 2)} as const;
134
+
135
+ /** Every mechanical concern, for the review panel. */
136
+ export const C4_CONCERNS = ${JSON.stringify(concerns, null, 2)} as const;
137
+
138
+ export const c4Associations: C4Association[] = ${JSON.stringify(set.associations, null, 2)};
139
+ `;
140
+ fs.writeFileSync(path.join(__dirname, 'c4Associations.fixture.ts'), assocSrc);
141
+
142
+ // --- Verify the pair actually agrees ----------------------------------
143
+ const reDerived = toC4(
144
+ JSON.parse(JSON.stringify(document)),
145
+ { view: 'container', systemLabel: SYSTEM_LABEL, associations: set.associations },
146
+ );
147
+ const claimed = new Set(set.associations.flatMap((a) => a.sourceKeys));
148
+ const orphans = derivedKeys.filter((k) => !claimed.has(k));
149
+
150
+ console.log('wrote c4Fixture.ts + c4Associations.fixture.ts');
151
+ console.log(' models composed :', files.length);
152
+ console.log(' components :', byId.size);
153
+ console.log(' container keys :', rollup.length);
154
+ console.log(' associations :', set.associations.length);
155
+ console.log(' merge candidates:', merges.length);
156
+ console.log(' concerns :', concerns.length);
157
+ console.log(' types :', JSON.stringify(
158
+ set.associations.reduce<Record<string, number>>((a, x) => ({ ...a, [x.type]: (a[x.type] ?? 0) + 1 }), {}),
159
+ ));
160
+ console.log(' containers drawn:', reDerived.nodes.filter((x) => x.kind === 'container').length);
161
+ if (orphans.length > 0) console.warn(' !! derived keys with no association:', orphans);
@@ -0,0 +1,358 @@
1
+ import React, { useState } from 'react';
2
+ import '@xyflow/react/dist/style.css';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ReactFlowProvider, type NodeProps } from '@xyflow/react';
5
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
6
+ import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
7
+ import { SubsystemComponentNode } from '../../../subsystem/nodes';
8
+ import type {
9
+ SubsystemComponent,
10
+ SubsystemGraphNode,
11
+ } from '../../../subsystem/model';
12
+ import { trailFromSteps } from './fixtures';
13
+
14
+ /**
15
+ * `deprecated` is the inverse of `proposed`: the claim was real, its source is
16
+ * gone upstream. Mirrors Proposed.stories.tsx so the two temporary states read
17
+ * as siblings — proposed = not built yet, deprecated = built once, removed since.
18
+ */
19
+ const meta = {
20
+ title: 'Subsystem/ComponentGraph/Deprecated',
21
+ parameters: { layout: 'fullscreen' },
22
+ tags: ['autodocs'],
23
+ decorators: [
24
+ (Story) => (
25
+ <ThemeProvider theme={defaultEditorTheme}>
26
+ <ReactFlowProvider>
27
+ <Story />
28
+ </ReactFlowProvider>
29
+ </ThemeProvider>
30
+ ),
31
+ ],
32
+ } satisfies Meta;
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+
37
+ const PURL = 'pkg:github/principal-ai/subsystem-modeling';
38
+
39
+ function nodeProps(c: SubsystemComponent): NodeProps<SubsystemGraphNode> {
40
+ return {
41
+ data: { component: c },
42
+ selected: false,
43
+ width: 230,
44
+ height: 84,
45
+ } as unknown as NodeProps<SubsystemGraphNode>;
46
+ }
47
+
48
+ /** Side-by-side single-node spotlights — deprecated vs its live neighbours. */
49
+ function NodeSpotlight({
50
+ label,
51
+ component,
52
+ }: {
53
+ label: string;
54
+ component: SubsystemComponent;
55
+ }) {
56
+ return (
57
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: 260 }}>
58
+ <div
59
+ style={{
60
+ fontFamily: 'ui-monospace, monospace',
61
+ fontSize: 11,
62
+ color: '#888',
63
+ minHeight: 32,
64
+ }}
65
+ >
66
+ {label}
67
+ </div>
68
+ <div style={{ position: 'relative', height: 110, width: 240 }}>
69
+ <SubsystemComponentNode {...nodeProps(component)} />
70
+ </div>
71
+ <pre
72
+ style={{
73
+ margin: 0,
74
+ fontSize: 10,
75
+ fontFamily: 'ui-monospace, monospace',
76
+ background: '#1a1a1a',
77
+ color: '#ccc',
78
+ padding: 8,
79
+ borderRadius: 4,
80
+ overflow: 'auto',
81
+ }}
82
+ >
83
+ {JSON.stringify(
84
+ {
85
+ construct: component.construct,
86
+ deprecated: component.deprecated || undefined,
87
+ removedIn: component.removedIn,
88
+ role: component.role,
89
+ symbol: component.symbol || undefined,
90
+ file: component.file || undefined,
91
+ },
92
+ null,
93
+ 2,
94
+ )}
95
+ </pre>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ const spotlightRows: Array<{ label: string; component: SubsystemComponent }> = [
101
+ {
102
+ label: 'live function (baseline)',
103
+ component: {
104
+ alias: 'live-fn',
105
+ name: 'auditSubsystemModel',
106
+ construct: 'function',
107
+ file: 'src/bun/verify-subsystem-component.ts',
108
+ purl: PURL,
109
+ symbol: 'auditSubsystemModel',
110
+ purpose: 'Deterministic dry-run audit of a stored model',
111
+ },
112
+ },
113
+ {
114
+ label: 'deprecated + removedIn — slate border, badge',
115
+ component: {
116
+ alias: 'dead-fn',
117
+ name: 'auditTopologyRelations',
118
+ construct: 'function',
119
+ file: 'src/bun/topology-audit.ts',
120
+ purl: PURL,
121
+ symbol: 'auditTopologyRelations',
122
+ purpose: 'Verifies authored relations[] endpoints',
123
+ deprecated: true,
124
+ removedIn: {
125
+ commit: 'e168afc',
126
+ reason: 'Remove topology relations; nest module boundaries by path',
127
+ },
128
+ },
129
+ },
130
+ {
131
+ label: 'deprecated, no removedIn (bare marker)',
132
+ component: {
133
+ alias: 'dead-bare',
134
+ name: 'graphifyHasRelationBetween',
135
+ construct: 'function',
136
+ file: 'src/bun/topology-audit.ts',
137
+ purl: PURL,
138
+ symbol: 'graphifyHasRelationBetween',
139
+ purpose: 'Relation predicate over the graphify edge set',
140
+ deprecated: true,
141
+ },
142
+ },
143
+ {
144
+ label: 'deprecated + role: entry (badge shows deprecated only)',
145
+ component: {
146
+ alias: 'dead-entry',
147
+ name: 'topologyRelationsAudit',
148
+ construct: 'function',
149
+ file: 'src/bun/topology-audit.ts',
150
+ purl: PURL,
151
+ symbol: 'topologyRelationsAudit',
152
+ purpose: 'Audit entry for relation endpoints',
153
+ deprecated: true,
154
+ role: 'entry',
155
+ },
156
+ },
157
+ {
158
+ label: 'proposed (the sibling temporary state)',
159
+ component: {
160
+ alias: 'prop-fn',
161
+ name: 'OpenCodeV2Lifecycle',
162
+ construct: 'function',
163
+ file: '',
164
+ purl: PURL,
165
+ symbol: 'OpenCodeV2Lifecycle',
166
+ purpose: 'Proposed Studio host lifecycle',
167
+ proposed: true,
168
+ },
169
+ },
170
+ ];
171
+
172
+ /** Spotlights: slate deprecated badge + border vs live, proposed, and entry. */
173
+ export const Spotlights: Story = {
174
+ render: () => (
175
+ <div
176
+ style={{
177
+ padding: 24,
178
+ display: 'flex',
179
+ flexWrap: 'wrap',
180
+ gap: 28,
181
+ alignItems: 'flex-start',
182
+ minHeight: '100vh',
183
+ boxSizing: 'border-box',
184
+ }}
185
+ >
186
+ {spotlightRows.map((row) => (
187
+ <NodeSpotlight key={row.component.alias} {...row} />
188
+ ))}
189
+ </div>
190
+ ),
191
+ };
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Full graph: live audit calling a deleted topology audit, which called two
195
+ // more deleted helpers. Mirrors the real wedged model.
196
+ // ---------------------------------------------------------------------------
197
+ const deprecatedComponents: SubsystemComponent[] = [
198
+ {
199
+ alias: 'audit',
200
+ name: 'auditSubsystemModel',
201
+ construct: 'function',
202
+ file: 'src/bun/verify-subsystem-component.ts',
203
+ purl: PURL,
204
+ symbol: 'auditSubsystemModel',
205
+ purpose: 'Deterministic dry-run audit of a stored model',
206
+ role: 'entry',
207
+ },
208
+ {
209
+ alias: 'topology',
210
+ name: 'auditTopologyRelations',
211
+ construct: 'function',
212
+ file: 'src/bun/topology-audit.ts',
213
+ purl: PURL,
214
+ symbol: 'auditTopologyRelations',
215
+ purpose: 'Orchestrates relations[] endpoint checks',
216
+ deprecated: true,
217
+ removedIn: {
218
+ commit: 'e168afc',
219
+ reason: 'Remove topology relations; nest module boundaries by path',
220
+ },
221
+ },
222
+ {
223
+ alias: 'topo-relation',
224
+ name: 'graphifyHasRelationBetween',
225
+ construct: 'function',
226
+ file: 'src/bun/topology-audit.ts',
227
+ purl: PURL,
228
+ symbol: 'graphifyHasRelationBetween',
229
+ purpose: 'True when Graphify records a relation between two nodes',
230
+ deprecated: true,
231
+ removedIn: {
232
+ commit: 'e168afc',
233
+ reason: 'Remove topology relations; nest module boundaries by path',
234
+ },
235
+ },
236
+ {
237
+ alias: 'topo-hints',
238
+ name: 'graphifyHasRelationTowardHints',
239
+ construct: 'function',
240
+ file: 'src/bun/topology-audit.ts',
241
+ purl: PURL,
242
+ symbol: 'graphifyHasRelationTowardHints',
243
+ purpose: 'Relation hints directed at a component',
244
+ deprecated: true,
245
+ removedIn: { commit: 'e168afc' },
246
+ },
247
+ {
248
+ alias: 'boundary',
249
+ name: 'auditBoundaryFields',
250
+ construct: 'function',
251
+ file: 'src/bun/boundary-audit.ts',
252
+ purl: PURL,
253
+ symbol: 'auditBoundaryFields',
254
+ purpose: 'Package/module containment + process checks',
255
+ },
256
+ ];
257
+
258
+ const deprecatedTrails = [
259
+ trailFromSteps('audit-steps', 'Audit (dry-run, graphify-backed)', [
260
+ ['audit', 'topology', 'calls', 'src/bun/verify-subsystem-component.ts', 1],
261
+ ['audit', 'boundary', 'calls', 'src/bun/verify-subsystem-component.ts', 1],
262
+ ['topology', 'topo-relation', 'calls', 'src/bun/topology-audit.ts', 1],
263
+ ['topology', 'topo-hints', 'calls', 'src/bun/topology-audit.ts', 1],
264
+ ]),
265
+ ];
266
+
267
+ function DeprecatedDemo() {
268
+ const [selected, setSelected] = useState<string | null>(null);
269
+ return (
270
+ <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
271
+ <SubsystemComponentGraph
272
+ components={deprecatedComponents}
273
+ trails={deprecatedTrails}
274
+ onSelect={(id) => setSelected(id)}
275
+ />
276
+ <div style={{ marginTop: 8, padding: '0 12px', fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
277
+ Click a slate `deprecated` badge to open the removal provenance
278
+ {selected ? ` · selected: ${selected}` : ''}
279
+ </div>
280
+ </div>
281
+ );
282
+ }
283
+
284
+ /** Full graph: the deleted topology subtree still wired into a live audit. */
285
+ export const RemovedSubtree: Story = {
286
+ render: () => <DeprecatedDemo />,
287
+ };
288
+
289
+ /**
290
+ * The popover states, isolated — every badge variant the popover can render.
291
+ * `Spotlights` shows the closed badge only; this is what the click opens.
292
+ */
293
+ export function RemovalPopover() {
294
+ const deprecatedColor = '#6b7280';
295
+ const variants = [
296
+ {
297
+ label: 'with removedIn',
298
+ removal: {
299
+ commit: 'e168afc',
300
+ reason: 'Remove topology relations; nest module boundaries by path',
301
+ },
302
+ },
303
+ { label: 'no removedIn (fallback copy)', removal: undefined },
304
+ ] as const;
305
+ return (
306
+ <div style={{ display: 'flex', gap: 24, padding: 24, alignItems: 'flex-start' }}>
307
+ {variants.map((v) => (
308
+ <div key={v.label} style={{ width: 300 }}>
309
+ <div
310
+ style={{
311
+ fontFamily: 'ui-monospace, monospace',
312
+ fontSize: 11,
313
+ color: '#888',
314
+ marginBottom: 6,
315
+ }}
316
+ >
317
+ {v.label}
318
+ </div>
319
+ <div
320
+ role="dialog"
321
+ aria-label="Removal provenance"
322
+ style={{
323
+ padding: '8px 10px',
324
+ borderRadius: 6,
325
+ background: '#1f1f1f',
326
+ border: `1px solid ${deprecatedColor}`,
327
+ boxShadow: '0 6px 20px rgba(0,0,0,0.35)',
328
+ fontFamily: 'system-ui, sans-serif',
329
+ fontSize: 13,
330
+ color: '#eee',
331
+ }}
332
+ >
333
+ <div style={{ fontWeight: 600, marginBottom: 4, color: deprecatedColor }}>
334
+ Deprecated
335
+ </div>
336
+ {v.removal ? (
337
+ <>
338
+ <div style={{ fontFamily: 'ui-monospace, monospace', marginBottom: 4 }}>
339
+ removed in {v.removal.commit}
340
+ </div>
341
+ {v.removal.reason && <div style={{ color: '#aaa' }}>{v.removal.reason}</div>}
342
+ </>
343
+ ) : (
344
+ <div style={{ color: '#aaa' }}>
345
+ This claim's source was removed upstream.
346
+ </div>
347
+ )}
348
+ </div>
349
+ </div>
350
+ ))}
351
+ </div>
352
+ );
353
+ }
354
+
355
+ /** The removal-provenance popover, both copy variants. */
356
+ export const Popover: Story = {
357
+ render: () => <RemovalPopover />,
358
+ };
@@ -46,24 +46,30 @@ const GRAPH_DESCRIPTION =
46
46
  /** Every chip state side by side, for quick visual review. */
47
47
  export const States: Story = {
48
48
  render: () => {
49
- const rows: Array<{ label: string; status: SubsystemDiagnosticStatus; issueCount?: number; stale?: boolean; busy?: boolean }> = [
49
+ const rows: Array<{ label: string; status: SubsystemDiagnosticStatus; issueCount?: number; fixableCount?: number; stale?: boolean; busy?: boolean }> = [
50
50
  { label: 'ok', status: 'ok' },
51
51
  { label: 'ok (stale)', status: 'ok', stale: true },
52
52
  { label: 'issues · 3', status: 'issues', issueCount: 3 },
53
53
  { label: 'gaps · 2', status: 'gaps', issueCount: 2 },
54
54
  { label: 'unknown (never checked)', status: 'unknown' },
55
55
  { label: 'issues (busy)', status: 'issues', issueCount: 3, busy: true },
56
+ { label: 'issues · 7, 7 fixable', status: 'issues', issueCount: 7, fixableCount: 7 },
57
+ { label: 'issues · 4, 2 fixable', status: 'issues', issueCount: 4, fixableCount: 2 },
58
+ { label: 'issues · 2, fixable + stale', status: 'issues', issueCount: 2, fixableCount: 2, stale: true },
59
+ { label: 'issues · 120, fixable capped', status: 'issues', issueCount: 120, fixableCount: 120 },
60
+ { label: 'issues (busy) hides badge', status: 'issues', issueCount: 3, fixableCount: 3, busy: true },
56
61
  ];
57
62
  return (
58
63
  <div style={{ display: 'flex', flexDirection: 'column', gap: 18, padding: 32 }}>
59
64
  {rows.map((row) => (
60
65
  <div key={row.label} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
61
- <span style={{ width: 200, fontFamily: 'monospace', fontSize: 12, opacity: 0.7 }}>
66
+ <span style={{ width: 220, fontFamily: 'monospace', fontSize: 12, opacity: 0.7 }}>
62
67
  {row.label}
63
68
  </span>
64
69
  <SubsystemDiagnosticToggle
65
70
  status={row.status}
66
71
  issueCount={row.issueCount}
72
+ fixableCount={row.fixableCount}
67
73
  stale={row.stale}
68
74
  busy={row.busy}
69
75
  onToggle={() => {}}
@@ -75,6 +81,25 @@ export const States: Story = {
75
81
  },
76
82
  };
77
83
 
84
+ /**
85
+ * The real case that motivated the badge: seven re-pin findings, every one a
86
+ * one-click fix the construct-fixer defers to the human. The chip announces
87
+ * them so the deferral isn't silent.
88
+ */
89
+ export const GraphFixableBadge: Story = {
90
+ render: () => (
91
+ <div style={{ width: '100%', height: '100vh' }}>
92
+ <SubsystemComponentGraph
93
+ components={graphComponents}
94
+ trails={graphEdges.trails}
95
+ title="Vestigial detail layer vs single-source declaration"
96
+ description={GRAPH_DESCRIPTION}
97
+ diagnostic={{ status: 'issues', issueCount: 7, fixableCount: 7, onToggle: () => {} }}
98
+ />
99
+ </div>
100
+ ),
101
+ };
102
+
78
103
  /** The chip in its real home: the sidebar title row, beside the description toggle. */
79
104
  export const GraphClean: Story = {
80
105
  render: () => (
@@ -8,7 +8,7 @@ import { PierreTrailCodeView } from '../../../pierre';
8
8
  import type { TrailViewerContext } from '../../../subsystem/SubsystemComponentGraph';
9
9
 
10
10
  const meta = {
11
- title: 'Subsystem/ComponentGraph/Flows',
11
+ title: 'Subsystem/ComponentGraph/Trails',
12
12
  component: SubsystemComponentGraph,
13
13
  parameters: {
14
14
  layout: 'fullscreen',
@@ -164,7 +164,7 @@ const drawingTrails: SubsystemTrail[] = [
164
164
  },
165
165
  ];
166
166
 
167
- function FlowsDemo() {
167
+ function TrailsDemo() {
168
168
  const renderTrailViewer = useCallback(
169
169
  ({
170
170
  trail,
@@ -173,6 +173,7 @@ function FlowsDemo() {
173
173
  proposedAliases,
174
174
  resolveSymbol,
175
175
  onSymbolClick,
176
+ onVisibleStepChange,
176
177
  }: TrailViewerContext) => (
177
178
  <PierreTrailCodeView
178
179
  trail={trail}
@@ -183,6 +184,7 @@ function FlowsDemo() {
183
184
  proposedAliases={proposedAliases}
184
185
  resolveSymbol={resolveSymbol}
185
186
  onSymbolClick={onSymbolClick}
187
+ onVisibleStepChange={onVisibleStepChange}
186
188
  />
187
189
  ),
188
190
  [],
@@ -217,8 +219,8 @@ function FlowsDemo() {
217
219
  );
218
220
  }
219
221
 
220
- export const ThreeFlows: Story = {
221
- render: () => <FlowsDemo />,
222
+ export const ThreeTrails: Story = {
223
+ render: () => <TrailsDemo />,
222
224
  };
223
225
 
224
226
  /**
@@ -281,6 +283,7 @@ function ProposedMissingStepDemo() {
281
283
  proposedAliases,
282
284
  resolveSymbol,
283
285
  onSymbolClick,
286
+ onVisibleStepChange,
284
287
  }: TrailViewerContext) => (
285
288
  <PierreTrailCodeView
286
289
  trail={trail}
@@ -291,6 +294,7 @@ function ProposedMissingStepDemo() {
291
294
  proposedAliases={proposedAliases}
292
295
  resolveSymbol={resolveSymbol}
293
296
  onSymbolClick={onSymbolClick}
297
+ onVisibleStepChange={onVisibleStepChange}
294
298
  />
295
299
  ),
296
300
  [],
@@ -480,6 +484,7 @@ function ClickableConstructsDemo() {
480
484
  proposedAliases,
481
485
  resolveSymbol,
482
486
  onSymbolClick,
487
+ onVisibleStepChange,
483
488
  }: TrailViewerContext) => (
484
489
  <PierreTrailCodeView
485
490
  trail={trail}
@@ -490,6 +495,7 @@ function ClickableConstructsDemo() {
490
495
  proposedAliases={proposedAliases}
491
496
  resolveSymbol={resolveSymbol}
492
497
  onSymbolClick={onSymbolClick}
498
+ onVisibleStepChange={onVisibleStepChange}
493
499
  />
494
500
  ),
495
501
  [],