edsger 0.68.0 → 0.70.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.
- package/dist/api/github.d.ts +23 -0
- package/dist/api/github.js +61 -0
- package/dist/commands/architecture-diagram/index.d.ts +8 -0
- package/dist/commands/architecture-diagram/index.js +10 -0
- package/dist/commands/class-diagram/index.d.ts +7 -0
- package/dist/commands/class-diagram/index.js +9 -0
- package/dist/commands/data-flow/index.d.ts +8 -6
- package/dist/commands/data-flow/index.js +21 -12
- package/dist/commands/diagram-shared/index.d.ts +21 -0
- package/dist/commands/diagram-shared/index.js +37 -0
- package/dist/commands/er-diagram/index.d.ts +19 -0
- package/dist/commands/er-diagram/index.js +55 -0
- package/dist/commands/flowchart/index.d.ts +8 -0
- package/dist/commands/flowchart/index.js +10 -0
- package/dist/commands/quality-benchmark/index.js +43 -9
- package/dist/commands/recipes/index.d.ts +3 -1
- package/dist/commands/recipes/index.js +10 -4
- package/dist/commands/screen-flow/index.d.ts +8 -6
- package/dist/commands/screen-flow/index.js +21 -12
- package/dist/commands/sequence-diagram/index.d.ts +19 -0
- package/dist/commands/sequence-diagram/index.js +55 -0
- package/dist/commands/state-diagram/index.d.ts +7 -0
- package/dist/commands/state-diagram/index.js +9 -0
- package/dist/index.js +144 -14
- package/dist/phases/architecture-diagram/index.d.ts +15 -0
- package/dist/phases/architecture-diagram/index.js +51 -0
- package/dist/phases/class-diagram/index.d.ts +14 -0
- package/dist/phases/class-diagram/index.js +76 -0
- package/dist/phases/data-flow/index.d.ts +6 -3
- package/dist/phases/data-flow/index.js +59 -38
- package/dist/phases/data-flow/mcp-server.d.ts +1 -1
- package/dist/phases/data-flow/mcp-server.js +2 -2
- package/dist/phases/data-flow/types.d.ts +1 -1
- package/dist/phases/data-flow/types.js +1 -1
- package/dist/phases/diagram-shared/clone-repos.d.ts +63 -0
- package/dist/phases/diagram-shared/clone-repos.js +153 -0
- package/dist/phases/diagram-shared/generate.d.ts +42 -0
- package/dist/phases/diagram-shared/generate.js +162 -0
- package/dist/phases/diagram-shared/graph.d.ts +62 -0
- package/dist/phases/diagram-shared/graph.js +169 -0
- package/dist/phases/diagram-shared/mcp.d.ts +35 -0
- package/dist/phases/diagram-shared/mcp.js +68 -0
- package/dist/phases/diagram-shared/prompts.d.ts +23 -0
- package/dist/phases/diagram-shared/prompts.js +35 -0
- package/dist/phases/er-diagram/index.d.ts +28 -0
- package/dist/phases/er-diagram/index.js +290 -0
- package/dist/phases/er-diagram/mcp-server.d.ts +77 -0
- package/dist/phases/er-diagram/mcp-server.js +144 -0
- package/dist/phases/er-diagram/prompts.d.ts +14 -0
- package/dist/phases/er-diagram/prompts.js +36 -0
- package/dist/phases/er-diagram/types.d.ts +76 -0
- package/dist/phases/er-diagram/types.js +84 -0
- package/dist/phases/flow-shared/clone-repos.d.ts +8 -2
- package/dist/phases/flow-shared/clone-repos.js +36 -18
- package/dist/phases/flowchart/index.d.ts +15 -0
- package/dist/phases/flowchart/index.js +50 -0
- package/dist/phases/output-contracts.js +178 -2
- package/dist/phases/recipes/index.d.ts +8 -1
- package/dist/phases/recipes/index.js +74 -17
- package/dist/phases/recipes/mcp-server.d.ts +4 -1
- package/dist/phases/recipes/mcp-server.js +43 -18
- package/dist/phases/screen-flow/index.d.ts +7 -4
- package/dist/phases/screen-flow/index.js +66 -45
- package/dist/phases/screen-flow/mcp-server.js +2 -2
- package/dist/phases/sequence-diagram/index.d.ts +30 -0
- package/dist/phases/sequence-diagram/index.js +290 -0
- package/dist/phases/sequence-diagram/mcp-server.d.ts +64 -0
- package/dist/phases/sequence-diagram/mcp-server.js +134 -0
- package/dist/phases/sequence-diagram/prompts.d.ts +14 -0
- package/dist/phases/sequence-diagram/prompts.js +36 -0
- package/dist/phases/sequence-diagram/types.d.ts +52 -0
- package/dist/phases/sequence-diagram/types.js +93 -0
- package/dist/phases/state-diagram/index.d.ts +15 -0
- package/dist/phases/state-diagram/index.js +53 -0
- package/dist/skills/phase/architecture-diagram/SKILL.md +41 -0
- package/dist/skills/phase/class-diagram/SKILL.md +44 -0
- package/dist/skills/phase/er-diagram/SKILL.md +71 -0
- package/dist/skills/phase/flowchart/SKILL.md +38 -0
- package/dist/skills/phase/sequence-diagram/SKILL.md +67 -0
- package/dist/skills/phase/state-diagram/SKILL.md +38 -0
- package/dist/workspace/session-workspace.d.ts +2 -2
- package/dist/workspace/session-workspace.js +2 -2
- package/package.json +1 -1
|
@@ -19,6 +19,27 @@
|
|
|
19
19
|
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
20
20
|
import { z } from 'zod';
|
|
21
21
|
import { RECIPE_CONTENT_MAX, RECIPE_EVIDENCE_MAX, RECIPE_NAME_MAX, RECIPE_SERVICE_NAME_MAX, RECIPE_SERVICES_MAX, RECIPE_SUMMARY_MAX, } from './types.js';
|
|
22
|
+
function resolveLinkScope(ctx) {
|
|
23
|
+
if (ctx.repositoryId) {
|
|
24
|
+
return {
|
|
25
|
+
table: 'repository_recipes',
|
|
26
|
+
column: 'repository_id',
|
|
27
|
+
id: ctx.repositoryId,
|
|
28
|
+
onConflict: 'repository_id,recipe_id',
|
|
29
|
+
label: 'this repository',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (ctx.productId) {
|
|
33
|
+
return {
|
|
34
|
+
table: 'product_recipes',
|
|
35
|
+
column: 'product_id',
|
|
36
|
+
id: ctx.productId,
|
|
37
|
+
onConflict: 'product_id,recipe_id',
|
|
38
|
+
label: 'this product',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
throw new Error('RecipesToolContext requires either productId or repositoryId to scope links');
|
|
42
|
+
}
|
|
22
43
|
export function createRecipesMutationCounts() {
|
|
23
44
|
return { created: 0, updated: 0, linked: 0, unlinked: 0 };
|
|
24
45
|
}
|
|
@@ -92,17 +113,18 @@ export function createCreateRecipeTool(ctx, counts, teamRecipeIds) {
|
|
|
92
113
|
return textError(`Failed to create recipe: ${insertRecipe.error?.message ?? 'unknown error'}`);
|
|
93
114
|
}
|
|
94
115
|
const recipeId = insertRecipe.data.id;
|
|
95
|
-
const
|
|
96
|
-
|
|
116
|
+
const scope = resolveLinkScope(ctx);
|
|
117
|
+
const linkErr = await ctx.supabase.from(scope.table).insert({
|
|
118
|
+
[scope.column]: scope.id,
|
|
97
119
|
recipe_id: recipeId,
|
|
98
120
|
evidence: args.evidence.trim(),
|
|
99
121
|
});
|
|
100
122
|
if (linkErr.error) {
|
|
101
|
-
return textError(`Recipe created (${recipeId}) but linking to
|
|
123
|
+
return textError(`Recipe created (${recipeId}) but linking to ${scope.label} failed: ${linkErr.error.message}`);
|
|
102
124
|
}
|
|
103
125
|
teamRecipeIds.add(recipeId);
|
|
104
126
|
counts.created += 1;
|
|
105
|
-
return textOk(`Created recipe ${recipeId} and linked to
|
|
127
|
+
return textOk(`Created recipe ${recipeId} and linked to ${scope.label}.`);
|
|
106
128
|
});
|
|
107
129
|
}
|
|
108
130
|
export function createUpdateRecipeTool(ctx, counts, teamRecipeIds) {
|
|
@@ -136,16 +158,17 @@ export function createUpdateRecipeTool(ctx, counts, teamRecipeIds) {
|
|
|
136
158
|
return textError(`Failed to update recipe: ${updateErr.error.message}`);
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
161
|
+
const scope = resolveLinkScope(ctx);
|
|
162
|
+
const linkErr = await ctx.supabase.from(scope.table).upsert({
|
|
163
|
+
[scope.column]: scope.id,
|
|
141
164
|
recipe_id: args.recipe_id,
|
|
142
165
|
evidence: args.evidence.trim(),
|
|
143
|
-
}, { onConflict:
|
|
166
|
+
}, { onConflict: scope.onConflict });
|
|
144
167
|
if (linkErr.error) {
|
|
145
|
-
return textError(`Recipe updated but linking to
|
|
168
|
+
return textError(`Recipe updated but linking to ${scope.label} failed: ${linkErr.error.message}`);
|
|
146
169
|
}
|
|
147
170
|
counts.updated += 1;
|
|
148
|
-
return textOk(`Updated recipe ${args.recipe_id} and linked to
|
|
171
|
+
return textOk(`Updated recipe ${args.recipe_id} and linked to ${scope.label}.`);
|
|
149
172
|
});
|
|
150
173
|
}
|
|
151
174
|
export function createLinkRecipeTool(ctx, counts, teamRecipeIds) {
|
|
@@ -156,36 +179,38 @@ export function createLinkRecipeTool(ctx, counts, teamRecipeIds) {
|
|
|
156
179
|
if (!teamRecipeIds.has(args.recipe_id)) {
|
|
157
180
|
return textError(`recipe_id ${args.recipe_id} is not in this team's recipe list. Use create_recipe for new entries.`);
|
|
158
181
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
182
|
+
const scope = resolveLinkScope(ctx);
|
|
183
|
+
const linkErr = await ctx.supabase.from(scope.table).upsert({
|
|
184
|
+
[scope.column]: scope.id,
|
|
161
185
|
recipe_id: args.recipe_id,
|
|
162
186
|
evidence: args.evidence.trim(),
|
|
163
|
-
}, { onConflict:
|
|
187
|
+
}, { onConflict: scope.onConflict });
|
|
164
188
|
if (linkErr.error) {
|
|
165
189
|
return textError(`Failed to link recipe: ${linkErr.error.message}`);
|
|
166
190
|
}
|
|
167
191
|
counts.linked += 1;
|
|
168
|
-
return textOk(`Linked recipe ${args.recipe_id} to
|
|
192
|
+
return textOk(`Linked recipe ${args.recipe_id} to ${scope.label}.`);
|
|
169
193
|
});
|
|
170
194
|
}
|
|
171
195
|
export function createUnlinkRecipeTool(ctx, counts, existingLinkIds) {
|
|
172
|
-
return tool('unlink_recipe', 'Drop a previously-linked recipe that you have confirmed is NO LONGER present in this repo. Only call this for ids in the "Currently linked
|
|
196
|
+
return tool('unlink_recipe', 'Drop a previously-linked recipe that you have confirmed is NO LONGER present in this repo. Only call this for ids in the "Currently linked" list. Does not delete the recipe itself.', {
|
|
173
197
|
recipe_id: z.string().uuid(),
|
|
174
198
|
}, async (args) => {
|
|
175
199
|
if (!existingLinkIds.has(args.recipe_id)) {
|
|
176
|
-
return textError(`recipe_id ${args.recipe_id} is not in the "Currently linked
|
|
200
|
+
return textError(`recipe_id ${args.recipe_id} is not in the "Currently linked" list — nothing to unlink.`);
|
|
177
201
|
}
|
|
202
|
+
const scope = resolveLinkScope(ctx);
|
|
178
203
|
const { error } = await ctx.supabase
|
|
179
|
-
.from(
|
|
204
|
+
.from(scope.table)
|
|
180
205
|
.delete()
|
|
181
|
-
.eq(
|
|
206
|
+
.eq(scope.column, scope.id)
|
|
182
207
|
.eq('recipe_id', args.recipe_id);
|
|
183
208
|
if (error) {
|
|
184
209
|
return textError(`Failed to unlink recipe: ${error.message}`);
|
|
185
210
|
}
|
|
186
211
|
existingLinkIds.delete(args.recipe_id);
|
|
187
212
|
counts.unlinked += 1;
|
|
188
|
-
return textOk(`Unlinked recipe ${args.recipe_id} from
|
|
213
|
+
return textOk(`Unlinked recipe ${args.recipe_id} from ${scope.label}.`);
|
|
189
214
|
});
|
|
190
215
|
}
|
|
191
216
|
export function createRecipesMcpServer(ctx, counts, teamRecipes, existingLinkIds) {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* screen-flow phase: clone the product's repo, ask Claude to map every
|
|
3
3
|
* user-facing screen and the transitions between them into a structured
|
|
4
|
-
* ScreenFlowExtraction, then persist the result to
|
|
5
|
-
*
|
|
4
|
+
* ScreenFlowExtraction, then persist the result to diagrams / diagram_nodes /
|
|
5
|
+
* diagram_edges (rows tagged `type = 'screen'`) via the Supabase SDK.
|
|
6
6
|
*
|
|
7
7
|
* Companion to find-architecture / find-bugs / find-features. Same workspace
|
|
8
8
|
* pattern, but writes to its own tables rather than filing issues.
|
|
9
9
|
*/
|
|
10
10
|
export interface ScreenFlowOptions {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** Product-scoped flow. Mutually exclusive with `repoId`. */
|
|
12
|
+
productId?: string;
|
|
13
|
+
/** Repo-only flow: a single repositories row, no product context. */
|
|
14
|
+
repoId?: string;
|
|
15
|
+
diagramId: string;
|
|
13
16
|
guidance?: string;
|
|
14
17
|
verbose?: boolean;
|
|
15
18
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* screen-flow phase: clone the product's repo, ask Claude to map every
|
|
3
3
|
* user-facing screen and the transitions between them into a structured
|
|
4
|
-
* ScreenFlowExtraction, then persist the result to
|
|
5
|
-
*
|
|
4
|
+
* ScreenFlowExtraction, then persist the result to diagrams / diagram_nodes /
|
|
5
|
+
* diagram_edges (rows tagged `type = 'screen'`) via the Supabase SDK.
|
|
6
6
|
*
|
|
7
7
|
* Companion to find-architecture / find-bugs / find-features. Same workspace
|
|
8
8
|
* pattern, but writes to its own tables rather than filing issues.
|
|
9
9
|
*/
|
|
10
10
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
11
|
+
import { getRepositoryBasics } from '../../api/github.js';
|
|
11
12
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
12
13
|
import { getSupabase } from '../../supabase/client.js';
|
|
13
14
|
import { logError, logInfo, logSuccess, logWarning } from '../../utils/logger.js';
|
|
14
15
|
import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
|
|
15
16
|
import { fetchProductBasics } from '../find-shared/mcp.js';
|
|
16
|
-
import {
|
|
17
|
+
import { cloneDiagramRepos, describeRepoScope } from '../diagram-shared/clone-repos.js';
|
|
17
18
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
18
19
|
import { createScreenFlowCaptureState, createScreenFlowMcpServer, validateConsistency, } from './mcp-server.js';
|
|
19
20
|
import { createScreenFlowSystemPrompt, createScreenFlowUserPrompt, } from './prompts.js';
|
|
@@ -26,25 +27,34 @@ const COLUMN_WIDTH = 380;
|
|
|
26
27
|
const ROW_HEIGHT = 480;
|
|
27
28
|
const COLUMNS = 4;
|
|
28
29
|
export async function runScreenFlowPhase(options) {
|
|
29
|
-
const { productId,
|
|
30
|
-
|
|
30
|
+
const { productId, repoId, diagramId, guidance, verbose } = options;
|
|
31
|
+
const repoOnly = !productId && Boolean(repoId);
|
|
32
|
+
if (productId) {
|
|
33
|
+
logInfo(`Starting screen-flow generation for product ${productId}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
logInfo(`Starting screen-flow generation for repository ${repoId}`);
|
|
37
|
+
}
|
|
31
38
|
const supabase = getSupabase();
|
|
32
|
-
await
|
|
33
|
-
const repositoryIds = await
|
|
34
|
-
const cloneResult = await
|
|
39
|
+
await markDiagramRunning(supabase, diagramId);
|
|
40
|
+
const repositoryIds = await getDiagramRepositoryIds(supabase, diagramId);
|
|
41
|
+
const cloneResult = await cloneDiagramRepos({
|
|
35
42
|
productId,
|
|
43
|
+
repoId,
|
|
36
44
|
repositoryIds,
|
|
37
45
|
workspaceKey: WORKSPACE_KEY,
|
|
38
46
|
verbose,
|
|
39
47
|
});
|
|
40
48
|
if (!cloneResult.ok) {
|
|
41
|
-
await
|
|
49
|
+
await markDiagramFailed(supabase, diagramId, cloneResult.message);
|
|
42
50
|
return { status: 'error', message: cloneResult.message };
|
|
43
51
|
}
|
|
44
52
|
const { projectDir, cleanupDir, repos } = cloneResult;
|
|
45
53
|
let succeeded = false;
|
|
46
54
|
try {
|
|
47
|
-
const product =
|
|
55
|
+
const product = repoOnly
|
|
56
|
+
? await resolveRepoBasics(repoId, repos)
|
|
57
|
+
: await fetchProductBasics(productId);
|
|
48
58
|
const systemPrompt = await createScreenFlowSystemPrompt({
|
|
49
59
|
projectDir,
|
|
50
60
|
hasCodebase: true,
|
|
@@ -94,17 +104,17 @@ export async function runScreenFlowPhase(options) {
|
|
|
94
104
|
}
|
|
95
105
|
if (!extraction) {
|
|
96
106
|
const msg = 'Screen flow extraction failed: agent did not call submit_screen_flow and no parseable screen_flow block was found in the response';
|
|
97
|
-
await
|
|
107
|
+
await markDiagramFailed(supabase, diagramId, msg);
|
|
98
108
|
return { status: 'error', message: msg };
|
|
99
109
|
}
|
|
100
110
|
logInfo(`Extraction produced ${extraction.nodes.length} screens / ${extraction.edges.length} transitions`);
|
|
101
111
|
const theme = resolveTheme(extraction.theme, repos);
|
|
102
112
|
if (Object.keys(theme).length > 0) {
|
|
103
113
|
logInfo(`Theme: ${Object.entries(theme).map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
104
|
-
await persistTheme(supabase,
|
|
114
|
+
await persistTheme(supabase, diagramId, theme);
|
|
105
115
|
}
|
|
106
|
-
const { nodesCreated, edgesCreated } = await
|
|
107
|
-
await
|
|
116
|
+
const { nodesCreated, edgesCreated } = await persistDiagram(supabase, diagramId, extraction);
|
|
117
|
+
await markDiagramSuccess(supabase, diagramId, extraction.summary);
|
|
108
118
|
succeeded = true;
|
|
109
119
|
logSuccess(`Screen flow generated: ${nodesCreated} screens, ${edgesCreated} transitions`);
|
|
110
120
|
return {
|
|
@@ -118,7 +128,7 @@ export async function runScreenFlowPhase(options) {
|
|
|
118
128
|
catch (error) {
|
|
119
129
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
120
130
|
logError(`Screen flow failed: ${errorMessage}`);
|
|
121
|
-
await
|
|
131
|
+
await markDiagramFailed(supabase, diagramId, errorMessage);
|
|
122
132
|
return { status: 'error', message: errorMessage };
|
|
123
133
|
}
|
|
124
134
|
finally {
|
|
@@ -148,12 +158,23 @@ function resolveTheme(agentTheme, repos) {
|
|
|
148
158
|
}
|
|
149
159
|
return {};
|
|
150
160
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Build "product basics" for repo-only mode from the repositories row,
|
|
163
|
+
* falling back to the cloned repo's full name when the row has no name.
|
|
164
|
+
*/
|
|
165
|
+
async function resolveRepoBasics(repositoryId, repos) {
|
|
166
|
+
const basics = await getRepositoryBasics(repositoryId).catch(() => null);
|
|
167
|
+
return {
|
|
168
|
+
name: basics?.fullName ?? repos[0]?.fullName ?? repositoryId,
|
|
169
|
+
description: basics?.description ?? undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Read the ordered repo set a diagram was scoped to (may be empty). */
|
|
173
|
+
async function getDiagramRepositoryIds(supabase, diagramId) {
|
|
153
174
|
const { data } = await supabase
|
|
154
|
-
.from('
|
|
175
|
+
.from('diagrams')
|
|
155
176
|
.select('repository_ids')
|
|
156
|
-
.eq('id',
|
|
177
|
+
.eq('id', diagramId)
|
|
157
178
|
.single();
|
|
158
179
|
return (data?.repository_ids ?? []).filter(Boolean);
|
|
159
180
|
}
|
|
@@ -213,31 +234,31 @@ function tryFallbackParse(resultMessage, assistantText) {
|
|
|
213
234
|
// ============================================================================
|
|
214
235
|
// Persistence
|
|
215
236
|
// ============================================================================
|
|
216
|
-
async function
|
|
237
|
+
async function markDiagramRunning(supabase, diagramId) {
|
|
217
238
|
const { error } = await supabase
|
|
218
|
-
.from('
|
|
239
|
+
.from('diagrams')
|
|
219
240
|
.update({ status: 'running', error: null })
|
|
220
|
-
.eq('id',
|
|
241
|
+
.eq('id', diagramId);
|
|
221
242
|
if (error) {
|
|
222
|
-
logWarning(`Could not mark
|
|
243
|
+
logWarning(`Could not mark diagram as running: ${error.message}`);
|
|
223
244
|
}
|
|
224
245
|
}
|
|
225
|
-
async function
|
|
246
|
+
async function markDiagramFailed(supabase, diagramId, errorMessage) {
|
|
226
247
|
await supabase
|
|
227
|
-
.from('
|
|
248
|
+
.from('diagrams')
|
|
228
249
|
.update({
|
|
229
250
|
status: 'failed',
|
|
230
251
|
error: errorMessage,
|
|
231
252
|
completed_at: new Date().toISOString(),
|
|
232
253
|
})
|
|
233
|
-
.eq('id',
|
|
254
|
+
.eq('id', diagramId);
|
|
234
255
|
}
|
|
235
|
-
async function persistTheme(supabase,
|
|
256
|
+
async function persistTheme(supabase, diagramId, theme) {
|
|
236
257
|
// Theme is screen-flow-specific; stash it inside the generic options JSONB.
|
|
237
258
|
const { data, error: readError } = await supabase
|
|
238
|
-
.from('
|
|
259
|
+
.from('diagrams')
|
|
239
260
|
.select('options')
|
|
240
|
-
.eq('id',
|
|
261
|
+
.eq('id', diagramId)
|
|
241
262
|
.single();
|
|
242
263
|
if (readError) {
|
|
243
264
|
logWarning(`Could not read flow options: ${readError.message}`);
|
|
@@ -248,34 +269,34 @@ async function persistTheme(supabase, flowId, theme) {
|
|
|
248
269
|
theme,
|
|
249
270
|
};
|
|
250
271
|
const { error } = await supabase
|
|
251
|
-
.from('
|
|
272
|
+
.from('diagrams')
|
|
252
273
|
.update({ options: nextOptions })
|
|
253
|
-
.eq('id',
|
|
274
|
+
.eq('id', diagramId);
|
|
254
275
|
if (error) {
|
|
255
276
|
logWarning(`Could not persist extracted theme: ${error.message}`);
|
|
256
277
|
}
|
|
257
278
|
}
|
|
258
|
-
async function
|
|
279
|
+
async function markDiagramSuccess(supabase, diagramId, summary) {
|
|
259
280
|
await supabase
|
|
260
|
-
.from('
|
|
281
|
+
.from('diagrams')
|
|
261
282
|
.update({
|
|
262
283
|
status: 'success',
|
|
263
284
|
summary,
|
|
264
285
|
error: null,
|
|
265
286
|
completed_at: new Date().toISOString(),
|
|
266
287
|
})
|
|
267
|
-
.eq('id',
|
|
288
|
+
.eq('id', diagramId);
|
|
268
289
|
}
|
|
269
|
-
async function
|
|
290
|
+
async function persistDiagram(supabase, diagramId, extraction) {
|
|
270
291
|
// Re-runs replace prior content for the same flow row.
|
|
271
|
-
await supabase.from('
|
|
272
|
-
await supabase.from('
|
|
292
|
+
await supabase.from('diagram_edges').delete().eq('diagram_id', diagramId);
|
|
293
|
+
await supabase.from('diagram_nodes').delete().eq('diagram_id', diagramId);
|
|
273
294
|
if (extraction.nodes.length === 0) {
|
|
274
295
|
return { nodesCreated: 0, edgesCreated: 0 };
|
|
275
296
|
}
|
|
276
|
-
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(
|
|
297
|
+
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(diagramId, n, i));
|
|
277
298
|
const { data: insertedNodes, error: nodesError } = await supabase
|
|
278
|
-
.from('
|
|
299
|
+
.from('diagram_nodes')
|
|
279
300
|
.insert(nodeRows)
|
|
280
301
|
.select('id, slug');
|
|
281
302
|
if (nodesError) {
|
|
@@ -283,11 +304,11 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
283
304
|
}
|
|
284
305
|
const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
|
|
285
306
|
const edgeRows = extraction.edges
|
|
286
|
-
.map((e) => buildEdgeRow(
|
|
307
|
+
.map((e) => buildEdgeRow(diagramId, e, slugToId))
|
|
287
308
|
.filter((e) => e !== null);
|
|
288
309
|
if (edgeRows.length > 0) {
|
|
289
310
|
const { error: edgesError } = await supabase
|
|
290
|
-
.from('
|
|
311
|
+
.from('diagram_edges')
|
|
291
312
|
.insert(edgeRows);
|
|
292
313
|
if (edgesError) {
|
|
293
314
|
throw new Error(`Failed to insert edges: ${edgesError.message}`);
|
|
@@ -298,9 +319,9 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
298
319
|
edgesCreated: edgeRows.length,
|
|
299
320
|
};
|
|
300
321
|
}
|
|
301
|
-
function buildNodeRow(
|
|
322
|
+
function buildNodeRow(diagramId, node, index) {
|
|
302
323
|
return {
|
|
303
|
-
|
|
324
|
+
diagram_id: diagramId,
|
|
304
325
|
slug: node.slug,
|
|
305
326
|
name: node.name,
|
|
306
327
|
kind: node.kind,
|
|
@@ -309,14 +330,14 @@ function buildNodeRow(flowId, node, index) {
|
|
|
309
330
|
position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
|
|
310
331
|
};
|
|
311
332
|
}
|
|
312
|
-
function buildEdgeRow(
|
|
333
|
+
function buildEdgeRow(diagramId, edge, slugToId) {
|
|
313
334
|
const fromId = slugToId.get(edge.fromSlug);
|
|
314
335
|
const toId = slugToId.get(edge.toSlug);
|
|
315
336
|
if (!fromId || !toId) {
|
|
316
337
|
return null;
|
|
317
338
|
}
|
|
318
339
|
return {
|
|
319
|
-
|
|
340
|
+
diagram_id: diagramId,
|
|
320
341
|
from_node_id: fromId,
|
|
321
342
|
to_node_id: toId,
|
|
322
343
|
label: edge.triggerLabel,
|
|
@@ -166,7 +166,7 @@ export function validateConsistency(extraction) {
|
|
|
166
166
|
for (const node of extraction.nodes) {
|
|
167
167
|
if (slugs.has(node.slug)) {
|
|
168
168
|
return {
|
|
169
|
-
error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the
|
|
169
|
+
error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the diagram. Re-call submit_screen_flow with deduplicated nodes.`,
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
slugs.add(node.slug);
|
|
@@ -208,7 +208,7 @@ export function createSubmitScreenFlowTool(state) {
|
|
|
208
208
|
.describe('1-3 sentence narrative of what kind of app this is and its primary user flows.'),
|
|
209
209
|
nodes: z
|
|
210
210
|
.array(screenNodeSchema)
|
|
211
|
-
.describe('Every user-facing screen, modal, drawer, tab, or named state. node.slug MUST be unique within the
|
|
211
|
+
.describe('Every user-facing screen, modal, drawer, tab, or named state. node.slug MUST be unique within the diagram.'),
|
|
212
212
|
edges: z
|
|
213
213
|
.array(screenEdgeSchema)
|
|
214
214
|
.describe('Transitions between screens. Every fromSlug / toSlug MUST reference a slug present in nodes; drop edges whose endpoints you did not emit.'),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequence-diagram phase: clone the product's repo, ask Claude to trace one
|
|
3
|
+
* scenario through the code and map the participants (actor / service /
|
|
4
|
+
* component / database / queue / external) and the ordered messages between
|
|
5
|
+
* them into a structured SequenceDiagramExtraction, then persist the result
|
|
6
|
+
* to diagrams / diagram_nodes / diagram_edges (rows tagged `type = 'sequence'`) via the
|
|
7
|
+
* Supabase SDK.
|
|
8
|
+
*
|
|
9
|
+
* Companion to data-flow / screen-flow / er-diagram: same generation pattern
|
|
10
|
+
* (workspace clone + Claude Agent SDK + in-process MCP server), same storage
|
|
11
|
+
* tables, different domain. Message ordering is persisted in the edge's
|
|
12
|
+
* `metadata.order`.
|
|
13
|
+
*/
|
|
14
|
+
export interface SequenceDiagramPhaseOptions {
|
|
15
|
+
/** Product-scoped diagram. Mutually exclusive with `repoId`. */
|
|
16
|
+
productId?: string;
|
|
17
|
+
/** Repo-only diagram: a single repositories row, no product context. */
|
|
18
|
+
repoId?: string;
|
|
19
|
+
diagramId: string;
|
|
20
|
+
guidance?: string;
|
|
21
|
+
verbose?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SequenceDiagramPhaseResult {
|
|
24
|
+
status: 'success' | 'error';
|
|
25
|
+
message: string;
|
|
26
|
+
nodesCreated?: number;
|
|
27
|
+
edgesCreated?: number;
|
|
28
|
+
summary?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function runSequenceDiagramPhase(options: SequenceDiagramPhaseOptions): Promise<SequenceDiagramPhaseResult>;
|