edsger 0.69.0 → 0.71.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 (78) 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/discover/index.d.ts +14 -0
  12. package/dist/commands/discover/index.js +29 -0
  13. package/dist/commands/er-diagram/index.d.ts +19 -0
  14. package/dist/commands/er-diagram/index.js +55 -0
  15. package/dist/commands/flowchart/index.d.ts +8 -0
  16. package/dist/commands/flowchart/index.js +10 -0
  17. package/dist/commands/screen-flow/index.d.ts +5 -5
  18. package/dist/commands/screen-flow/index.js +8 -8
  19. package/dist/commands/sequence-diagram/index.d.ts +19 -0
  20. package/dist/commands/sequence-diagram/index.js +55 -0
  21. package/dist/commands/state-diagram/index.d.ts +7 -0
  22. package/dist/commands/state-diagram/index.js +9 -0
  23. package/dist/index.js +139 -5
  24. package/dist/phases/architecture-diagram/index.d.ts +15 -0
  25. package/dist/phases/architecture-diagram/index.js +51 -0
  26. package/dist/phases/class-diagram/index.d.ts +14 -0
  27. package/dist/phases/class-diagram/index.js +76 -0
  28. package/dist/phases/data-flow/index.d.ts +2 -2
  29. package/dist/phases/data-flow/index.js +37 -37
  30. package/dist/phases/data-flow/mcp-server.d.ts +1 -1
  31. package/dist/phases/data-flow/mcp-server.js +2 -2
  32. package/dist/phases/data-flow/types.d.ts +1 -1
  33. package/dist/phases/data-flow/types.js +1 -1
  34. package/dist/phases/diagram-shared/clone-repos.d.ts +63 -0
  35. package/dist/phases/diagram-shared/clone-repos.js +153 -0
  36. package/dist/phases/diagram-shared/generate.d.ts +42 -0
  37. package/dist/phases/diagram-shared/generate.js +162 -0
  38. package/dist/phases/diagram-shared/graph.d.ts +62 -0
  39. package/dist/phases/diagram-shared/graph.js +169 -0
  40. package/dist/phases/diagram-shared/mcp.d.ts +35 -0
  41. package/dist/phases/diagram-shared/mcp.js +68 -0
  42. package/dist/phases/diagram-shared/prompts.d.ts +23 -0
  43. package/dist/phases/diagram-shared/prompts.js +35 -0
  44. package/dist/phases/discover-services/index.d.ts +29 -0
  45. package/dist/phases/discover-services/index.js +528 -0
  46. package/dist/phases/er-diagram/index.d.ts +28 -0
  47. package/dist/phases/er-diagram/index.js +290 -0
  48. package/dist/phases/er-diagram/mcp-server.d.ts +77 -0
  49. package/dist/phases/er-diagram/mcp-server.js +144 -0
  50. package/dist/phases/er-diagram/prompts.d.ts +14 -0
  51. package/dist/phases/er-diagram/prompts.js +36 -0
  52. package/dist/phases/er-diagram/types.d.ts +76 -0
  53. package/dist/phases/er-diagram/types.js +84 -0
  54. package/dist/phases/flowchart/index.d.ts +15 -0
  55. package/dist/phases/flowchart/index.js +50 -0
  56. package/dist/phases/output-contracts.js +178 -2
  57. package/dist/phases/screen-flow/index.d.ts +3 -3
  58. package/dist/phases/screen-flow/index.js +47 -45
  59. package/dist/phases/screen-flow/mcp-server.js +2 -2
  60. package/dist/phases/sequence-diagram/index.d.ts +30 -0
  61. package/dist/phases/sequence-diagram/index.js +290 -0
  62. package/dist/phases/sequence-diagram/mcp-server.d.ts +64 -0
  63. package/dist/phases/sequence-diagram/mcp-server.js +134 -0
  64. package/dist/phases/sequence-diagram/prompts.d.ts +14 -0
  65. package/dist/phases/sequence-diagram/prompts.js +36 -0
  66. package/dist/phases/sequence-diagram/types.d.ts +52 -0
  67. package/dist/phases/sequence-diagram/types.js +93 -0
  68. package/dist/phases/state-diagram/index.d.ts +15 -0
  69. package/dist/phases/state-diagram/index.js +53 -0
  70. package/dist/skills/phase/architecture-diagram/SKILL.md +41 -0
  71. package/dist/skills/phase/class-diagram/SKILL.md +44 -0
  72. package/dist/skills/phase/er-diagram/SKILL.md +71 -0
  73. package/dist/skills/phase/flowchart/SKILL.md +38 -0
  74. package/dist/skills/phase/sequence-diagram/SKILL.md +67 -0
  75. package/dist/skills/phase/state-diagram/SKILL.md +38 -0
  76. package/dist/workspace/session-workspace.d.ts +2 -2
  77. package/dist/workspace/session-workspace.js +2 -2
  78. package/package.json +1 -1
