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,290 @@
1
+ /**
2
+ * er-diagram phase: clone the product's repo, ask Claude to map every
3
+ * persistence entity (table / view / enum / junction) and the relationships
4
+ * between them into a structured ErDiagramExtraction, then persist the result
5
+ * to diagrams / diagram_nodes / diagram_edges (rows tagged `type = 'er'`) via the
6
+ * Supabase SDK.
7
+ *
8
+ * Companion to data-flow / screen-flow: same generation pattern (workspace
9
+ * clone + Claude Agent SDK + in-process MCP server), same storage tables,
10
+ * different domain.
11
+ */
12
+ import { query } from '@anthropic-ai/claude-agent-sdk';
13
+ import { getRepositoryBasics } from '../../api/github.js';
14
+ import { DEFAULT_MODEL } from '../../constants.js';
15
+ import { getSupabase } from '../../supabase/client.js';
16
+ import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
17
+ import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
18
+ import { fetchProductBasics } from '../find-shared/mcp.js';
19
+ import { cloneDiagramRepos, describeRepoScope, } from '../diagram-shared/clone-repos.js';
20
+ import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
21
+ import { createErDiagramCaptureState, createErDiagramMcpServer, validateConsistency, } from './mcp-server.js';
22
+ import { createErDiagramSystemPrompt, createErDiagramUserPrompt, } from './prompts.js';
23
+ import { isErDiagramExtraction, } from './types.js';
24
+ const WORKSPACE_KEY = 'er-diagram';
25
+ const MAX_TURNS = 150;
26
+ // Auto-layout: simple grid; users can drag afterwards and we persist positions.
27
+ const COLUMN_WIDTH = 320;
28
+ const ROW_HEIGHT = 260;
29
+ const COLUMNS = 4;
30
+ export async function runErDiagramPhase(options) {
31
+ const { productId, repoId, diagramId, guidance, verbose } = options;
32
+ const repoOnly = !productId && Boolean(repoId);
33
+ if (productId) {
34
+ logInfo(`Starting er-diagram generation for product ${productId}`);
35
+ }
36
+ else {
37
+ logInfo(`Starting er-diagram generation for repository ${repoId}`);
38
+ }
39
+ const supabase = getSupabase();
40
+ await markDiagramRunning(supabase, diagramId);
41
+ const repositoryIds = await getDiagramRepositoryIds(supabase, diagramId);
42
+ const cloneResult = await cloneDiagramRepos({
43
+ productId,
44
+ repoId,
45
+ repositoryIds,
46
+ workspaceKey: WORKSPACE_KEY,
47
+ verbose,
48
+ });
49
+ if (!cloneResult.ok) {
50
+ await markDiagramFailed(supabase, diagramId, cloneResult.message);
51
+ return { status: 'error', message: cloneResult.message };
52
+ }
53
+ const { projectDir, cleanupDir, repos } = cloneResult;
54
+ let succeeded = false;
55
+ try {
56
+ const product = repoOnly
57
+ ? await resolveRepoBasics(repoId, repos)
58
+ : await fetchProductBasics(productId);
59
+ const systemPrompt = await createErDiagramSystemPrompt({
60
+ projectDir,
61
+ hasCodebase: true,
62
+ });
63
+ const repoScope = describeRepoScope(repos);
64
+ const userPrompt = createErDiagramUserPrompt({
65
+ productName: product.name,
66
+ productDescription: product.description,
67
+ guidance: [guidance, repoScope].filter(Boolean).join('\n\n') || undefined,
68
+ });
69
+ logInfo('Running Claude er-diagram extraction...');
70
+ const captureState = createErDiagramCaptureState();
71
+ const mcpServer = createErDiagramMcpServer(captureState, {
72
+ onProgress: ({ phase, message }) => {
73
+ logInfo(`[${phase}] ${message}`);
74
+ },
75
+ });
76
+ let lastAssistantResponse = '';
77
+ let extraction = null;
78
+ for await (const message of query({
79
+ prompt: createPromptGenerator(userPrompt),
80
+ options: {
81
+ systemPrompt: {
82
+ type: 'preset',
83
+ preset: 'claude_code',
84
+ append: systemPrompt,
85
+ },
86
+ model: DEFAULT_MODEL,
87
+ maxTurns: MAX_TURNS,
88
+ permissionMode: 'bypassPermissions',
89
+ cwd: projectDir,
90
+ mcpServers: {
91
+ 'er-diagram': mcpServer,
92
+ },
93
+ },
94
+ })) {
95
+ const { assistantBuffer, extraction: nextExtraction } = processSdkMessage(message, lastAssistantResponse, captureState, verbose);
96
+ lastAssistantResponse = assistantBuffer;
97
+ if (nextExtraction) {
98
+ extraction = nextExtraction;
99
+ }
100
+ }
101
+ if (!extraction) {
102
+ const msg = 'ER diagram extraction failed: agent did not call submit_er_diagram and no parseable er_diagram block was found in the response';
103
+ await markDiagramFailed(supabase, diagramId, msg);
104
+ return { status: 'error', message: msg };
105
+ }
106
+ logInfo(`Extraction produced ${extraction.entities.length} entities / ${extraction.relations.length} relationships`);
107
+ const { nodesCreated, edgesCreated } = await persistDiagram(supabase, diagramId, extraction);
108
+ await markDiagramSuccess(supabase, diagramId, extraction.summary);
109
+ succeeded = true;
110
+ logSuccess(`ER diagram generated: ${nodesCreated} entities, ${edgesCreated} relationships`);
111
+ return {
112
+ status: 'success',
113
+ message: `ER diagram generated (${nodesCreated} entities, ${edgesCreated} relationships)`,
114
+ nodesCreated,
115
+ edgesCreated,
116
+ summary: extraction.summary,
117
+ };
118
+ }
119
+ catch (error) {
120
+ const errorMessage = error instanceof Error ? error.message : String(error);
121
+ logError(`ER diagram failed: ${errorMessage}`);
122
+ await markDiagramFailed(supabase, diagramId, errorMessage);
123
+ return { status: 'error', message: errorMessage };
124
+ }
125
+ finally {
126
+ if (succeeded) {
127
+ cleanupIssueRepo(cleanupDir);
128
+ }
129
+ }
130
+ }
131
+ /**
132
+ * Build "product basics" for repo-only mode from the repositories row,
133
+ * falling back to the cloned repo's full name when the row has no name.
134
+ */
135
+ async function resolveRepoBasics(repositoryId, repos) {
136
+ const basics = await getRepositoryBasics(repositoryId).catch(() => null);
137
+ return {
138
+ name: basics?.fullName ?? repos[0]?.fullName ?? repositoryId,
139
+ description: basics?.description ?? undefined,
140
+ };
141
+ }
142
+ /** Read the ordered repo set a diagram was scoped to (may be empty). */
143
+ async function getDiagramRepositoryIds(supabase, diagramId) {
144
+ const { data } = await supabase
145
+ .from('diagrams')
146
+ .select('repository_ids')
147
+ .eq('id', diagramId)
148
+ .single();
149
+ return (data?.repository_ids ?? []).filter(Boolean);
150
+ }
151
+ function processSdkMessage(
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ message, assistantBuffer, captureState, verbose) {
154
+ if (message.type === 'assistant') {
155
+ const next = assistantBuffer +
156
+ extractTextFromContent(message.message?.content ?? [], verbose);
157
+ return { assistantBuffer: next, extraction: null };
158
+ }
159
+ if (message.type === 'user' && verbose) {
160
+ const userContent = message.message?.content;
161
+ if (Array.isArray(userContent)) {
162
+ extractTextFromContent(userContent, verbose);
163
+ }
164
+ return { assistantBuffer, extraction: null };
165
+ }
166
+ if (message.type !== 'result') {
167
+ return { assistantBuffer, extraction: null };
168
+ }
169
+ if (captureState.captured) {
170
+ return { assistantBuffer, extraction: captureState.captured };
171
+ }
172
+ const fallback = tryFallbackParse(message, assistantBuffer);
173
+ if (fallback) {
174
+ logWarning('Agent emitted a fenced er_diagram block instead of calling submit_er_diagram; using the parsed text as a fallback.');
175
+ return { assistantBuffer, extraction: fallback };
176
+ }
177
+ if (message.subtype !== 'success') {
178
+ logError(`Extraction incomplete: ${message.subtype}`);
179
+ }
180
+ return { assistantBuffer, extraction: null };
181
+ }
182
+ function tryFallbackParse(resultMessage, assistantText) {
183
+ const responseText = resultMessage.subtype === 'success'
184
+ ? resultMessage.result || assistantText
185
+ : assistantText;
186
+ const parsed = tryExtractResult(responseText, 'er_diagram');
187
+ if (!isErDiagramExtraction(parsed)) {
188
+ return null;
189
+ }
190
+ const { error } = validateConsistency(parsed);
191
+ if (error) {
192
+ logWarning(`Fallback extraction failed consistency check: ${error}`);
193
+ return null;
194
+ }
195
+ return parsed;
196
+ }
197
+ // ============================================================================
198
+ // Persistence
199
+ // ============================================================================
200
+ async function markDiagramRunning(supabase, diagramId) {
201
+ const { error } = await supabase
202
+ .from('diagrams')
203
+ .update({ status: 'running', error: null })
204
+ .eq('id', diagramId);
205
+ if (error) {
206
+ logWarning(`Could not mark diagram as running: ${error.message}`);
207
+ }
208
+ }
209
+ async function markDiagramFailed(supabase, diagramId, errorMessage) {
210
+ await supabase
211
+ .from('diagrams')
212
+ .update({
213
+ status: 'failed',
214
+ error: errorMessage,
215
+ completed_at: new Date().toISOString(),
216
+ })
217
+ .eq('id', diagramId);
218
+ }
219
+ async function markDiagramSuccess(supabase, diagramId, summary) {
220
+ await supabase
221
+ .from('diagrams')
222
+ .update({
223
+ status: 'success',
224
+ summary,
225
+ error: null,
226
+ completed_at: new Date().toISOString(),
227
+ })
228
+ .eq('id', diagramId);
229
+ }
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
+ if (extraction.entities.length === 0) {
234
+ return { nodesCreated: 0, edgesCreated: 0 };
235
+ }
236
+ const nodeRows = extraction.entities.map((e, i) => buildNodeRow(diagramId, e, i));
237
+ const { data: insertedNodes, error: nodesError } = await supabase
238
+ .from('diagram_nodes')
239
+ .insert(nodeRows)
240
+ .select('id, slug');
241
+ if (nodesError) {
242
+ throw new Error(`Failed to insert entities: ${nodesError.message}`);
243
+ }
244
+ const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
245
+ const edgeRows = extraction.relations
246
+ .map((r) => buildEdgeRow(diagramId, r, slugToId))
247
+ .filter((e) => e !== null);
248
+ if (edgeRows.length > 0) {
249
+ const { error: edgesError } = await supabase
250
+ .from('diagram_edges')
251
+ .insert(edgeRows);
252
+ if (edgesError) {
253
+ throw new Error(`Failed to insert relationships: ${edgesError.message}`);
254
+ }
255
+ }
256
+ return {
257
+ nodesCreated: nodeRows.length,
258
+ edgesCreated: edgeRows.length,
259
+ };
260
+ }
261
+ function buildNodeRow(diagramId, entity, index) {
262
+ return {
263
+ diagram_id: diagramId,
264
+ slug: entity.slug,
265
+ name: entity.name,
266
+ kind: entity.kind,
267
+ schema: entity,
268
+ position_x: (index % COLUMNS) * COLUMN_WIDTH,
269
+ position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
270
+ };
271
+ }
272
+ function buildEdgeRow(diagramId, relation, slugToId) {
273
+ const fromId = slugToId.get(relation.fromSlug);
274
+ const toId = slugToId.get(relation.toSlug);
275
+ if (!fromId || !toId) {
276
+ return null;
277
+ }
278
+ return {
279
+ diagram_id: diagramId,
280
+ from_node_id: fromId,
281
+ to_node_id: toId,
282
+ label: relation.label ?? null,
283
+ source_anchor: relation.sourceFile ?? null,
284
+ kind: relation.kind,
285
+ metadata: {
286
+ sourceColumn: relation.sourceColumn ?? null,
287
+ targetColumn: relation.targetColumn ?? null,
288
+ },
289
+ };
290
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * In-process MCP server for the er-diagram phase. Exposes a single tool —
3
+ * `submit_er_diagram` — that the agent calls with the structured extraction,
4
+ * plus `record_progress` for streaming status messages.
5
+ *
6
+ * Mirrors the shape of phases/data-flow/mcp-server.ts; see that file for
7
+ * the design rationale (zod schema + cross-field consistency + capture state).
8
+ */
9
+ import { z } from 'zod';
10
+ import type { ErDiagramExtraction } from './types.js';
11
+ export interface ErDiagramCaptureState {
12
+ captured: ErDiagramExtraction | null;
13
+ }
14
+ export declare function createErDiagramCaptureState(): ErDiagramCaptureState;
15
+ export type ErDiagramProgressSink = (event: {
16
+ phase: 'detection' | 'enumeration' | 'entities' | 'relations' | 'submission';
17
+ message: string;
18
+ }) => void;
19
+ export declare function validateConsistency(extraction: ErDiagramExtraction): {
20
+ error: string | null;
21
+ };
22
+ export declare function createSubmitErDiagramTool(state: ErDiagramCaptureState): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
23
+ summary: z.ZodString;
24
+ entities: z.ZodArray<z.ZodObject<{
25
+ slug: z.ZodString;
26
+ name: z.ZodString;
27
+ kind: z.ZodEnum<{
28
+ enum: "enum";
29
+ entity: "entity";
30
+ view: "view";
31
+ junction: "junction";
32
+ }>;
33
+ file: z.ZodOptional<z.ZodString>;
34
+ description: z.ZodOptional<z.ZodString>;
35
+ columns: z.ZodOptional<z.ZodArray<z.ZodObject<{
36
+ name: z.ZodString;
37
+ type: z.ZodOptional<z.ZodString>;
38
+ isPrimaryKey: z.ZodOptional<z.ZodBoolean>;
39
+ isForeignKey: z.ZodOptional<z.ZodBoolean>;
40
+ isNullable: z.ZodOptional<z.ZodBoolean>;
41
+ isUnique: z.ZodOptional<z.ZodBoolean>;
42
+ description: z.ZodOptional<z.ZodString>;
43
+ references: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strip>>>;
45
+ stats: z.ZodOptional<z.ZodArray<z.ZodObject<{
46
+ label: z.ZodString;
47
+ value: z.ZodString;
48
+ }, z.core.$strip>>>;
49
+ }, z.core.$strip>>;
50
+ relations: z.ZodArray<z.ZodObject<{
51
+ fromSlug: z.ZodString;
52
+ toSlug: z.ZodString;
53
+ kind: z.ZodEnum<{
54
+ "one-to-one": "one-to-one";
55
+ "one-to-many": "one-to-many";
56
+ "many-to-many": "many-to-many";
57
+ inherits: "inherits";
58
+ }>;
59
+ label: z.ZodOptional<z.ZodString>;
60
+ sourceColumn: z.ZodOptional<z.ZodString>;
61
+ targetColumn: z.ZodOptional<z.ZodString>;
62
+ sourceFile: z.ZodOptional<z.ZodString>;
63
+ }, z.core.$strip>>;
64
+ }>;
65
+ export declare function createRecordProgressTool(sink?: ErDiagramProgressSink): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
66
+ phase: z.ZodEnum<{
67
+ detection: "detection";
68
+ enumeration: "enumeration";
69
+ submission: "submission";
70
+ entities: "entities";
71
+ relations: "relations";
72
+ }>;
73
+ message: z.ZodString;
74
+ }>;
75
+ export declare function createErDiagramMcpServer(state: ErDiagramCaptureState, options?: {
76
+ onProgress?: ErDiagramProgressSink;
77
+ }): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * In-process MCP server for the er-diagram phase. Exposes a single tool —
3
+ * `submit_er_diagram` — that the agent calls with the structured extraction,
4
+ * plus `record_progress` for streaming status messages.
5
+ *
6
+ * Mirrors the shape of phases/data-flow/mcp-server.ts; see that file for
7
+ * the design rationale (zod schema + cross-field consistency + capture state).
8
+ */
9
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
10
+ import { z } from 'zod';
11
+ export function createErDiagramCaptureState() {
12
+ return { captured: null };
13
+ }
14
+ // ---------------------------------------------------------------------------
15
+ // Zod schemas (mirror types.ts)
16
+ // ---------------------------------------------------------------------------
17
+ const erColumnSchema = z.object({
18
+ name: z.string().min(1),
19
+ type: z.string().optional(),
20
+ isPrimaryKey: z.boolean().optional(),
21
+ isForeignKey: z.boolean().optional(),
22
+ isNullable: z.boolean().optional(),
23
+ isUnique: z.boolean().optional(),
24
+ description: z.string().optional(),
25
+ references: z.string().optional(),
26
+ });
27
+ const erStatSchema = z.object({
28
+ label: z.string(),
29
+ value: z.string(),
30
+ });
31
+ const erEntitySchema = z.object({
32
+ slug: z.string().min(1),
33
+ name: z.string().min(1),
34
+ kind: z.enum(['entity', 'view', 'enum', 'junction']),
35
+ file: z.string().optional(),
36
+ description: z.string().optional(),
37
+ columns: z.array(erColumnSchema).optional(),
38
+ stats: z.array(erStatSchema).optional(),
39
+ });
40
+ const erRelationSchema = z.object({
41
+ fromSlug: z.string().min(1),
42
+ toSlug: z.string().min(1),
43
+ kind: z.enum(['one-to-one', 'one-to-many', 'many-to-many', 'inherits']),
44
+ label: z.string().optional(),
45
+ sourceColumn: z.string().optional(),
46
+ targetColumn: z.string().optional(),
47
+ sourceFile: z.string().optional(),
48
+ });
49
+ export function validateConsistency(extraction) {
50
+ const slugs = new Set();
51
+ for (const entity of extraction.entities) {
52
+ if (slugs.has(entity.slug)) {
53
+ return {
54
+ error: `Duplicate entity slug "${entity.slug}". Each entity.slug MUST be unique within the diagram. Re-call submit_er_diagram with deduplicated entities.`,
55
+ };
56
+ }
57
+ slugs.add(entity.slug);
58
+ }
59
+ for (const relation of extraction.relations) {
60
+ if (!slugs.has(relation.fromSlug)) {
61
+ return {
62
+ error: `Relation fromSlug "${relation.fromSlug}" → "${relation.toSlug}" does not match any entity slug. Either add the missing entity or drop the relation, then re-call submit_er_diagram.`,
63
+ };
64
+ }
65
+ if (!slugs.has(relation.toSlug)) {
66
+ return {
67
+ error: `Relation fromSlug "${relation.fromSlug}" → toSlug "${relation.toSlug}" does not match any entity slug. Either add the missing entity or drop the relation, then re-call submit_er_diagram.`,
68
+ };
69
+ }
70
+ }
71
+ return { error: null };
72
+ }
73
+ export function createSubmitErDiagramTool(state) {
74
+ return tool('submit_er_diagram', [
75
+ 'Submit the final entity-relationship diagram. Call this EXACTLY once,',
76
+ 'when you have finished mapping every entity and relationship. Pass',
77
+ 'the full structured diagram as the argument. After this call succeeds,',
78
+ 'end your turn — do NOT also paste the same data as a fenced code',
79
+ 'block. If validation fails, the error message tells you what to fix;',
80
+ 'call the tool again with corrected data.',
81
+ ].join(' '), {
82
+ summary: z
83
+ .string()
84
+ .min(1)
85
+ .describe("1-3 sentence narrative of the data model: the core entities and how they're related."),
86
+ entities: z
87
+ .array(erEntitySchema)
88
+ .describe('Every entity: table / view / enum / junction table. entity.slug MUST be unique within the diagram.'),
89
+ relations: z
90
+ .array(erRelationSchema)
91
+ .describe('Relationships. fromSlug = entity holding the foreign key (child / "many" side), toSlug = referenced entity (parent / "one" side). Every fromSlug / toSlug MUST reference a slug present in entities; drop relations whose endpoints you did not emit.'),
92
+ }, async (args) => {
93
+ const extraction = {
94
+ summary: args.summary,
95
+ entities: args.entities,
96
+ relations: args.relations,
97
+ };
98
+ const { error } = validateConsistency(extraction);
99
+ if (error) {
100
+ return {
101
+ content: [{ type: 'text', text: error }],
102
+ isError: true,
103
+ };
104
+ }
105
+ state.captured = extraction;
106
+ return {
107
+ content: [
108
+ {
109
+ type: 'text',
110
+ text: `Captured ${extraction.entities.length} entities / ${extraction.relations.length} relationships. End your turn now.`,
111
+ },
112
+ ],
113
+ };
114
+ });
115
+ }
116
+ export function createRecordProgressTool(sink) {
117
+ return tool('record_progress', 'Send a short status update to the user. Does not affect the extraction. Call it at each phase boundary so the user sees progress.', {
118
+ phase: z
119
+ .enum([
120
+ 'detection',
121
+ 'enumeration',
122
+ 'entities',
123
+ 'relations',
124
+ 'submission',
125
+ ])
126
+ .describe('Which phase the message belongs to.'),
127
+ message: z.string().min(1).describe('Human-readable status update.'),
128
+ }, async (args) => {
129
+ sink?.({ phase: args.phase, message: args.message });
130
+ return {
131
+ content: [{ type: 'text', text: 'ok' }],
132
+ };
133
+ });
134
+ }
135
+ export function createErDiagramMcpServer(state, options) {
136
+ return createSdkMcpServer({
137
+ name: 'er-diagram',
138
+ version: '1.0.0',
139
+ tools: [
140
+ createSubmitErDiagramTool(state),
141
+ createRecordProgressTool(options?.onProgress),
142
+ ],
143
+ });
144
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Prompts for the er-diagram phase. Loads the system prompt body from
3
+ * `skills/phase/er-diagram/SKILL.md` (with optional project override) and
4
+ * appends the JSON output contract.
5
+ */
6
+ export declare function createErDiagramSystemPrompt(options?: {
7
+ projectDir?: string;
8
+ hasCodebase?: boolean;
9
+ }): Promise<string>;
10
+ export declare function createErDiagramUserPrompt(args: {
11
+ productName: string;
12
+ productDescription?: string;
13
+ guidance?: string;
14
+ }): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Prompts for the er-diagram phase. Loads the system prompt body from
3
+ * `skills/phase/er-diagram/SKILL.md` (with optional project override) and
4
+ * appends the JSON output contract.
5
+ */
6
+ import { processConditionals, resolveSkill, } from '../../services/skill-resolver.js';
7
+ import { OUTPUT_CONTRACTS } from '../output-contracts.js';
8
+ export async function createErDiagramSystemPrompt(options) {
9
+ const skill = await resolveSkill('phase/er-diagram', {
10
+ projectDir: options?.projectDir,
11
+ });
12
+ if (!skill) {
13
+ throw new Error('Failed to load skill: phase/er-diagram');
14
+ }
15
+ const prompt = processConditionals(skill.prompt, {
16
+ hasCodebase: options?.hasCodebase ?? true,
17
+ });
18
+ return `${prompt}
19
+
20
+ ${OUTPUT_CONTRACTS['er-diagram']}`;
21
+ }
22
+ export function createErDiagramUserPrompt(args) {
23
+ const guidanceBlock = args.guidance
24
+ ? `\n\n**Human guidance for this run** (focus or exclude as instructed):\n${args.guidance}`
25
+ : '';
26
+ const descBlock = args.productDescription
27
+ ? `\n**Product description**: ${args.productDescription}`
28
+ : '';
29
+ return `Map the entity-relationship model for **${args.productName}**.${descBlock}${guidanceBlock}
30
+
31
+ Start by detecting the stack and locating the schema source of truth (database migrations, ORM models — Prisma / SQLAlchemy / TypeORM / ActiveRecord / Drizzle, \`CREATE TABLE\` SQL, or typed schema files). Read just enough per entity to fill in a useful set of columns and the relationships — you do not need to read everything.
32
+
33
+ Call \`mcp__er-diagram__record_progress\` at each phase boundary so the user can see your progress (otherwise the CLI looks frozen).
34
+
35
+ When you are done, return the result by **calling the \`mcp__er-diagram__submit_er_diagram\` tool exactly once** with \`summary\`, \`entities\`, and \`relations\` as arguments. Do not paste the JSON as a fenced text block — the tool call is the deliverable. If the tool returns an error, fix the issue it describes and call the tool again.`;
36
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ER Diagram domain types.
3
+ *
4
+ * An ErEntity is a structured description of one persistence entity in a
5
+ * product — a table, a view, or an enum. The CLI extracts these from schema
6
+ * files (migrations, ORM models, type definitions) and the desktop renders
7
+ * them as an entity-relationship diagram.
8
+ *
9
+ * Companion to DataNodeSchema / ScreenSchema: same flow-graph shape (nodes +
10
+ * edges sharing the `diagrams` table storing the JSONB schema), different domain.
11
+ * ER edges describe foreign-key / inheritance relationships between entities,
12
+ * with a cardinality, not data movement or user navigation.
13
+ */
14
+ export type ErEntityKind = 'entity' | 'view' | 'enum' | 'junction';
15
+ export interface ErColumn {
16
+ /** Column / field name. */
17
+ name: string;
18
+ /** Data type as written in the schema (e.g. 'uuid', 'varchar(255)', 'jsonb'). */
19
+ type?: string;
20
+ /** True if part of the primary key. */
21
+ isPrimaryKey?: boolean;
22
+ /** True if a foreign key referencing another entity. */
23
+ isForeignKey?: boolean;
24
+ /** True if the column is nullable. */
25
+ isNullable?: boolean;
26
+ /** True if the column has a unique constraint. */
27
+ isUnique?: boolean;
28
+ /** One short note about the column's purpose. */
29
+ description?: string;
30
+ /** For foreign keys: 'entity-slug.column' the FK points at. */
31
+ references?: string;
32
+ }
33
+ export interface ErStat {
34
+ label: string;
35
+ value: string;
36
+ }
37
+ export interface ErEntity {
38
+ /** Stable slug within the diagram (e.g. 'users', 'order-items'). */
39
+ slug: string;
40
+ /** Human-readable name (usually the table name). */
41
+ name: string;
42
+ kind: ErEntityKind;
43
+ /** Source file path (jump anchor): the migration / model / schema file. */
44
+ file?: string;
45
+ /** One-sentence description. */
46
+ description?: string;
47
+ /** Columns / fields of the entity. */
48
+ columns?: ErColumn[];
49
+ /** Volume / cardinality hints — free-form key/value pairs. */
50
+ stats?: ErStat[];
51
+ }
52
+ /**
53
+ * Relationship kinds. Direction: fromSlug is the entity that owns the
54
+ * foreign key (the "many"/child side), toSlug is the referenced entity
55
+ * (the "one"/parent side). `inherits` models table inheritance / subtyping.
56
+ */
57
+ export type ErRelationKind = 'one-to-one' | 'one-to-many' | 'many-to-many' | 'inherits';
58
+ export interface ErRelation {
59
+ fromSlug: string;
60
+ toSlug: string;
61
+ kind: ErRelationKind;
62
+ /** Free-form descriptor: 'placed by', 'belongs to', 'tagged with'. */
63
+ label?: string;
64
+ /** Column on the from-entity that holds the foreign key. */
65
+ sourceColumn?: string;
66
+ /** Column on the to-entity being referenced (usually its primary key). */
67
+ targetColumn?: string;
68
+ /** File containing the relationship definition (FK constraint / model). */
69
+ sourceFile?: string;
70
+ }
71
+ export interface ErDiagramExtraction {
72
+ summary: string;
73
+ entities: ErEntity[];
74
+ relations: ErRelation[];
75
+ }
76
+ export declare function isErDiagramExtraction(value: unknown): value is ErDiagramExtraction;