edsger 0.69.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 (74) hide show
  1. package/dist/api/github.d.ts +1 -1
  2. package/dist/api/github.js +1 -1
  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 +5 -5
  8. package/dist/commands/data-flow/index.js +8 -8
  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/screen-flow/index.d.ts +5 -5
  16. package/dist/commands/screen-flow/index.js +8 -8
  17. package/dist/commands/sequence-diagram/index.d.ts +19 -0
  18. package/dist/commands/sequence-diagram/index.js +55 -0
  19. package/dist/commands/state-diagram/index.d.ts +7 -0
  20. package/dist/commands/state-diagram/index.js +9 -0
  21. package/dist/index.js +117 -5
  22. package/dist/phases/architecture-diagram/index.d.ts +15 -0
  23. package/dist/phases/architecture-diagram/index.js +51 -0
  24. package/dist/phases/class-diagram/index.d.ts +14 -0
  25. package/dist/phases/class-diagram/index.js +76 -0
  26. package/dist/phases/data-flow/index.d.ts +2 -2
  27. package/dist/phases/data-flow/index.js +36 -36
  28. package/dist/phases/data-flow/mcp-server.d.ts +1 -1
  29. package/dist/phases/data-flow/mcp-server.js +2 -2
  30. package/dist/phases/data-flow/types.d.ts +1 -1
  31. package/dist/phases/data-flow/types.js +1 -1
  32. package/dist/phases/diagram-shared/clone-repos.d.ts +63 -0
  33. package/dist/phases/diagram-shared/clone-repos.js +153 -0
  34. package/dist/phases/diagram-shared/generate.d.ts +42 -0
  35. package/dist/phases/diagram-shared/generate.js +162 -0
  36. package/dist/phases/diagram-shared/graph.d.ts +62 -0
  37. package/dist/phases/diagram-shared/graph.js +169 -0
  38. package/dist/phases/diagram-shared/mcp.d.ts +35 -0
  39. package/dist/phases/diagram-shared/mcp.js +68 -0
  40. package/dist/phases/diagram-shared/prompts.d.ts +23 -0
  41. package/dist/phases/diagram-shared/prompts.js +35 -0
  42. package/dist/phases/er-diagram/index.d.ts +28 -0
  43. package/dist/phases/er-diagram/index.js +290 -0
  44. package/dist/phases/er-diagram/mcp-server.d.ts +77 -0
  45. package/dist/phases/er-diagram/mcp-server.js +144 -0
  46. package/dist/phases/er-diagram/prompts.d.ts +14 -0
  47. package/dist/phases/er-diagram/prompts.js +36 -0
  48. package/dist/phases/er-diagram/types.d.ts +76 -0
  49. package/dist/phases/er-diagram/types.js +84 -0
  50. package/dist/phases/flowchart/index.d.ts +15 -0
  51. package/dist/phases/flowchart/index.js +50 -0
  52. package/dist/phases/output-contracts.js +178 -2
  53. package/dist/phases/screen-flow/index.d.ts +3 -3
  54. package/dist/phases/screen-flow/index.js +43 -43
  55. package/dist/phases/screen-flow/mcp-server.js +2 -2
  56. package/dist/phases/sequence-diagram/index.d.ts +30 -0
  57. package/dist/phases/sequence-diagram/index.js +290 -0
  58. package/dist/phases/sequence-diagram/mcp-server.d.ts +64 -0
  59. package/dist/phases/sequence-diagram/mcp-server.js +134 -0
  60. package/dist/phases/sequence-diagram/prompts.d.ts +14 -0
  61. package/dist/phases/sequence-diagram/prompts.js +36 -0
  62. package/dist/phases/sequence-diagram/types.d.ts +52 -0
  63. package/dist/phases/sequence-diagram/types.js +93 -0
  64. package/dist/phases/state-diagram/index.d.ts +15 -0
  65. package/dist/phases/state-diagram/index.js +53 -0
  66. package/dist/skills/phase/architecture-diagram/SKILL.md +41 -0
  67. package/dist/skills/phase/class-diagram/SKILL.md +44 -0
  68. package/dist/skills/phase/er-diagram/SKILL.md +71 -0
  69. package/dist/skills/phase/flowchart/SKILL.md +38 -0
  70. package/dist/skills/phase/sequence-diagram/SKILL.md +67 -0
  71. package/dist/skills/phase/state-diagram/SKILL.md +38 -0
  72. package/dist/workspace/session-workspace.d.ts +2 -2
  73. package/dist/workspace/session-workspace.js +2 -2
  74. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * architecture-diagram phase: map a component/dependency diagram (C4-ish) —