@@ -0,0 +1,290 @@
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
+ import { query } from '@anthropic-ai/claude-agent-sdk';
15
+ import { getRepositoryBasics } from '../../api/github.js';
16
+ import { DEFAULT_MODEL } from '../../constants.js';
17
+ import { getSupabase } from '../../supabase/client.js';
18
+ import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
19
+ import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
20
+ import { cloneDiagramRepos, describeRepoScope, } from '../diagram-shared/clone-repos.js';
21
+ import { fetchProductBasics } from '../find-shared/mcp.js';
22
+ import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
23
+ import { createSequenceDiagramCaptureState, createSequenceDiagramMcpServer, validateConsistency, } from './mcp-server.js';
24
+ import { createSequenceDiagramSystemPrompt, createSequenceDiagramUserPrompt, } from './prompts.js';
25
+ import { isSequenceDiagramExtraction, } from './types.js';
26
+ const WORKSPACE_KEY = 'sequence-diagram';
27
+ const MAX_TURNS = 150;
28
+ // Auto-layout: participants laid out left-to-right as lifeline columns; users
29
+ // can drag afterwards and we persist positions.
30
+ const COLUMN_WIDTH = 280;
31
+ export async function runSequenceDiagramPhase(options) {
32
+ const { productId, repoId, diagramId, guidance, verbose } = options;
33
+ const repoOnly = !productId && Boolean(repoId);
34
+ if (productId) {
35
+ logInfo(`Starting sequence-diagram generation for product ${productId}`);
36
+ }
37
+ else {
38
+ logInfo(`Starting sequence-diagram generation for repository ${repoId}`);
39
+ }
40
+ const supabase = getSupabase();
41
+ await markDiagramRunning(supabase, diagramId);
42
+ const repositoryIds = await getDiagramRepositoryIds(supabase, diagramId);
43
+ const cloneResult = await cloneDiagramRepos({
44
+ productId,
45
+ repoId,
46
+ repositoryIds,
47
+ workspaceKey: WORKSPACE_KEY,
48
+ verbose,
49
+ });
50
+ if (!cloneResult.ok) {
51
+ await markDiagramFailed(supabase, diagramId, cloneResult.message);
52
+ return { status: 'error', message: cloneResult.message };
53
+ }
54
+ const { projectDir, cleanupDir, repos } = cloneResult;
55
+ let succeeded = false;
56
+ try {
57
+ const product = repoOnly
58
+ ? await resolveRepoBasics(repoId, repos)
59
+ : await fetchProductBasics(productId);
60
+ const systemPrompt = await createSequenceDiagramSystemPrompt({
61
+ projectDir,
62
+ hasCodebase: true,
63
+ });
64
+ const repoScope = describeRepoScope(repos);
65
+ const userPrompt = createSequenceDiagramUserPrompt({
66
+ productName: product.name,
67
+ productDescription: product.description,
68
+ guidance: [guidance, repoScope].filter(Boolean).join('\n\n') || undefined,
69
+ });
70
+ logInfo('Running Claude sequence-diagram extraction...');
71
+ const captureState = createSequenceDiagramCaptureState();
72
+ const mcpServer = createSequenceDiagramMcpServer(captureState, {
73
+ onProgress: ({ phase, message }) => {
74
+ logInfo(`[${phase}] ${message}`);
75
+ },
76
+ });
77
+ let lastAssistantResponse = '';
78
+ let extraction = null;
79
+ for await (const message of query({
80
+ prompt: createPromptGenerator(userPrompt),
81
+ options: {
82
+ systemPrompt: {
83
+ type: 'preset',
84
+ preset: 'claude_code',
85
+ append: systemPrompt,
86
+ },
87
+ model: DEFAULT_MODEL,
88
+ maxTurns: MAX_TURNS,
89
+ permissionMode: 'bypassPermissions',
90
+ cwd: projectDir,
91
+ mcpServers: {
92
+ 'sequence-diagram': mcpServer,
93
+ },
94
+ },
95
+ })) {
96
+ const { assistantBuffer, extraction: nextExtraction } = processSdkMessage(message, lastAssistantResponse, captureState, verbose);
97
+ lastAssistantResponse = assistantBuffer;
98
+ if (nextExtraction) {
99
+ extraction = nextExtraction;
100
+ }
101
+ }
102
+ if (!extraction) {
103
+ const msg = 'Sequence diagram extraction failed: agent did not call submit_sequence_diagram and no parseable sequence_diagram block was found in the response';
104
+ await markDiagramFailed(supabase, diagramId, msg);
105
+ return { status: 'error', message: msg };
106
+ }
107
+ logInfo(`Extraction produced ${extraction.participants.length} participants / ${extraction.messages.length} messages`);
108
+ const { nodesCreated, edgesCreated } = await persistDiagram(supabase, diagramId, extraction);
109
+ await markDiagramSuccess(supabase, diagramId, extraction.summary);
110
+ succeeded = true;
111
+ logSuccess(`Sequence diagram generated: ${nodesCreated} participants, ${edgesCreated} messages`);
112
+ return {
113
+ status: 'success',
114
+ message: `Sequence diagram generated (${nodesCreated} participants, ${edgesCreated} messages)`,
115
+ nodesCreated,
116
+ edgesCreated,
117
+ summary: extraction.summary,
118
+ };
119
+ }
120
+ catch (error) {
121
+ const errorMessage = error instanceof Error ? error.message : String(error);
122
+ logError(`Sequence diagram failed: ${errorMessage}`);
123
+ await markDiagramFailed(supabase, diagramId, errorMessage);
124
+ return { status: 'error', message: errorMessage };
125
+ }
126
+ finally {
127
+ if (succeeded) {
128
+ cleanupIssueRepo(cleanupDir);
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * Build "product basics" for repo-only mode from the repositories row,
134
+ * falling back to the cloned repo's full name when the row has no name.
135
+ */
136
+ async function resolveRepoBasics(repositoryId, repos) {
137
+ const basics = await getRepositoryBasics(repositoryId).catch(() => null);
138
+ return {
139
+ name: basics?.fullName ?? repos[0]?.fullName ?? repositoryId,
140
+ description: basics?.description ?? undefined,
141
+ };
142
+ }
143
+ /** Read the ordered repo set a diagram was scoped to (may be empty). */
144
+ async function getDiagramRepositoryIds(supabase, diagramId) {
145
+ const { data } = await supabase
146
+ .from('diagrams')
147
+ .select('repository_ids')
148
+ .eq('id', diagramId)
149
+ .single();
150
+ return (data?.repository_ids ?? []).filter(Boolean);
151
+ }
152
+ function processSdkMessage(
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ message, assistantBuffer, captureState, verbose) {
155
+ if (message.type === 'assistant') {
156
+ const next = assistantBuffer +
157
+ extractTextFromContent(message.message?.content ?? [], verbose);
158
+ return { assistantBuffer: next, extraction: null };
159
+ }
160
+ if (message.type === 'user' && verbose) {
161
+ const userContent = message.message?.content;
162
+ if (Array.isArray(userContent)) {
163
+ extractTextFromContent(userContent, verbose);
164
+ }
165
+ return { assistantBuffer, extraction: null };
166
+ }
167
+ if (message.type !== 'result') {
168
+ return { assistantBuffer, extraction: null };
169
+ }
170
+ if (captureState.captured) {
171
+ return { assistantBuffer, extraction: captureState.captured };
172
+ }
173
+ const fallback = tryFallbackParse(message, assistantBuffer);
174
+ if (fallback) {
175
+ logWarning('Agent emitted a fenced sequence_diagram block instead of calling submit_sequence_diagram; using the parsed text as a fallback.');
176
+ return { assistantBuffer, extraction: fallback };
177
+ }
178
+ if (message.subtype !== 'success') {
179
+ logError(`Extraction incomplete: ${message.subtype}`);
180
+ }
181
+ return { assistantBuffer, extraction: null };
182
+ }
183
+ function tryFallbackParse(resultMessage, assistantText) {
184
+ const responseText = resultMessage.subtype === 'success'
185
+ ? resultMessage.result || assistantText
186
+ : assistantText;
187
+ const parsed = tryExtractResult(responseText, 'sequence_diagram');
188
+ if (!isSequenceDiagramExtraction(parsed)) {
189
+ return null;
190
+ }
191
+ const { error } = validateConsistency(parsed);
192
+ if (error) {
193
+ logWarning(`Fallback extraction failed consistency check: ${error}`);
194
+ return null;
195
+ }
196
+ return parsed;
197
+ }
198
+ // ============================================================================
199
+ // Persistence
200
+ // ============================================================================
201
+ async function markDiagramRunning(supabase, diagramId) {
202
+ const { error } = await supabase
203
+ .from('diagrams')
204
+ .update({ status: 'running', error: null })
205
+ .eq('id', diagramId);
206
+ if (error) {
207
+ logWarning(`Could not mark diagram as running: ${error.message}`);
208
+ }
209
+ }
210
+ async function markDiagramFailed(supabase, diagramId, errorMessage) {
211
+ await supabase
212
+ .from('diagrams')
213
+ .update({
214
+ status: 'failed',
215
+ error: errorMessage,
216
+ completed_at: new Date().toISOString(),
217
+ })
218
+ .eq('id', diagramId);
219
+ }
220
+ async function markDiagramSuccess(supabase, diagramId, summary) {
221
+ await supabase
222
+ .from('diagrams')
223
+ .update({
224
+ status: 'success',
225
+ summary,
226
+ error: null,
227
+ completed_at: new Date().toISOString(),
228
+ })
229
+ .eq('id', diagramId);
230
+ }
231
+ async function persistDiagram(supabase, diagramId, extraction) {
232
+ await supabase.from('diagram_edges').delete().eq('diagram_id', diagramId);
233
+ await supabase.from('diagram_nodes').delete().eq('diagram_id', diagramId);
234
+ if (extraction.participants.length === 0) {
235
+ return { nodesCreated: 0, edgesCreated: 0 };
236
+ }
237
+ const nodeRows = extraction.participants.map((p, i) => buildNodeRow(diagramId, p, i));
238
+ const { data: insertedNodes, error: nodesError } = await supabase
239
+ .from('diagram_nodes')
240
+ .insert(nodeRows)
241
+ .select('id, slug');
242
+ if (nodesError) {
243
+ throw new Error(`Failed to insert participants: ${nodesError.message}`);
244
+ }
245
+ const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
246
+ const edgeRows = extraction.messages
247
+ .map((m) => buildEdgeRow(diagramId, m, slugToId))
248
+ .filter((e) => e !== null);
249
+ if (edgeRows.length > 0) {
250
+ const { error: edgesError } = await supabase
251
+ .from('diagram_edges')
252
+ .insert(edgeRows);
253
+ if (edgesError) {
254
+ throw new Error(`Failed to insert messages: ${edgesError.message}`);
255
+ }
256
+ }
257
+ return {
258
+ nodesCreated: nodeRows.length,
259
+ edgesCreated: edgeRows.length,
260
+ };
261
+ }
262
+ function buildNodeRow(diagramId, participant, index) {
263
+ return {
264
+ diagram_id: diagramId,
265
+ slug: participant.slug,
266
+ name: participant.name,
267
+ kind: participant.kind,
268
+ schema: participant,
269
+ // Lifelines sit in a single row, ordered left-to-right by emission order.
270
+ position_x: index * COLUMN_WIDTH,
271
+ position_y: 0,
272
+ };
273
+ }
274
+ function buildEdgeRow(diagramId, message, slugToId) {
275
+ const fromId = slugToId.get(message.fromSlug);
276
+ const toId = slugToId.get(message.toSlug);
277
+ if (!fromId || !toId) {
278
+ return null;
279
+ }
280
+ return {
281
+ diagram_id: diagramId,
282
+ from_node_id: fromId,
283
+ to_node_id: toId,
284
+ label: message.label ?? null,
285
+ source_anchor: message.sourceFile ?? null,
286
+ kind: message.kind,
287
+ // Message timeline position — the renderer sorts edges by this.
288
+ metadata: { order: message.order },
289
+ };
290
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * In-process MCP server for the sequence-diagram phase. Exposes a single
3
+ * tool — `submit_sequence_diagram` — that the agent calls with the structured
4
+ * extraction, 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 { SequenceDiagramExtraction } from './types.js';
11
+ export interface SequenceDiagramCaptureState {
12
+ captured: SequenceDiagramExtraction | null;
13
+ }
14
+ export declare function createSequenceDiagramCaptureState(): SequenceDiagramCaptureState;
15
+ export type SequenceDiagramProgressSink = (event: {
16
+ phase: 'detection' | 'enumeration' | 'participants' | 'messages' | 'submission';
17
+ message: string;
18
+ }) => void;
19
+ export declare function validateConsistency(extraction: SequenceDiagramExtraction): {
20
+ error: string | null;
21
+ };
22
+ export declare function createSubmitSequenceDiagramTool(state: SequenceDiagramCaptureState): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
23
+ summary: z.ZodString;
24
+ participants: z.ZodArray<z.ZodObject<{
25
+ slug: z.ZodString;
26
+ name: z.ZodString;
27
+ kind: z.ZodEnum<{
28
+ service: "service";
29
+ external: "external";
30
+ queue: "queue";
31
+ actor: "actor";
32
+ component: "component";
33
+ database: "database";
34
+ }>;
35
+ file: z.ZodOptional<z.ZodString>;
36
+ description: z.ZodOptional<z.ZodString>;
37
+ }, z.core.$strip>>;
38
+ messages: z.ZodArray<z.ZodObject<{
39
+ fromSlug: z.ZodString;
40
+ toSlug: z.ZodString;
41
+ kind: z.ZodEnum<{
42
+ sync: "sync";
43
+ async: "async";
44
+ return: "return";
45
+ self: "self";
46
+ }>;
47
+ order: z.ZodNumber;
48
+ label: z.ZodOptional<z.ZodString>;
49
+ sourceFile: z.ZodOptional<z.ZodString>;
50
+ }, z.core.$strip>>;
51
+ }>;
52
+ export declare function createRecordProgressTool(sink?: SequenceDiagramProgressSink): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
53
+ phase: z.ZodEnum<{
54
+ detection: "detection";
55
+ enumeration: "enumeration";
56
+ submission: "submission";
57
+ participants: "participants";
58
+ messages: "messages";
59
+ }>;
60
+ message: z.ZodString;
61
+ }>;
62
+ export declare function createSequenceDiagramMcpServer(state: SequenceDiagramCaptureState, options?: {
63
+ onProgress?: SequenceDiagramProgressSink;
64
+ }): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * In-process MCP server for the sequence-diagram phase. Exposes a single
3
+ * tool — `submit_sequence_diagram` — that the agent calls with the structured
4
+ * extraction, 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 createSequenceDiagramCaptureState() {
12
+ return { captured: null };
13
+ }
14
+ // ---------------------------------------------------------------------------
15
+ // Zod schemas (mirror types.ts)
16
+ // ---------------------------------------------------------------------------
17
+ const participantSchema = z.object({
18
+ slug: z.string().min(1),
19
+ name: z.string().min(1),
20
+ kind: z.enum([
21
+ 'actor',
22
+ 'service',
23
+ 'component',
24
+ 'database',
25
+ 'queue',
26
+ 'external',
27
+ ]),
28
+ file: z.string().optional(),
29
+ description: z.string().optional(),
30
+ });
31
+ const messageSchema = z.object({
32
+ fromSlug: z.string().min(1),
33
+ toSlug: z.string().min(1),
34
+ kind: z.enum(['sync', 'async', 'return', 'self']),
35
+ order: z.number().int(),
36
+ label: z.string().optional(),
37
+ sourceFile: z.string().optional(),
38
+ });
39
+ export function validateConsistency(extraction) {
40
+ const slugs = new Set();
41
+ for (const participant of extraction.participants) {
42
+ if (slugs.has(participant.slug)) {
43
+ return {
44
+ error: `Duplicate participant slug "${participant.slug}". Each participant.slug MUST be unique within the diagram. Re-call submit_sequence_diagram with deduplicated participants.`,
45
+ };
46
+ }
47
+ slugs.add(participant.slug);
48
+ }
49
+ for (const message of extraction.messages) {
50
+ if (!slugs.has(message.fromSlug)) {
51
+ return {
52
+ error: `Message fromSlug "${message.fromSlug}" → "${message.toSlug}" does not match any participant slug. Either add the missing participant or drop the message, then re-call submit_sequence_diagram.`,
53
+ };
54
+ }
55
+ if (!slugs.has(message.toSlug)) {
56
+ return {
57
+ error: `Message fromSlug "${message.fromSlug}" → toSlug "${message.toSlug}" does not match any participant slug. Either add the missing participant or drop the message, then re-call submit_sequence_diagram.`,
58
+ };
59
+ }
60
+ }
61
+ return { error: null };
62
+ }
63
+ export function createSubmitSequenceDiagramTool(state) {
64
+ return tool('submit_sequence_diagram', [
65
+ 'Submit the final sequence diagram. Call this EXACTLY once, when you',
66
+ 'have finished mapping every participant and the ordered messages',
67
+ 'between them. Pass the full structured diagram as the argument. After',
68
+ 'this call succeeds, end your turn — do NOT also paste the same data as',
69
+ 'a fenced code block. If validation fails, the error message tells you',
70
+ 'what to fix; call the tool again with corrected data.',
71
+ ].join(' '), {
72
+ summary: z
73
+ .string()
74
+ .min(1)
75
+ .describe('1-3 sentence narrative of the scenario this diagram captures and the participants involved.'),
76
+ participants: z
77
+ .array(participantSchema)
78
+ .describe('Every participant / lifeline: actor / service / component / database / queue / external. participant.slug MUST be unique within the diagram.'),
79
+ messages: z
80
+ .array(messageSchema)
81
+ .describe('The ordered interactions. Each message has a 1-based `order` defining its position in the timeline. fromSlug = sender, toSlug = receiver. Every fromSlug / toSlug MUST reference a slug present in participants; drop messages whose endpoints you did not emit.'),
82
+ }, async (args) => {
83
+ const extraction = {
84
+ summary: args.summary,
85
+ participants: args.participants,
86
+ messages: args.messages,
87
+ };
88
+ const { error } = validateConsistency(extraction);
89
+ if (error) {
90
+ return {
91
+ content: [{ type: 'text', text: error }],
92
+ isError: true,
93
+ };
94
+ }
95
+ state.captured = extraction;
96
+ return {
97
+ content: [
98
+ {
99
+ type: 'text',
100
+ text: `Captured ${extraction.participants.length} participants / ${extraction.messages.length} messages. End your turn now.`,
101
+ },
102
+ ],
103
+ };
104
+ });
105
+ }
106
+ export function createRecordProgressTool(sink) {
107
+ 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.', {
108
+ phase: z
109
+ .enum([
110
+ 'detection',
111
+ 'enumeration',
112
+ 'participants',
113
+ 'messages',
114
+ 'submission',
115
+ ])
116
+ .describe('Which phase the message belongs to.'),
117
+ message: z.string().min(1).describe('Human-readable status update.'),
118
+ }, async (args) => {
119
+ sink?.({ phase: args.phase, message: args.message });
120
+ return {
121
+ content: [{ type: 'text', text: 'ok' }],
122
+ };
123
+ });
124
+ }
125
+ export function createSequenceDiagramMcpServer(state, options) {
126
+ return createSdkMcpServer({
127
+ name: 'sequence-diagram',
128
+ version: '1.0.0',
129
+ tools: [
130
+ createSubmitSequenceDiagramTool(state),
131
+ createRecordProgressTool(options?.onProgress),
132
+ ],
133
+ });
134
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Prompts for the sequence-diagram phase. Loads the system prompt body from
3
+ * `skills/phase/sequence-diagram/SKILL.md` (with optional project override)
4
+ * and appends the JSON output contract.
5
+ */
6
+ export declare function createSequenceDiagramSystemPrompt(options?: {
7
+ projectDir?: string;
8
+ hasCodebase?: boolean;
9
+ }): Promise<string>;
10
+ export declare function createSequenceDiagramUserPrompt(args: {
11
+ productName: string;
12
+ productDescription?: string;
13
+ guidance?: string;
14
+ }): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Prompts for the sequence-diagram phase. Loads the system prompt body from
3
+ * `skills/phase/sequence-diagram/SKILL.md` (with optional project override)
4
+ * and 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 createSequenceDiagramSystemPrompt(options) {
9
+ const skill = await resolveSkill('phase/sequence-diagram', {
10
+ projectDir: options?.projectDir,
11
+ });
12
+ if (!skill) {
13
+ throw new Error('Failed to load skill: phase/sequence-diagram');
14
+ }
15
+ const prompt = processConditionals(skill.prompt, {
16
+ hasCodebase: options?.hasCodebase ?? true,
17
+ });
18
+ return `${prompt}
19
+
20
+ ${OUTPUT_CONTRACTS['sequence-diagram']}`;
21
+ }
22
+ export function createSequenceDiagramUserPrompt(args) {
23
+ const scenarioBlock = args.guidance
24
+ ? `\n\n**Scenario to map** (human guidance — diagram THIS interaction):\n${args.guidance}`
25
+ : '\n\nNo specific scenario was given — pick the single most central end-to-end interaction in the product (e.g. the primary user action, request lifecycle, or auth flow) and map that one.';
26
+ const descBlock = args.productDescription
27
+ ? `\n**Product description**: ${args.productDescription}`
28
+ : '';
29
+ return `Map a sequence diagram for **${args.productName}**.${descBlock}${scenarioBlock}
30
+
31
+ A sequence diagram captures ONE scenario / flow of control as an ordered series of messages between participants. Start by detecting the stack, then trace the chosen scenario through the code — the entry point (route handler / event handler / CLI command), the services and components it calls, the datastores and external systems it touches — following the call chain in order. Read just enough per step to label each message accurately.
32
+
33
+ Call \`mcp__sequence-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__sequence-diagram__submit_sequence_diagram\` tool exactly once** with \`summary\`, \`participants\`, and \`messages\` as arguments. Number the messages with a 1-based \`order\` that reflects execution order. 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,52 @@
1
+ /**
2
+ * Sequence Diagram domain types.
3
+ *
4
+ * A SequenceParticipant is one lifeline in an interaction — an actor, a
5
+ * service, a database, an external system, a queue, or an internal
6
+ * component. A SequenceMessage is one ordered interaction between two
7
+ * participants (a call, an async send, a return, or a self-message).
8
+ *
9
+ * A sequence diagram captures ONE scenario / flow of control (e.g. "user
10
+ * checkout", "OAuth login"); the user's guidance picks the scenario. The CLI
11
+ * extracts it from source code (call sites, handlers, SDK calls) and the
12
+ * desktop renders participants as lifelines with the messages ordered by the
13
+ * `order` field.
14
+ *
15
+ * Companion to DataNodeSchema / ErEntity: same flow-graph shape (nodes +
16
+ * edges sharing the `diagrams` table storing the JSONB schema), different domain.
17
+ */
18
+ export type SequenceParticipantKind = 'actor' | 'service' | 'component' | 'database' | 'queue' | 'external';
19
+ export interface SequenceParticipant {
20
+ /** Stable slug within the diagram (e.g. 'user', 'auth-service'). */
21
+ slug: string;
22
+ /** Human-readable name. */
23
+ name: string;
24
+ kind: SequenceParticipantKind;
25
+ /** Source file path (jump anchor) where the participant is defined. */
26
+ file?: string;
27
+ /** One-sentence description of the participant's role. */
28
+ description?: string;
29
+ }
30
+ /**
31
+ * Message kinds describe the nature of the interaction arrow.
32
+ */
33
+ export type SequenceMessageKind = 'sync' | 'async' | 'return' | 'self';
34
+ export interface SequenceMessage {
35
+ /** Sender participant slug. */
36
+ fromSlug: string;
37
+ /** Receiver participant slug. */
38
+ toSlug: string;
39
+ kind: SequenceMessageKind;
40
+ /** 1-based position of this message in the interaction timeline. */
41
+ order: number;
42
+ /** The call / message text ('POST /login', 'validateToken()', 'rows'). */
43
+ label?: string;
44
+ /** File + line where this call happens (for jump-to-code). */
45
+ sourceFile?: string;
46
+ }
47
+ export interface SequenceDiagramExtraction {
48
+ summary: string;
49
+ participants: SequenceParticipant[];
50
+ messages: SequenceMessage[];
51
+ }
52
+ export declare function isSequenceDiagramExtraction(value: unknown): value is SequenceDiagramExtraction;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Sequence Diagram domain types.
3
+ *
4
+ * A SequenceParticipant is one lifeline in an interaction — an actor, a
5
+ * service, a database, an external system, a queue, or an internal
6
+ * component. A SequenceMessage is one ordered interaction between two
7
+ * participants (a call, an async send, a return, or a self-message).
8
+ *
9
+ * A sequence diagram captures ONE scenario / flow of control (e.g. "user
10
+ * checkout", "OAuth login"); the user's guidance picks the scenario. The CLI
11
+ * extracts it from source code (call sites, handlers, SDK calls) and the
12
+ * desktop renders participants as lifelines with the messages ordered by the
13
+ * `order` field.
14
+ *
15
+ * Companion to DataNodeSchema / ErEntity: same flow-graph shape (nodes +
16
+ * edges sharing the `diagrams` table storing the JSONB schema), different domain.
17
+ */
18
+ // ============================================================================
19
+ // Runtime validation for AI-produced extraction
20
+ // ============================================================================
21
+ const PARTICIPANT_KINDS = new Set([
22
+ 'actor',
23
+ 'service',
24
+ 'component',
25
+ 'database',
26
+ 'queue',
27
+ 'external',
28
+ ]);
29
+ const MESSAGE_KINDS = new Set([
30
+ 'sync',
31
+ 'async',
32
+ 'return',
33
+ 'self',
34
+ ]);
35
+ function isRecord(value) {
36
+ return typeof value === 'object' && value !== null;
37
+ }
38
+ function isSequenceParticipant(value) {
39
+ if (!isRecord(value)) {
40
+ return false;
41
+ }
42
+ if (typeof value.slug !== 'string' || value.slug.length === 0) {
43
+ return false;
44
+ }
45
+ if (typeof value.name !== 'string' || value.name.length === 0) {
46
+ return false;
47
+ }
48
+ if (typeof value.kind !== 'string' ||
49
+ !PARTICIPANT_KINDS.has(value.kind)) {
50
+ return false;
51
+ }
52
+ return true;
53
+ }
54
+ function isSequenceMessage(value) {
55
+ if (!isRecord(value)) {
56
+ return false;
57
+ }
58
+ if (typeof value.fromSlug !== 'string') {
59
+ return false;
60
+ }
61
+ if (typeof value.toSlug !== 'string') {
62
+ return false;
63
+ }
64
+ if (typeof value.kind !== 'string' ||
65
+ !MESSAGE_KINDS.has(value.kind)) {
66
+ return false;
67
+ }
68
+ if (typeof value.order !== 'number') {
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+ export function isSequenceDiagramExtraction(value) {
74
+ if (!isRecord(value)) {
75
+ return false;
76
+ }
77
+ if (typeof value.summary !== 'string') {
78
+ return false;
79
+ }
80
+ if (!Array.isArray(value.participants)) {
81
+ return false;
82
+ }
83
+ if (!Array.isArray(value.messages)) {
84
+ return false;
85
+ }
86
+ if (!value.participants.every(isSequenceParticipant)) {
87
+ return false;
88
+ }
89
+ if (!value.messages.every(isSequenceMessage)) {
90
+ return false;
91
+ }
92
+ return true;
93
+ }