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.
Files changed (83) hide show
  1. package/dist/api/github.d.ts +23 -0
  2. package/dist/api/github.js +61 -0
  3. package/dist/commands/architecture-diagram/index.d.ts +8 -0
  4. package/dist/commands/architecture-diagram/index.js +10 -0
  5. package/dist/commands/class-diagram/index.d.ts +7 -0
  6. package/dist/commands/class-diagram/index.js +9 -0
  7. package/dist/commands/data-flow/index.d.ts +8 -6
  8. package/dist/commands/data-flow/index.js +21 -12
  9. package/dist/commands/diagram-shared/index.d.ts +21 -0
  10. package/dist/commands/diagram-shared/index.js +37 -0
  11. package/dist/commands/er-diagram/index.d.ts +19 -0
  12. package/dist/commands/er-diagram/index.js +55 -0
  13. package/dist/commands/flowchart/index.d.ts +8 -0
  14. package/dist/commands/flowchart/index.js +10 -0
  15. package/dist/commands/quality-benchmark/index.js +43 -9
  16. package/dist/commands/recipes/index.d.ts +3 -1
  17. package/dist/commands/recipes/index.js +10 -4
  18. package/dist/commands/screen-flow/index.d.ts +8 -6
  19. package/dist/commands/screen-flow/index.js +21 -12
  20. package/dist/commands/sequence-diagram/index.d.ts +19 -0
  21. package/dist/commands/sequence-diagram/index.js +55 -0
  22. package/dist/commands/state-diagram/index.d.ts +7 -0
  23. package/dist/commands/state-diagram/index.js +9 -0
  24. package/dist/index.js +144 -14
  25. package/dist/phases/architecture-diagram/index.d.ts +15 -0
  26. package/dist/phases/architecture-diagram/index.js +51 -0
  27. package/dist/phases/class-diagram/index.d.ts +14 -0
  28. package/dist/phases/class-diagram/index.js +76 -0
  29. package/dist/phases/data-flow/index.d.ts +6 -3
  30. package/dist/phases/data-flow/index.js +59 -38
  31. package/dist/phases/data-flow/mcp-server.d.ts +1 -1
  32. package/dist/phases/data-flow/mcp-server.js +2 -2
  33. package/dist/phases/data-flow/types.d.ts +1 -1
  34. package/dist/phases/data-flow/types.js +1 -1
  35. package/dist/phases/diagram-shared/clone-repos.d.ts +63 -0
  36. package/dist/phases/diagram-shared/clone-repos.js +153 -0
  37. package/dist/phases/diagram-shared/generate.d.ts +42 -0
  38. package/dist/phases/diagram-shared/generate.js +162 -0
  39. package/dist/phases/diagram-shared/graph.d.ts +62 -0
  40. package/dist/phases/diagram-shared/graph.js +169 -0
  41. package/dist/phases/diagram-shared/mcp.d.ts +35 -0
  42. package/dist/phases/diagram-shared/mcp.js +68 -0
  43. package/dist/phases/diagram-shared/prompts.d.ts +23 -0
  44. package/dist/phases/diagram-shared/prompts.js +35 -0
  45. package/dist/phases/er-diagram/index.d.ts +28 -0
  46. package/dist/phases/er-diagram/index.js +290 -0
  47. package/dist/phases/er-diagram/mcp-server.d.ts +77 -0
  48. package/dist/phases/er-diagram/mcp-server.js +144 -0
  49. package/dist/phases/er-diagram/prompts.d.ts +14 -0
  50. package/dist/phases/er-diagram/prompts.js +36 -0
  51. package/dist/phases/er-diagram/types.d.ts +76 -0
  52. package/dist/phases/er-diagram/types.js +84 -0
  53. package/dist/phases/flow-shared/clone-repos.d.ts +8 -2
  54. package/dist/phases/flow-shared/clone-repos.js +36 -18
  55. package/dist/phases/flowchart/index.d.ts +15 -0
  56. package/dist/phases/flowchart/index.js +50 -0
  57. package/dist/phases/output-contracts.js +178 -2
  58. package/dist/phases/recipes/index.d.ts +8 -1
  59. package/dist/phases/recipes/index.js +74 -17
  60. package/dist/phases/recipes/mcp-server.d.ts +4 -1
  61. package/dist/phases/recipes/mcp-server.js +43 -18
  62. package/dist/phases/screen-flow/index.d.ts +7 -4
  63. package/dist/phases/screen-flow/index.js +66 -45
  64. package/dist/phases/screen-flow/mcp-server.js +2 -2
  65. package/dist/phases/sequence-diagram/index.d.ts +30 -0
  66. package/dist/phases/sequence-diagram/index.js +290 -0
  67. package/dist/phases/sequence-diagram/mcp-server.d.ts +64 -0
  68. package/dist/phases/sequence-diagram/mcp-server.js +134 -0
  69. package/dist/phases/sequence-diagram/prompts.d.ts +14 -0
  70. package/dist/phases/sequence-diagram/prompts.js +36 -0
  71. package/dist/phases/sequence-diagram/types.d.ts +52 -0
  72. package/dist/phases/sequence-diagram/types.js +93 -0
  73. package/dist/phases/state-diagram/index.d.ts +15 -0
  74. package/dist/phases/state-diagram/index.js +53 -0
  75. package/dist/skills/phase/architecture-diagram/SKILL.md +41 -0
  76. package/dist/skills/phase/class-diagram/SKILL.md +44 -0
  77. package/dist/skills/phase/er-diagram/SKILL.md +71 -0
  78. package/dist/skills/phase/flowchart/SKILL.md +38 -0
  79. package/dist/skills/phase/sequence-diagram/SKILL.md +67 -0
  80. package/dist/skills/phase/state-diagram/SKILL.md +38 -0
  81. package/dist/workspace/session-workspace.d.ts +2 -2
  82. package/dist/workspace/session-workspace.js +2 -2
  83. 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 linkErr = await ctx.supabase.from('product_recipes').insert({
96
- product_id: ctx.productId,
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 product failed: ${linkErr.error.message}`);
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 this product.`);
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 linkErr = await ctx.supabase.from('product_recipes').upsert({
140
- product_id: ctx.productId,
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: 'product_id,recipe_id' });
166
+ }, { onConflict: scope.onConflict });
144
167
  if (linkErr.error) {
145
- return textError(`Recipe updated but linking to product failed: ${linkErr.error.message}`);
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 this product.`);
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 linkErr = await ctx.supabase.from('product_recipes').upsert({
160
- product_id: ctx.productId,
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: 'product_id,recipe_id' });
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 this product.`);
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 to this product" list. Does not delete the recipe itself.', {
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 to this product" list — nothing to unlink.`);
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('product_recipes')
204
+ .from(scope.table)
180
205
  .delete()
