@principal-ai/subsystems-react 0.24.0 → 0.24.2
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.
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +3 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +2 -2
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +30 -0
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -0
- package/dist/subsystem/ConstructsCatalog.js +329 -0
- package/dist/subsystem/ConstructsCatalog.js.map +1 -0
- package/dist/subsystem/DiagnosticToggle.d.ts +46 -0
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -0
- package/dist/subsystem/DiagnosticToggle.js +95 -0
- package/dist/subsystem/DiagnosticToggle.js.map +1 -0
- package/dist/subsystem/IssueList.d.ts +103 -0
- package/dist/subsystem/IssueList.d.ts.map +1 -0
- package/dist/subsystem/IssueList.js +310 -0
- package/dist/subsystem/IssueList.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +28 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +81 -58
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +2 -2
- package/dist/subsystem/SubsystemFileTree.js.map +1 -1
- package/dist/subsystem/model.d.ts +13 -2
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +12 -0
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.js +5 -5
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +37 -0
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +287 -0
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +138 -0
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +130 -0
- package/src/subsystem/ComponentDeclaration.tsx +4 -1
- package/src/subsystem/ConstructsCatalog.tsx +604 -0
- package/src/subsystem/DiagnosticToggle.tsx +160 -0
- package/src/subsystem/IssueList.tsx +519 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +119 -38
- package/src/subsystem/SubsystemFileTree.tsx +2 -2
- package/src/subsystem/model.test.ts +34 -0
- package/src/subsystem/model.ts +18 -2
- package/src/subsystem/nodes.tsx +5 -5
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import '@xyflow/react/dist/style.css';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
|
|
6
|
+
import { PierreFileView, PierreSnippetView } from '../../../pierre';
|
|
7
|
+
import type { SubsystemComponent } from '../../../subsystem/model';
|
|
8
|
+
import type { SubsystemOpenFileOptions } from '../../../subsystem/declarationRef';
|
|
9
|
+
import type { GraphifyComponentDetail } from '../../../graphify';
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: 'Subsystem/ComponentGraph/Constructs',
|
|
13
|
+
component: SubsystemComponentGraph,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'fullscreen',
|
|
16
|
+
},
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
decorators: [
|
|
19
|
+
(Story) => (
|
|
20
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
21
|
+
<Story />
|
|
22
|
+
</ThemeProvider>
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
} satisfies Meta<typeof SubsystemComponentGraph>;
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof meta>;
|
|
29
|
+
|
|
30
|
+
const PURL = 'pkg:github/you/notes-intake';
|
|
31
|
+
|
|
32
|
+
/** One construct of each core kind, no relations — the catalog view. */
|
|
33
|
+
const constructsOnly: SubsystemComponent[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 'submit-note',
|
|
36
|
+
name: 'submitNote',
|
|
37
|
+
construct: 'function',
|
|
38
|
+
symbol: 'submitNote',
|
|
39
|
+
role: 'entry',
|
|
40
|
+
purl: PURL,
|
|
41
|
+
file: 'src/api/submitNote.ts',
|
|
42
|
+
purpose: 'HTTP/handler entry — validates and hands off to the service.',
|
|
43
|
+
declarationRef: {
|
|
44
|
+
file: 'src/api/submitNote.ts',
|
|
45
|
+
startLine: 7,
|
|
46
|
+
lineHash: 'storybook-placeholder',
|
|
47
|
+
capturedAt: new Date(0).toISOString(),
|
|
48
|
+
},
|
|
49
|
+
declaration: {
|
|
50
|
+
kind: 'function',
|
|
51
|
+
parameters: [
|
|
52
|
+
{ name: 'req', type: 'SubmitNoteRequest' },
|
|
53
|
+
{ name: 'ctx', type: 'RequestContext' },
|
|
54
|
+
],
|
|
55
|
+
returnType: 'Promise<NoteId>',
|
|
56
|
+
callers: [],
|
|
57
|
+
callees: [{ nodeId: 'note-service', name: 'NoteService' }],
|
|
58
|
+
} satisfies GraphifyComponentDetail,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'note-service',
|
|
62
|
+
name: 'NoteService',
|
|
63
|
+
construct: 'class',
|
|
64
|
+
symbol: 'NoteService',
|
|
65
|
+
purl: PURL,
|
|
66
|
+
file: 'src/NoteService.ts',
|
|
67
|
+
purpose: 'Owns intake rules; methods are separate nodes when they matter.',
|
|
68
|
+
declarationRef: {
|
|
69
|
+
file: 'src/NoteService.ts',
|
|
70
|
+
startLine: 3,
|
|
71
|
+
lineHash: 'storybook-placeholder',
|
|
72
|
+
capturedAt: new Date(0).toISOString(),
|
|
73
|
+
},
|
|
74
|
+
declaration: {
|
|
75
|
+
kind: 'class',
|
|
76
|
+
methods: [
|
|
77
|
+
{
|
|
78
|
+
nodeId: 'note-service-persist',
|
|
79
|
+
name: 'persist',
|
|
80
|
+
parameters: [{ name: 'draft', type: 'NoteDraft' }],
|
|
81
|
+
returnType: 'Promise<NoteId>',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
properties: [{ name: 'repo', type: 'NoteRepository' }],
|
|
85
|
+
implements: ['NoteRepository'],
|
|
86
|
+
} satisfies GraphifyComponentDetail,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'note-service-persist',
|
|
90
|
+
name: 'persist',
|
|
91
|
+
construct: 'method',
|
|
92
|
+
symbol: 'NoteService.persist',
|
|
93
|
+
purl: PURL,
|
|
94
|
+
file: 'src/NoteService.ts',
|
|
95
|
+
purpose: 'Instance method on NoteService — written as its own construct.',
|
|
96
|
+
declarationRef: {
|
|
97
|
+
file: 'src/NoteService.ts',
|
|
98
|
+
startLine: 14,
|
|
99
|
+
lineHash: 'storybook-placeholder',
|
|
100
|
+
capturedAt: new Date(0).toISOString(),
|
|
101
|
+
},
|
|
102
|
+
declaration: {
|
|
103
|
+
kind: 'function',
|
|
104
|
+
parameters: [{ name: 'draft', type: 'NoteDraft' }],
|
|
105
|
+
returnType: 'Promise<NoteId>',
|
|
106
|
+
callers: [{ nodeId: 'submit-note', name: 'submitNote' }],
|
|
107
|
+
callees: [],
|
|
108
|
+
} satisfies GraphifyComponentDetail,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'note-repository',
|
|
112
|
+
name: 'NoteRepository',
|
|
113
|
+
construct: 'interface',
|
|
114
|
+
symbol: 'NoteRepository',
|
|
115
|
+
purl: PURL,
|
|
116
|
+
file: 'src/NoteRepository.ts',
|
|
117
|
+
purpose: 'Persistence contract the service depends on.',
|
|
118
|
+
declarationRef: {
|
|
119
|
+
file: 'src/NoteRepository.ts',
|
|
120
|
+
startLine: 1,
|
|
121
|
+
lineHash: 'storybook-placeholder',
|
|
122
|
+
capturedAt: new Date(0).toISOString(),
|
|
123
|
+
},
|
|
124
|
+
declaration: {
|
|
125
|
+
kind: 'type',
|
|
126
|
+
properties: [
|
|
127
|
+
{ name: 'save', type: '(draft: NoteDraft) => Promise<NoteId>' },
|
|
128
|
+
{ name: 'findById', type: '(id: NoteId) => Promise<Note | null>' },
|
|
129
|
+
],
|
|
130
|
+
implementors: ['NoteService'],
|
|
131
|
+
} satisfies GraphifyComponentDetail,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'note-id',
|
|
135
|
+
name: 'NoteId',
|
|
136
|
+
construct: 'type_alias',
|
|
137
|
+
symbol: 'NoteId',
|
|
138
|
+
purl: PURL,
|
|
139
|
+
file: 'src/types.ts',
|
|
140
|
+
purpose: 'Branded id type — a declaration, not a runtime instance.',
|
|
141
|
+
declarationRef: {
|
|
142
|
+
file: 'src/types.ts',
|
|
143
|
+
startLine: 1,
|
|
144
|
+
lineHash: 'storybook-placeholder',
|
|
145
|
+
capturedAt: new Date(0).toISOString(),
|
|
146
|
+
},
|
|
147
|
+
declaration: {
|
|
148
|
+
kind: 'type',
|
|
149
|
+
aliasOf: "string & { readonly __brand: 'NoteId' }",
|
|
150
|
+
} satisfies GraphifyComponentDetail,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'note-kind',
|
|
154
|
+
name: 'NoteKind',
|
|
155
|
+
construct: 'enum',
|
|
156
|
+
symbol: 'NoteKind',
|
|
157
|
+
purl: PURL,
|
|
158
|
+
file: 'src/types.ts',
|
|
159
|
+
purpose: 'Closed set of note kinds (idea, task, …).',
|
|
160
|
+
declarationRef: {
|
|
161
|
+
file: 'src/types.ts',
|
|
162
|
+
startLine: 5,
|
|
163
|
+
lineHash: 'storybook-placeholder',
|
|
164
|
+
capturedAt: new Date(0).toISOString(),
|
|
165
|
+
},
|
|
166
|
+
declaration: {
|
|
167
|
+
kind: 'type',
|
|
168
|
+
enumMembers: [
|
|
169
|
+
{ name: 'Idea', value: "'idea'" },
|
|
170
|
+
{ name: 'Task', value: "'task'" },
|
|
171
|
+
],
|
|
172
|
+
} satisfies GraphifyComponentDetail,
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const storyFiles: Record<string, string> = {
|
|
177
|
+
'src/api/submitNote.ts': `import { NoteService } from '../NoteService';
|
|
178
|
+
import type { NoteId, SubmitNoteRequest, RequestContext } from '../types';
|
|
179
|
+
|
|
180
|
+
const service = new NoteService();
|
|
181
|
+
|
|
182
|
+
/** HTTP/handler entry — validates and hands off to the service. */
|
|
183
|
+
export async function submitNote(
|
|
184
|
+
req: SubmitNoteRequest,
|
|
185
|
+
ctx: RequestContext,
|
|
186
|
+
): Promise<NoteId> {
|
|
187
|
+
return service.persist({ ...req, actor: ctx.actor });
|
|
188
|
+
}
|
|
189
|
+
`,
|
|
190
|
+
'src/NoteService.ts': `import type { NoteDraft, NoteId, NoteRepository } from './types';
|
|
191
|
+
|
|
192
|
+
export class NoteService implements NoteRepository {
|
|
193
|
+
constructor(readonly repo: NoteRepository) {}
|
|
194
|
+
|
|
195
|
+
async save(draft: NoteDraft): Promise<NoteId> {
|
|
196
|
+
return this.persist(draft);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async findById(id: NoteId): Promise<Note | null> {
|
|
200
|
+
return this.repo.findById(id);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async persist(draft: NoteDraft): Promise<NoteId> {
|
|
204
|
+
return this.repo.save(draft);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
`,
|
|
208
|
+
'src/NoteRepository.ts': `import type { Note, NoteDraft, NoteId } from './types';
|
|
209
|
+
|
|
210
|
+
export interface NoteRepository {
|
|
211
|
+
save(draft: NoteDraft): Promise<NoteId>;
|
|
212
|
+
findById(id: NoteId): Promise<Note | null>;
|
|
213
|
+
}
|
|
214
|
+
`,
|
|
215
|
+
'src/types.ts': `export type NoteId = string & { readonly __brand: 'NoteId' };
|
|
216
|
+
|
|
217
|
+
export type NoteDraft = { title: string; kind: NoteKind };
|
|
218
|
+
|
|
219
|
+
export enum NoteKind {
|
|
220
|
+
Idea = 'idea',
|
|
221
|
+
Task = 'task',
|
|
222
|
+
}
|
|
223
|
+
`,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function readStoryFile(path: string): Promise<string> {
|
|
227
|
+
const content = storyFiles[path];
|
|
228
|
+
if (content == null) return Promise.reject(new Error(`No story fixture for ${path}`));
|
|
229
|
+
return Promise.resolve(content);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderFileViewer(file: string, opts?: SubsystemOpenFileOptions) {
|
|
233
|
+
const startLine = opts?.startLine;
|
|
234
|
+
if (startLine != null && !opts?.fullFile) {
|
|
235
|
+
return (
|
|
236
|
+
<PierreSnippetView
|
|
237
|
+
filePath={file}
|
|
238
|
+
fileName={file.split('/').pop() ?? file}
|
|
239
|
+
startLine={startLine}
|
|
240
|
+
endLine={startLine}
|
|
241
|
+
focusLine={startLine}
|
|
242
|
+
contextLines={12}
|
|
243
|
+
readFile={readStoryFile}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return (
|
|
248
|
+
<PierreFileView
|
|
249
|
+
filePath={file}
|
|
250
|
+
fileName={file.split('/').pop() ?? file}
|
|
251
|
+
readFile={readStoryFile}
|
|
252
|
+
focusLine={opts?.fullFile ? startLine : undefined}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function CatalogDemo({ hideSidebar = false }: { hideSidebar?: boolean }) {
|
|
258
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
259
|
+
return (
|
|
260
|
+
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
261
|
+
<SubsystemComponentGraph
|
|
262
|
+
components={constructsOnly}
|
|
263
|
+
relations={[]}
|
|
264
|
+
title="Constructs at a glance"
|
|
265
|
+
description="Layer 1 — constructs only. Click a file path or L# to open the declaration source."
|
|
266
|
+
hideSidebar={hideSidebar}
|
|
267
|
+
onSelect={(id) => setSelected(id)}
|
|
268
|
+
renderFileViewer={renderFileViewer}
|
|
269
|
+
/>
|
|
270
|
+
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
271
|
+
{selected
|
|
272
|
+
? `last toggled: ${selected}`
|
|
273
|
+
: 'click constructs to toggle their signatures'}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Homepage-style embed: list of constructs on the left, signature on the right. */
|
|
280
|
+
export const Catalog: Story = {
|
|
281
|
+
render: () => <CatalogDemo />,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** Same catalog, no title chrome — the shape the home-page constructs layer uses. */
|
|
285
|
+
export const CatalogEmbed: Story = {
|
|
286
|
+
render: () => <CatalogDemo hideSidebar />,
|
|
287
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import '@xyflow/react/dist/style.css';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
|
|
6
|
+
import {
|
|
7
|
+
SubsystemDiagnosticToggle,
|
|
8
|
+
type SubsystemDiagnosticStatus,
|
|
9
|
+
} from '../../../subsystem/DiagnosticToggle';
|
|
10
|
+
import { components, graphSpecFromEdges } from './fixtures';
|
|
11
|
+
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'Subsystem/ComponentGraph/Diagnostics',
|
|
14
|
+
component: SubsystemComponentGraph,
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'fullscreen',
|
|
17
|
+
},
|
|
18
|
+
decorators: [
|
|
19
|
+
(Story) => (
|
|
20
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
21
|
+
<Story />
|
|
22
|
+
</ThemeProvider>
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
} satisfies Meta<typeof SubsystemComponentGraph>;
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof meta>;
|
|
29
|
+
|
|
30
|
+
const graphComponents = components([
|
|
31
|
+
['entry', 'checkoutApi', 'function', 'src/checkout/api.ts', 'pkg:github/you/your-app', 'Handles cart requests.', 'checkoutApi'],
|
|
32
|
+
['store', 'cartStore', 'store', 'src/checkout/cartStore.ts', 'pkg:github/you/your-app', 'Retained cart state.', 'cartStore'],
|
|
33
|
+
['stripe', 'Stripe', 'external', '', 'external', undefined, undefined],
|
|
34
|
+
['caller', 'Web client', 'external', '', 'external', undefined, undefined],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const graphEdges = graphSpecFromEdges([
|
|
38
|
+
['caller', 'entry', 'calls'],
|
|
39
|
+
['entry', 'store', 'writes'],
|
|
40
|
+
['entry', 'stripe', 'calls'],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const GRAPH_DESCRIPTION =
|
|
44
|
+
'A small e-commerce checkout subsystem: an HTTP entry point that writes to retained cart state and calls out to Stripe.';
|
|
45
|
+
|
|
46
|
+
/** Every chip state side by side, for quick visual review. */
|
|
47
|
+
export const States: Story = {
|
|
48
|
+
render: () => {
|
|
49
|
+
const rows: Array<{ label: string; status: SubsystemDiagnosticStatus; issueCount?: number; stale?: boolean; busy?: boolean }> = [
|
|
50
|
+
{ label: 'ok', status: 'ok' },
|
|
51
|
+
{ label: 'ok (stale)', status: 'ok', stale: true },
|
|
52
|
+
{ label: 'issues · 3', status: 'issues', issueCount: 3 },
|
|
53
|
+
{ label: 'gaps · 2', status: 'gaps', issueCount: 2 },
|
|
54
|
+
{ label: 'unknown (never checked)', status: 'unknown' },
|
|
55
|
+
{ label: 'issues (busy)', status: 'issues', issueCount: 3, busy: true },
|
|
56
|
+
];
|
|
57
|
+
return (
|
|
58
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 18, padding: 32 }}>
|
|
59
|
+
{rows.map((row) => (
|
|
60
|
+
<div key={row.label} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
61
|
+
<span style={{ width: 200, fontFamily: 'monospace', fontSize: 12, opacity: 0.7 }}>
|
|
62
|
+
{row.label}
|
|
63
|
+
</span>
|
|
64
|
+
<SubsystemDiagnosticToggle
|
|
65
|
+
status={row.status}
|
|
66
|
+
issueCount={row.issueCount}
|
|
67
|
+
stale={row.stale}
|
|
68
|
+
busy={row.busy}
|
|
69
|
+
onOpen={() => {}}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The chip in its real home: the sidebar title row, beside the description toggle. */
|
|
79
|
+
export const GraphClean: Story = {
|
|
80
|
+
render: () => (
|
|
81
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
82
|
+
<SubsystemComponentGraph
|
|
83
|
+
components={graphComponents}
|
|
84
|
+
relations={graphEdges.relations}
|
|
85
|
+
walkthroughs={graphEdges.walkthroughs}
|
|
86
|
+
title="Checkout"
|
|
87
|
+
description={GRAPH_DESCRIPTION}
|
|
88
|
+
diagnostic={{ status: 'ok', onOpen: () => {} }}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const GraphWithIssues: Story = {
|
|
95
|
+
render: () => (
|
|
96
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
97
|
+
<SubsystemComponentGraph
|
|
98
|
+
components={graphComponents}
|
|
99
|
+
relations={graphEdges.relations}
|
|
100
|
+
walkthroughs={graphEdges.walkthroughs}
|
|
101
|
+
title="Checkout"
|
|
102
|
+
description={GRAPH_DESCRIPTION}
|
|
103
|
+
diagnostic={{ status: 'issues', issueCount: 3, onOpen: () => {} }}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Stale report: the color stays, a hollow ring marks that inputs changed. */
|
|
110
|
+
export const GraphStale: Story = {
|
|
111
|
+
render: () => (
|
|
112
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
113
|
+
<SubsystemComponentGraph
|
|
114
|
+
components={graphComponents}
|
|
115
|
+
relations={graphEdges.relations}
|
|
116
|
+
walkthroughs={graphEdges.walkthroughs}
|
|
117
|
+
title="Checkout"
|
|
118
|
+
description={GRAPH_DESCRIPTION}
|
|
119
|
+
diagnostic={{ status: 'issues', issueCount: 3, stale: true, onOpen: () => {} }}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** No title/description — the chip still anchors the title row at the right. */
|
|
126
|
+
export const GraphChipOnly: Story = {
|
|
127
|
+
render: () => (
|
|
128
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
129
|
+
<SubsystemComponentGraph
|
|
130
|
+
components={graphComponents}
|
|
131
|
+
relations={graphEdges.relations}
|
|
132
|
+
walkthroughs={graphEdges.walkthroughs}
|
|
133
|
+
title="Checkout"
|
|
134
|
+
diagnostic={{ status: 'gaps', issueCount: 2, onOpen: () => {} }}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
),
|
|
138
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import '@xyflow/react/dist/style.css';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
|
|
6
|
+
import { SubsystemIssueList, type SubsystemIssue } from '../../../subsystem/IssueList';
|
|
7
|
+
import { components, graphSpecFromEdges } from './fixtures';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Subsystem/ComponentGraph/Issues',
|
|
11
|
+
component: SubsystemComponentGraph,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'fullscreen',
|
|
14
|
+
},
|
|
15
|
+
decorators: [
|
|
16
|
+
(Story) => (
|
|
17
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
18
|
+
<Story />
|
|
19
|
+
</ThemeProvider>
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
} satisfies Meta<typeof SubsystemComponentGraph>;
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
|
|
27
|
+
const ISSUES: SubsystemIssue[] = [
|
|
28
|
+
{
|
|
29
|
+
id: 'i1',
|
|
30
|
+
severity: 'error',
|
|
31
|
+
kind: 'missing_file',
|
|
32
|
+
message: 'File src/checkout/cartStore.ts is not present on disk (repo pkg:github/you/your-app).',
|
|
33
|
+
target: { kind: 'component', id: 'store', label: 'cartStore' },
|
|
34
|
+
fix: { label: 'Use graphify file' },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'i2',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
kind: 'construct_mismatch',
|
|
40
|
+
message: 'Claimed function but graphify infers class.',
|
|
41
|
+
target: { kind: 'component', id: 'entry', label: 'checkoutApi' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'i3',
|
|
45
|
+
severity: 'info',
|
|
46
|
+
kind: 'signature_unconfirmed',
|
|
47
|
+
message: 'Named parameter types are not present in the graphify cache.',
|
|
48
|
+
target: { kind: 'component', id: 'entry', label: 'checkoutApi' },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'i4',
|
|
52
|
+
severity: 'error',
|
|
53
|
+
kind: 'topology_broken_endpoint',
|
|
54
|
+
message: 'Relation endpoint "store" does not resolve to a component.',
|
|
55
|
+
target: { kind: 'relation', id: 'r1', label: 'checkoutApi → cartStore', detail: 'writes' },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'i5',
|
|
59
|
+
severity: 'warn',
|
|
60
|
+
kind: 'boundary_module_file_mismatch',
|
|
61
|
+
message: 'Module "src/checkout" contains a file outside its declared root.',
|
|
62
|
+
target: { kind: 'module', id: 'src/checkout', label: 'src/checkout' },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'i6',
|
|
66
|
+
severity: 'info',
|
|
67
|
+
kind: 'walkthrough',
|
|
68
|
+
message: 'Step 2 references a line that moved (was :42).',
|
|
69
|
+
target: { kind: 'walkthrough', id: 'flow-1', label: 'Checkout', detail: 'step 2' },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'i7',
|
|
73
|
+
severity: 'warn',
|
|
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' },
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const graphComponents = components([
|
|
81
|
+
['entry', 'checkoutApi', 'function', 'src/checkout/api.ts', 'pkg:github/you/your-app', 'Handles cart requests.', 'checkoutApi'],
|
|
82
|
+
['store', 'cartStore', 'store', 'src/checkout/cartStore.ts', 'pkg:github/you/your-app', 'Retained cart state.', 'cartStore'],
|
|
83
|
+
['stripe', 'Stripe', 'external', '', 'external', undefined, undefined],
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const graphEdges = graphSpecFromEdges([
|
|
87
|
+
['entry', 'store', 'writes'],
|
|
88
|
+
['entry', 'stripe', 'calls'],
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/** The catalogue, grouped by verification layer. */
|
|
92
|
+
export const List: Story = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<div style={{ display: 'flex', flexDirection: 'column', width: 380, height: '100vh', borderRight: '1px solid #333' }}>
|
|
95
|
+
<SubsystemIssueList
|
|
96
|
+
issues={ISSUES}
|
|
97
|
+
onSelectIssue={() => {}}
|
|
98
|
+
onApplyFix={() => {}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** All four layers clean — the happy state. */
|
|
105
|
+
export const AllGood: Story = {
|
|
106
|
+
render: () => (
|
|
107
|
+
<div style={{ display: 'flex', flexDirection: 'column', width: 380, height: '100vh', borderRight: '1px solid #333' }}>
|
|
108
|
+
<SubsystemIssueList issues={[]} />
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** In place: diagnostics active, so the sidebar bottom panel is the issue list. */
|
|
114
|
+
export const GraphWithIssues: Story = {
|
|
115
|
+
render: () => (
|
|
116
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
117
|
+
<SubsystemComponentGraph
|
|
118
|
+
components={graphComponents}
|
|
119
|
+
relations={graphEdges.relations}
|
|
120
|
+
walkthroughs={graphEdges.walkthroughs}
|
|
121
|
+
title="Checkout"
|
|
122
|
+
description="A small e-commerce checkout subsystem."
|
|
123
|
+
diagnostic={{ status: 'issues', issueCount: 5, onOpen: () => {} }}
|
|
124
|
+
issues={ISSUES}
|
|
125
|
+
onSelectIssue={() => {}}
|
|
126
|
+
onApplyIssueFix={() => {}}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
),
|
|
130
|
+
};
|
|
@@ -118,6 +118,8 @@ export interface ComponentDeclarationProps {
|
|
|
118
118
|
onVerify?: (componentId: string) => void;
|
|
119
119
|
/** Live verification status for the selected component. */
|
|
120
120
|
verification?: ComponentVerificationState | null;
|
|
121
|
+
/** Start with the file path row visible (catalog / source-first views). */
|
|
122
|
+
defaultShowFile?: boolean;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
function verificationSummary(
|
|
@@ -256,11 +258,12 @@ export function ComponentDeclaration({
|
|
|
256
258
|
maxWidth,
|
|
257
259
|
onVerify,
|
|
258
260
|
verification,
|
|
261
|
+
defaultShowFile = false,
|
|
259
262
|
}: ComponentDeclarationProps) {
|
|
260
263
|
const { theme, mode } = useTheme();
|
|
261
264
|
const pierreSyntaxTheme = resolvePierreSyntaxThemeName(mode);
|
|
262
265
|
const [fileHovered, setFileHovered] = useState(false);
|
|
263
|
-
const [showFile, setShowFile] = useState(
|
|
266
|
+
const [showFile, setShowFile] = useState(defaultShowFile);
|
|
264
267
|
const [showPurpose, setShowPurpose] = useState(false);
|
|
265
268
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
266
269
|
const color = componentColor(component, pierreSyntaxTheme);
|