3
+ * the modules / services / packages / datastores / external systems that make
4
+ * up the product and how they depend on one another. Persisted to the
5
+ * diagrams tables with `type = 'architecture'`.
6
+ */
7
+ import { z } from 'zod';
8
+ import { generateDiagram, } from '../diagram-shared/generate.js';
9
+ import { buildDiagramSystemPrompt, buildDiagramUserPrompt, } from '../diagram-shared/prompts.js';
10
+ const archNode = z.object({
11
+ slug: z.string().min(1),
12
+ name: z.string().min(1),
13
+ kind: z.enum(['module', 'service', 'package', 'ui', 'datastore', 'external']),
14
+ file: z.string().optional(),
15
+ description: z.string().optional(),
16
+ tech: z.string().optional(),
17
+ responsibilities: z.array(z.string()).optional(),
18
+ });
19
+ const archEdge = z.object({
20
+ fromSlug: z.string().min(1),
21
+ toSlug: z.string().min(1),
22
+ kind: z.enum(['depends-on', 'calls', 'imports', 'data']),
23
+ label: z.string().optional(),
24
+ sourceFile: z.string().optional(),
25
+ });
26
+ export function runArchitectureDiagramPhase(options) {
27
+ return generateDiagram({
28
+ ...options,
29
+ workspaceKey: 'architecture-diagram',
30
+ fenceName: 'architecture_diagram',
31
+ nounPlural: 'components',
32
+ edgeNounPlural: 'dependencies',
33
+ mcpConfig: {
34
+ name: 'architecture-diagram',
35
+ toolName: 'architecture_diagram',
36
+ summaryDescribe: '1-3 sentence narrative of the system architecture and its main building blocks.',
37
+ nodesSchema: z.array(archNode),
38
+ nodesDescribe: 'Every component: module / service / package / ui / datastore / external. slug MUST be unique.',
39
+ edgesSchema: z.array(archEdge),
40
+ edgesDescribe: 'Dependencies. kind = depends-on / calls / imports / data. fromSlug is the depender. Endpoints MUST reference emitted components.',
41
+ },
42
+ buildSystemPrompt: (a) => buildDiagramSystemPrompt('phase/architecture-diagram', 'architecture-diagram', a),
43
+ buildUserPrompt: (a) => buildDiagramUserPrompt({
44
+ ...a,
45
+ task: 'Map the architecture (component & dependency) diagram for',
46
+ mcpName: 'architecture-diagram',
47
+ toolName: 'architecture_diagram',
48
+ process: 'Detect the stack and the top-level structure (workspaces/packages, service boundaries, top-level src dirs, deployment units). Treat each meaningful unit as a component of the right kind (a UI/front-end, backend services, internal modules/packages, datastores, third-party/external systems). Wire dependency edges from the import graph, cross-service calls, and datastore/external access. Aim for a readable ~10-25 components — group leaf files into their module; do not draw a file-level graph.',
49
+ }),
50
+ });
51
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * class-diagram phase: map a UML class diagram — classes / interfaces / enums
3
+ * with their members, and the inheritance / composition / association edges
4
+ * between them. Persisted to the diagrams tables with `type = 'class'`.
5
+ */
6
+ import { type DiagramPhaseResult } from '../diagram-shared/generate.js';
7
+ export interface ClassDiagramPhaseOptions {
8
+ productId?: string;
9
+ repoId?: string;
10
+ diagramId: string;
11
+ guidance?: string;
12
+ verbose?: boolean;
13
+ }
14
+ export declare function runClassDiagramPhase(options: ClassDiagramPhaseOptions): Promise<DiagramPhaseResult>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * class-diagram phase: map a UML class diagram — classes / interfaces / enums
3
+ * with their members, and the inheritance / composition / association edges
4
+ * between them. Persisted to the diagrams tables with `type = 'class'`.
5
+ */
6
+ import { z } from 'zod';
7
+ import { generateDiagram, } from '../diagram-shared/generate.js';
8
+ import { buildDiagramSystemPrompt, buildDiagramUserPrompt, } from '../diagram-shared/prompts.js';
9
+ const visibility = z.enum(['public', 'private', 'protected']);
10
+ const classNode = z.object({
11
+ slug: z.string().min(1),
12
+ name: z.string().min(1),
13
+ kind: z.enum(['class', 'interface', 'abstract', 'enum']),
14
+ file: z.string().optional(),
15
+ description: z.string().optional(),
16
+ stereotype: z.string().optional(),
17
+ attributes: z
18
+ .array(z.object({
19
+ name: z.string(),
20
+ type: z.string().optional(),
21
+ visibility: visibility.optional(),
22
+ isStatic: z.boolean().optional(),
23
+ }))
24
+ .optional(),
25
+ methods: z
26
+ .array(z.object({
27
+ name: z.string(),
28
+ params: z.string().optional(),
29
+ returnType: z.string().optional(),
30
+ visibility: visibility.optional(),
31
+ isStatic: z.boolean().optional(),
32
+ isAbstract: z.boolean().optional(),
33
+ }))
34
+ .optional(),
35
+ });
36
+ const classEdge = z.object({
37
+ fromSlug: z.string().min(1),
38
+ toSlug: z.string().min(1),
39
+ kind: z.enum([
40
+ 'inheritance',
41
+ 'implementation',
42
+ 'composition',
43
+ 'aggregation',
44
+ 'association',
45
+ 'dependency',
46
+ ]),
47
+ /** Role name or multiplicity, e.g. "1..*", "owns". */
48
+ label: z.string().optional(),
49
+ sourceFile: z.string().optional(),
50
+ });
51
+ export function runClassDiagramPhase(options) {
52
+ return generateDiagram({
53
+ ...options,
54
+ workspaceKey: 'class-diagram',
55
+ fenceName: 'class_diagram',
56
+ nounPlural: 'classes',
57
+ edgeNounPlural: 'relationships',
58
+ mcpConfig: {
59
+ name: 'class-diagram',
60
+ toolName: 'class_diagram',
61
+ summaryDescribe: '1-3 sentence narrative of the core types and how they relate.',
62
+ nodesSchema: z.array(classNode),
63
+ nodesDescribe: 'Every class / interface / abstract / enum with its key attributes and methods. slug MUST be unique.',
64
+ edgesSchema: z.array(classEdge),
65
+ edgesDescribe: 'Relationships. kind = inheritance / implementation / composition / aggregation / association / dependency. fromSlug is the child / owner / dependent side. Endpoints MUST reference emitted classes.',
66
+ },
67
+ buildSystemPrompt: (a) => buildDiagramSystemPrompt('phase/class-diagram', 'class-diagram', a),
68
+ buildUserPrompt: (a) => buildDiagramUserPrompt({
69
+ ...a,
70
+ task: 'Map a UML class diagram for',
71
+ mcpName: 'class-diagram',
72
+ toolName: 'class_diagram',
73
+ process: 'Detect the language/OO model, then map the core domain types — classes, interfaces, abstract base types, enums — capturing each one\'s defining attributes and methods (visibility + static/abstract where it matters; you need not list every member of huge types). Wire edges: `inheritance` (extends), `implementation` (implements), `composition`/`aggregation` (owns/holds), `association` (references), `dependency` (uses). Prefer the ~30 most important types if there are many.',
74
+ }),
75
+ });
76
+ }
@@ -2,7 +2,7 @@
2
2
  * data-flow phase: clone the product's repo, ask Claude to map every data
3
3
  * node (source / dataset / transform / sink / queue / model) and the
4
4
  * connections between them into a structured DataFlowExtraction, then
5
- * persist the result to flows / flow_nodes / flow_edges (rows tagged
5
+ * persist the result to diagrams / diagram_nodes / diagram_edges (rows tagged
6
6
  * `type = 'data'`) via the Supabase SDK.
7
7
  *
8
8
  * Companion to screen-flow: same generation pattern (workspace clone +
@@ -14,7 +14,7 @@ export interface DataFlowPhaseOptions {
14
14
  productId?: string;
15
15
  /** Repo-only flow: a single repositories row, no product context. */
16
16
  repoId?: string;
17
- flowId: string;
17
+ diagramId: string;
18
18
  guidance?: string;
19
19
  verbose?: boolean;
20
20
  }
@@ -2,7 +2,7 @@
2
2
  * data-flow phase: clone the product's repo, ask Claude to map every data
3
3
  * node (source / dataset / transform / sink / queue / model) and the
4
4
  * connections between them into a structured DataFlowExtraction, then
5
- * persist the result to flows / flow_nodes / flow_edges (rows tagged
5
+ * persist the result to diagrams / diagram_nodes / diagram_edges (rows tagged
6
6
  * `type = 'data'`) via the Supabase SDK.
7
7
  *
8
8
  * Companion to screen-flow: same generation pattern (workspace clone +
@@ -16,7 +16,7 @@ import { getSupabase } from '../../supabase/client.js';
16
16
  import { logError, logInfo, logSuccess, logWarning } from '../../utils/logger.js';
17
17
  import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
18
18
  import { fetchProductBasics } from '../find-shared/mcp.js';
19
- import { cloneFlowRepos, describeRepoScope } from '../flow-shared/clone-repos.js';
19
+ import { cloneDiagramRepos, describeRepoScope } from '../diagram-shared/clone-repos.js';
20
20
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
21
21
  import { createDataFlowCaptureState, createDataFlowMcpServer, validateConsistency, } from './mcp-server.js';
22
22
  import { createDataFlowSystemPrompt, createDataFlowUserPrompt, } from './prompts.js';
@@ -28,7 +28,7 @@ const COLUMN_WIDTH = 320;
28
28
  const ROW_HEIGHT = 220;
29
29
  const COLUMNS = 4;
30
30
  export async function runDataFlowPhase(options) {
31
- const { productId, repoId, flowId, guidance, verbose } = options;
31
+ const { productId, repoId, diagramId, guidance, verbose } = options;
32
32
  const repoOnly = !productId && Boolean(repoId);
33
33
  if (productId) {
34
34
  logInfo(`Starting data-flow generation for product ${productId}`);
@@ -37,9 +37,9 @@ export async function runDataFlowPhase(options) {
37
37
  logInfo(`Starting data-flow generation for repository ${repoId}`);
38
38
  }
39
39
  const supabase = getSupabase();
40
- await markFlowRunning(supabase, flowId);
41
- const repositoryIds = await getFlowRepositoryIds(supabase, flowId);
42
- const cloneResult = await cloneFlowRepos({
40
+ await markDiagramRunning(supabase, diagramId);
41
+ const repositoryIds = await getDiagramRepositoryIds(supabase, diagramId);
42
+ const cloneResult = await cloneDiagramRepos({
43
43
  productId,
44
44
  repoId,
45
45
  repositoryIds,
@@ -47,7 +47,7 @@ export async function runDataFlowPhase(options) {
47
47
  verbose,
48
48
  });
49
49
  if (!cloneResult.ok) {
50
- await markFlowFailed(supabase, flowId, cloneResult.message);
50
+ await markDiagramFailed(supabase, diagramId, cloneResult.message);
51
51
  return { status: 'error', message: cloneResult.message };
52
52
  }
53
53
  const { projectDir, cleanupDir, repos } = cloneResult;
@@ -100,12 +100,12 @@ export async function runDataFlowPhase(options) {
100
100
  }
101
101
  if (!extraction) {
102
102
  const msg = 'Data flow extraction failed: agent did not call submit_data_flow and no parseable data_flow block was found in the response';
103
- await markFlowFailed(supabase, flowId, msg);
103
+ await markDiagramFailed(supabase, diagramId, msg);
104
104
  return { status: 'error', message: msg };
105
105
  }
106
106
  logInfo(`Extraction produced ${extraction.nodes.length} nodes / ${extraction.edges.length} connections`);
107
- const { nodesCreated, edgesCreated } = await persistFlow(supabase, flowId, extraction);
108
- await markFlowSuccess(supabase, flowId, extraction.summary);
107
+ const { nodesCreated, edgesCreated } = await persistDiagram(supabase, diagramId, extraction);
108
+ await markDiagramSuccess(supabase, diagramId, extraction.summary);
109
109
  succeeded = true;
110
110
  logSuccess(`Data flow generated: ${nodesCreated} nodes, ${edgesCreated} connections`);
111
111
  return {
@@ -119,7 +119,7 @@ export async function runDataFlowPhase(options) {
119
119
  catch (error) {
120
120
  const errorMessage = error instanceof Error ? error.message : String(error);
121
121
  logError(`Data flow failed: ${errorMessage}`);
122
- await markFlowFailed(supabase, flowId, errorMessage);
122
+ await markDiagramFailed(supabase, diagramId, errorMessage);
123
123
  return { status: 'error', message: errorMessage };
124
124
  }
125
125
  finally {
@@ -139,12 +139,12 @@ async function resolveRepoBasics(repositoryId, repos) {
139
139
  description: basics?.description ?? undefined,
140
140
  };
141
141
  }
142
- /** Read the ordered repo set a flow was scoped to (may be empty). */
143
- async function getFlowRepositoryIds(supabase, flowId) {
142
+ /** Read the ordered repo set a diagram was scoped to (may be empty). */
143
+ async function getDiagramRepositoryIds(supabase, diagramId) {
144
144
  const { data } = await supabase
145
- .from('flows')
145
+ .from('diagrams')
146
146
  .select('repository_ids')
147
- .eq('id', flowId)
147
+ .eq('id', diagramId)
148
148
  .single();
149
149
  return (data?.repository_ids ?? []).filter(Boolean);
150
150
  }
@@ -197,45 +197,45 @@ function tryFallbackParse(resultMessage, assistantText) {
197
197
  // ============================================================================
198
198
  // Persistence
199
199
  // ============================================================================
200
- async function markFlowRunning(supabase, flowId) {
200
+ async function markDiagramRunning(supabase, diagramId) {
201
201
  const { error } = await supabase
202
- .from('flows')
202
+ .from('diagrams')
203
203
  .update({ status: 'running', error: null })
204
- .eq('id', flowId);
204
+ .eq('id', diagramId);
205
205
  if (error) {
206
- logWarning(`Could not mark flow as running: ${error.message}`);
206
+ logWarning(`Could not mark diagram as running: ${error.message}`);
207
207
  }
208
208
  }
209
- async function markFlowFailed(supabase, flowId, errorMessage) {
209
+ async function markDiagramFailed(supabase, diagramId, errorMessage) {
210
210
  await supabase
211
- .from('flows')
211
+ .from('diagrams')
212
212
  .update({
213
213
  status: 'failed',
214
214
  error: errorMessage,
215
215
  completed_at: new Date().toISOString(),
216
216
  })
217
- .eq('id', flowId);
217
+ .eq('id', diagramId);
218
218
  }
219
- async function markFlowSuccess(supabase, flowId, summary) {
219
+ async function markDiagramSuccess(supabase, diagramId, summary) {
220
220
  await supabase
221
- .from('flows')
221
+ .from('diagrams')
222
222
  .update({
223
223
  status: 'success',
224
224
  summary,
225
225
  error: null,
226
226
  completed_at: new Date().toISOString(),
227
227
  })
228
- .eq('id', flowId);
228
+ .eq('id', diagramId);
229
229
  }
230
- async function persistFlow(supabase, flowId, extraction) {
231
- await supabase.from('flow_edges').delete().eq('flow_id', flowId);
232
- await supabase.from('flow_nodes').delete().eq('flow_id', flowId);
230
+ async function persistDiagram(supabase, diagramId, extraction) {
231
+ await supabase.from('diagram_edges').delete().eq('diagram_id', diagramId);
232
+ await supabase.from('diagram_nodes').delete().eq('diagram_id', diagramId);
233
233
  if (extraction.nodes.length === 0) {
234
234
  return { nodesCreated: 0, edgesCreated: 0 };
235
235
  }
236
- const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(flowId, n, i));
236
+ const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(diagramId, n, i));
237
237
  const { data: insertedNodes, error: nodesError } = await supabase
238
- .from('flow_nodes')
238
+ .from('diagram_nodes')
239
239
  .insert(nodeRows)
240
240
  .select('id, slug');
241
241
  if (nodesError) {
@@ -243,11 +243,11 @@ async function persistFlow(supabase, flowId, extraction) {
243
243
  }
244
244
  const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
245
245
  const edgeRows = extraction.edges
246
- .map((e) => buildEdgeRow(flowId, e, slugToId))
246
+ .map((e) => buildEdgeRow(diagramId, e, slugToId))
247
247
  .filter((e) => e !== null);
248
248
  if (edgeRows.length > 0) {
249
249
  const { error: edgesError } = await supabase
250
- .from('flow_edges')
250
+ .from('diagram_edges')
251
251
  .insert(edgeRows);
252
252
  if (edgesError) {
253
253
  throw new Error(`Failed to insert edges: ${edgesError.message}`);
@@ -258,9 +258,9 @@ async function persistFlow(supabase, flowId, extraction) {
258
258
  edgesCreated: edgeRows.length,
259
259
  };
260
260
  }
261
- function buildNodeRow(flowId, node, index) {
261
+ function buildNodeRow(diagramId, node, index) {
262
262
  return {
263
- flow_id: flowId,
263
+ diagram_id: diagramId,
264
264
  slug: node.slug,
265
265
  name: node.name,
266
266
  kind: node.kind,
@@ -269,14 +269,14 @@ function buildNodeRow(flowId, node, index) {
269
269
  position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
270
270
  };
271
271
  }
272
- function buildEdgeRow(flowId, edge, slugToId) {
272
+ function buildEdgeRow(diagramId, edge, slugToId) {
273
273
  const fromId = slugToId.get(edge.fromSlug);
274
274
  const toId = slugToId.get(edge.toSlug);
275
275
  if (!fromId || !toId) {
276
276
  return null;
277
277
  }
278
278
  return {
279
- flow_id: flowId,
279
+ diagram_id: diagramId,
280
280
  from_node_id: fromId,
281
281
  to_node_id: toId,
282
282
  label: edge.label ?? null,
@@ -27,8 +27,8 @@ export declare function createSubmitDataFlowTool(state: DataFlowCaptureState): i
27
27
  kind: z.ZodEnum<{
28
28
  model: "model";
29
29
  source: "source";
30
- dataset: "dataset";
31
30
  transform: "transform";
31
+ dataset: "dataset";
32
32
  sink: "sink";
33
33
  queue: "queue";
34
34
  }>;
@@ -53,7 +53,7 @@ export function validateConsistency(extraction) {
53
53
  for (const node of extraction.nodes) {
54
54
  if (slugs.has(node.slug)) {
55
55
  return {
56
- error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the flow. Re-call submit_data_flow with deduplicated nodes.`,
56
+ error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the diagram. Re-call submit_data_flow with deduplicated nodes.`,
57
57
  };
58
58
  }
59
59
  slugs.add(node.slug);
@@ -87,7 +87,7 @@ export function createSubmitDataFlowTool(state) {
87
87
  .describe('1-3 sentence narrative of what this system does with data and the primary pipelines.'),
88
88
  nodes: z
89
89
  .array(dataNodeSchema)
90
- .describe('Every data node: source / dataset / transform / sink / queue / model. node.slug MUST be unique within the flow.'),
90
+ .describe('Every data node: source / dataset / transform / sink / queue / model. node.slug MUST be unique within the diagram.'),
91
91
  edges: z
92
92
  .array(dataEdgeSchema)
93
93
  .describe('Connections. fromSlug = upstream, toSlug = downstream. Every fromSlug / toSlug MUST reference a slug present in nodes; drop edges whose endpoints you did not emit.'),
@@ -8,7 +8,7 @@
8
8
  * unified <DataNodePreview> component.
9
9
  *
10
10
  * Companion to ScreenSchema: same flow-graph shape (nodes + edges with a
11
- * shared `flows` table storing the JSONB schema), different domain. Data
11
+ * shared `diagrams` table storing the JSONB schema), different domain. Data
12
12
  * flow edges describe how data moves between nodes, not user navigation.
13
13
  */
14
14
  export type DataNodeKind = 'source' | 'dataset' | 'transform' | 'sink' | 'queue' | 'model';
@@ -8,7 +8,7 @@
8
8
  * unified <DataNodePreview> component.
9
9
  *
10
10
  * Companion to ScreenSchema: same flow-graph shape (nodes + edges with a
11
- * shared `flows` table storing the JSONB schema), different domain. Data
11
+ * shared `diagrams` table storing the JSONB schema), different domain. Data
12
12
  * flow edges describe how data moves between nodes, not user navigation.
13
13
  */
14
14
  // ============================================================================
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared multi-repo cloning for flow generation (screen-flow / data-flow).
3
+ *
4
+ * A flow can be scoped to one or several repositories (the user picks them in
5
+ * the desktop UI; the chosen set is stored on `diagrams.repository_ids`). This
6
+ * helper resolves that set, clones each repo into a per-flow parent workspace
7
+ * directory, and returns the directory the agent should run against:
8
+ * - single repo → the repo's own clone dir
9
+ * - many repos → the parent dir holding every clone as a subdirectory,
10
+ * so the agent can explore them all and produce one unified
11
+ * flow.
12
+ *
13
+ * Falls back to the product's primary repo when `repository_ids` is empty
14
+ * (older diagrams, or single-repo products).
15
+ */
16
+ export interface ClonedRepo {
17
+ fullName: string;
18
+ owner: string;
19
+ repo: string;
20
+ dir: string;
21
+ }
22
+ export interface CloneFlowReposSuccess {
23
+ ok: true;
24
+ /** Directory to point the agent at (parent dir for multi-repo). */
25
+ projectDir: string;
26
+ /** Directory to clean up afterwards (always the per-flow parent). */
27
+ cleanupDir: string;
28
+ repos: ClonedRepo[];
29
+ }
30
+ export interface CloneFlowReposFailure {
31
+ ok: false;
32
+ message: string;
33
+ }
34
+ export declare function safeDirName(fullName: string): string;
35
+ /**
36
+ * Resolve the repositories a flow targets (by id, preserving the stored
37
+ * order), falling back to the product's primary repo.
38
+ *
39
+ * In repo-only mode there is no product, so no `fallback` is provided: the
40
+ * set is resolved purely from `repositoryIds`.
41
+ */
42
+ export declare function resolveTargetRepos(productId: string | undefined, repositoryIds: string[], fallback?: {
43
+ owner: string;
44
+ repo: string;
45
+ }): Promise<{
46
+ fullName: string;
47
+ owner: string;
48
+ repo: string;
49
+ }[]>;
50
+ export declare function cloneDiagramRepos(opts: {
51
+ /** Product-scoped flow. Mutually exclusive with `repoId`. */
52
+ productId?: string;
53
+ /** Repo-only flow: a single repositories row, no product context. */
54
+ repoId?: string;
55
+ repositoryIds: string[];
56
+ workspaceKey: string;
57
+ verbose?: boolean;
58
+ }): Promise<CloneFlowReposSuccess | CloneFlowReposFailure>;
59
+ /**
60
+ * Build a short note describing the repo scope, appended to the agent's user
61
+ * prompt so it knows whether to map one repo or unify several.
62
+ */
63
+ export declare function describeRepoScope(repos: ClonedRepo[]): string;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Shared multi-repo cloning for flow generation (screen-flow / data-flow).
3
+ *
4
+ * A flow can be scoped to one or several repositories (the user picks them in
5
+ * the desktop UI; the chosen set is stored on `diagrams.repository_ids`). This
6
+ * helper resolves that set, clones each repo into a per-flow parent workspace
7
+ * directory, and returns the directory the agent should run against:
8
+ * - single repo → the repo's own clone dir
9
+ * - many repos → the parent dir holding every clone as a subdirectory,
10
+ * so the agent can explore them all and produce one unified
11
+ * flow.
12
+ *
13
+ * Falls back to the product's primary repo when `repository_ids` is empty
14
+ * (older diagrams, or single-repo products).
15
+ */
16
+ import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
17
+ import { getSupabase } from '../../supabase/client.js';
18
+ import { logInfo, logWarning } from '../../utils/logger.js';
19
+ import { cloneIssueRepo, ensureWorkspaceDir, getIssueRepoPath, } from '../../workspace/workspace-manager.js';
20
+ export function safeDirName(fullName) {
21
+ return fullName.replace(/[^a-zA-Z0-9._-]/g, '_');
22
+ }
23
+ /**
24
+ * Resolve the repositories a flow targets (by id, preserving the stored
25
+ * order), falling back to the product's primary repo.
26
+ *
27
+ * In repo-only mode there is no product, so no `fallback` is provided: the
28
+ * set is resolved purely from `repositoryIds`.
29
+ */
30
+ export async function resolveTargetRepos(productId, repositoryIds, fallback) {
31
+ if (repositoryIds.length === 0) {
32
+ if (fallback) {
33
+ return [
34
+ {
35
+ fullName: `${fallback.owner}/${fallback.repo}`,
36
+ owner: fallback.owner,
37
+ repo: fallback.repo,
38
+ },
39
+ ];
40
+ }
41
+ return [];
42
+ }
43
+ const supabase = getSupabase();
44
+ const { data } = await supabase
45
+ .from('repositories')
46
+ .select('id, full_name')
47
+ .in('id', repositoryIds);
48
+ const byId = new Map((data ?? []).map((r) => [r.id, r.full_name]));
49
+ // Preserve the caller's order (diagrams.repository_ids is ordered).
50
+ const resolved = [];
51
+ for (const id of repositoryIds) {
52
+ const fullName = byId.get(id);
53
+ if (!fullName) {
54
+ continue;
55
+ }
56
+ const [owner, repo] = fullName.split('/');
57
+ if (!owner || !repo) {
58
+ continue;
59
+ }
60
+ resolved.push({ fullName, owner, repo });
61
+ }
62
+ // If none resolved (deleted repos / RLS), fall back to the primary repo so
63
+ // generation still produces something useful (product mode only).
64
+ if (resolved.length === 0 && fallback) {
65
+ return [
66
+ {
67
+ fullName: `${fallback.owner}/${fallback.repo}`,
68
+ owner: fallback.owner,
69
+ repo: fallback.repo,
70
+ },
71
+ ];
72
+ }
73
+ return resolved;
74
+ }
75
+ export async function cloneDiagramRepos(opts) {
76
+ const { productId, repoId, repositoryIds, workspaceKey, verbose } = opts;
77
+ const repoOnly = !productId && Boolean(repoId);
78
+ // Resolve the auth token. Product mode reuses the product's installation /
79
+ // PAT for every repo; repo-only mode resolves it from the first (only) repo.
80
+ const gh = repoOnly
81
+ ? await getGitHubConfigByRepository(repositoryIds[0] ?? repoId, verbose)
82
+ : await getGitHubConfigByProduct(productId, verbose);
83
+ if (!gh.configured || !gh.token || !gh.owner || !gh.repo) {
84
+ return {
85
+ ok: false,
86
+ message: gh.message ||
87
+ (repoOnly
88
+ ? 'GitHub repository not configured. Connect the repo first.'
89
+ : 'GitHub repository not configured for this product. Connect a repo first.'),
90
+ };
91
+ }
92
+ // In repo-only mode there is no product primary-repo fallback; targets come
93
+ // purely from repositoryIds.
94
+ const targets = await resolveTargetRepos(productId, repositoryIds, repoOnly ? undefined : { owner: gh.owner, repo: gh.repo });
95
+ if (targets.length === 0) {
96
+ return {
97
+ ok: false,
98
+ message: 'No repositories resolved for this flow.',
99
+ };
100
+ }
101
+ const workspaceRoot = ensureWorkspaceDir();
102
+ const parentDir = getIssueRepoPath(workspaceRoot, `${workspaceKey}-${repoOnly ? `repo-${repoId}` : productId}`);
103
+ const repos = [];
104
+ for (const target of targets) {
105
+ try {
106
+ // The product-level token (installation or user PAT/OAuth) is reused for
107
+ // every repo; if it can't access one, that clone fails and we skip it
108
+ // rather than aborting the whole generation.
109
+ const { repoPath } = cloneIssueRepo(parentDir, safeDirName(target.fullName), target.owner, target.repo, gh.token);
110
+ repos.push({
111
+ fullName: target.fullName,
112
+ owner: target.owner,
113
+ repo: target.repo,
114
+ dir: repoPath,
115
+ });
116
+ }
117
+ catch (err) {
118
+ logWarning(`Skipping ${target.fullName}: clone failed (${err instanceof Error ? err.message : String(err)})`);
119
+ }
120
+ }
121
+ if (repos.length === 0) {
122
+ return {
123
+ ok: false,
124
+ message: 'Failed to clone any of the selected repositories.',
125
+ };
126
+ }
127
+ if (repos.length > 1) {
128
+ logInfo(`Cloned ${repos.length} repos for ${workspaceKey}: ${repos.map((r) => r.fullName).join(', ')}`);
129
+ }
130
+ return {
131
+ ok: true,
132
+ // Single repo: run directly in its dir. Multi-repo: run in the parent so
133
+ // the agent sees every clone as a subdirectory.
134
+ projectDir: repos.length === 1 ? repos[0].dir : parentDir,
135
+ cleanupDir: parentDir,
136
+ repos,
137
+ };
138
+ }
139
+ /**
140
+ * Build a short note describing the repo scope, appended to the agent's user
141
+ * prompt so it knows whether to map one repo or unify several.
142
+ */
143
+ export function describeRepoScope(repos) {
144
+ if (repos.length <= 1) {
145
+ return '';
146
+ }
147
+ const list = repos.map((r) => `- ${r.fullName} (subdirectory: ${safeDirName(r.fullName)})`);
148
+ return [
149
+ `This product spans ${repos.length} repositories, each cloned into its own subdirectory of the working directory:`,
150
+ ...list,
151
+ 'Explore all of them and produce a single unified flow that spans the repositories.',
152
+ ].join('\n');
153
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared diagram generation runner.
3
+ *
4
+ * Encapsulates the common pipeline every node/edge diagram phase uses: clone
5
+ * the repo(s), resolve product/repo basics, build prompts, run the Claude
6
+ * Agent SDK loop with an in-process MCP capture server, fall back to a fenced
7
+ * block if the agent forgot the tool, then persist to the diagrams tables and
8
+ * flip status. Each lean phase just supplies its domain MCP config + prompts.
9
+ */
10
+ import { type DiagramMcpConfig } from './mcp.js';
11
+ export interface GenerateDiagramOptions {
12
+ productId?: string;
13
+ repoId?: string;
14
+ diagramId: string;
15
+ guidance?: string;
16
+ verbose?: boolean;
17
+ /** Workspace clone key, e.g. 'state-diagram'. */
18
+ workspaceKey: string;
19
+ /** Fenced block name for the fallback parser, e.g. 'state_diagram'. */
20
+ fenceName: string;
21
+ /** Plural noun for log/result messages, e.g. 'states', 'classes'. */
22
+ nounPlural: string;
23
+ edgeNounPlural: string;
24
+ mcpConfig: DiagramMcpConfig;
25
+ buildSystemPrompt: (args: {
26
+ projectDir: string;
27
+ hasCodebase: boolean;
28
+ }) => Promise<string>;
29
+ buildUserPrompt: (args: {
30
+ productName: string;
31
+ productDescription?: string;
32
+ guidance?: string;
33
+ }) => string;
34
+ }
35
+ export interface DiagramPhaseResult {
36
+ status: 'success' | 'error';
37
+ message: string;
38
+ nodesCreated?: number;
39
+ edgesCreated?: number;
40
+ summary?: string;
41
+ }
42
+ export declare function generateDiagram(options: GenerateDiagramOptions): Promise<DiagramPhaseResult>;