181
- .eq('product_id', ctx.productId)
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 this product.`);
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 flows / flow_nodes /
5
- * flow_edges (rows tagged `type = 'screen'`) via the Supabase SDK.
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
- productId: string;
12
- flowId: string;
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 flows / flow_nodes /
5
- * flow_edges (rows tagged `type = 'screen'`) via the Supabase SDK.
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 { cloneFlowRepos, describeRepoScope } from '../flow-shared/clone-repos.js';
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, flowId, guidance, verbose } = options;
30
- logInfo(`Starting screen-flow generation for product ${productId}`);
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 markFlowRunning(supabase, flowId);
33
- const repositoryIds = await getFlowRepositoryIds(supabase, flowId);
34
- const cloneResult = await cloneFlowRepos({
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 markFlowFailed(supabase, flowId, cloneResult.message);
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 = await fetchProductBasics(productId);
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 markFlowFailed(supabase, flowId, msg);
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, flowId, theme);
114
+ await persistTheme(supabase, diagramId, theme);
105
115
  }
106
- const { nodesCreated, edgesCreated } = await persistFlow(supabase, flowId, extraction);
107
- await markFlowSuccess(supabase, flowId, extraction.summary);
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 markFlowFailed(supabase, flowId, errorMessage);
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
- /** Read the ordered repo set a flow was scoped to (may be empty). */
152
- async function getFlowRepositoryIds(supabase, flowId) {
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('flows')
175
+ .from('diagrams')
155
176
  .select('repository_ids')
156
- .eq('id', flowId)
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 markFlowRunning(supabase, flowId) {
237
+ async function markDiagramRunning(supabase, diagramId) {
217
238
  const { error } = await supabase
218
- .from('flows')
239
+ .from('diagrams')
219
240
  .update({ status: 'running', error: null })
220
- .eq('id', flowId);
241
+ .eq('id', diagramId);
221
242
  if (error) {
222
- logWarning(`Could not mark flow as running: ${error.message}`);
243
+ logWarning(`Could not mark diagram as running: ${error.message}`);
223
244
  }
224
245
  }
225
- async function markFlowFailed(supabase, flowId, errorMessage) {
246
+ async function markDiagramFailed(supabase, diagramId, errorMessage) {
226
247
  await supabase
227
- .from('flows')
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', flowId);
254
+ .eq('id', diagramId);
234
255
  }
235
- async function persistTheme(supabase, flowId, theme) {
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('flows')
259
+ .from('diagrams')
239
260
  .select('options')
240
- .eq('id', flowId)
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('flows')
272
+ .from('diagrams')
252
273
  .update({ options: nextOptions })
253
- .eq('id', flowId);
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 markFlowSuccess(supabase, flowId, summary) {
279
+ async function markDiagramSuccess(supabase, diagramId, summary) {
259
280
  await supabase
260
- .from('flows')
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', flowId);
288
+ .eq('id', diagramId);
268
289
  }
269
- async function persistFlow(supabase, flowId, extraction) {
290
+ async function persistDiagram(supabase, diagramId, extraction) {
270
291
  // Re-runs replace prior content for the same flow row.
271
- await supabase.from('flow_edges').delete().eq('flow_id', flowId);
272
- await supabase.from('flow_nodes').delete().eq('flow_id', flowId);
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(flowId, n, i));
297
+ const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(diagramId, n, i));
277
298
  const { data: insertedNodes, error: nodesError } = await supabase
278
- .from('flow_nodes')
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(flowId, e, slugToId))
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('flow_edges')
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(flowId, node, index) {
322
+ function buildNodeRow(diagramId, node, index) {
302
323
  return {
303
- flow_id: flowId,
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(flowId, edge, slugToId) {
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
- flow_id: flowId,
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 flow. Re-call submit_screen_flow with deduplicated nodes.`,
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 flow.'),
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>;