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.
- package/dist/api/github.d.ts +1 -1
- package/dist/api/github.js +1 -1
- package/dist/commands/architecture-diagram/index.d.ts +8 -0
- package/dist/commands/architecture-diagram/index.js +10 -0
- package/dist/commands/class-diagram/index.d.ts +7 -0
- package/dist/commands/class-diagram/index.js +9 -0
- package/dist/commands/data-flow/index.d.ts +5 -5
- package/dist/commands/data-flow/index.js +8 -8
- package/dist/commands/diagram-shared/index.d.ts +21 -0
- package/dist/commands/diagram-shared/index.js +37 -0
- package/dist/commands/er-diagram/index.d.ts +19 -0
- package/dist/commands/er-diagram/index.js +55 -0
- package/dist/commands/flowchart/index.d.ts +8 -0
- package/dist/commands/flowchart/index.js +10 -0
- package/dist/commands/screen-flow/index.d.ts +5 -5
- package/dist/commands/screen-flow/index.js +8 -8
- package/dist/commands/sequence-diagram/index.d.ts +19 -0
- package/dist/commands/sequence-diagram/index.js +55 -0
- package/dist/commands/state-diagram/index.d.ts +7 -0
- package/dist/commands/state-diagram/index.js +9 -0
- package/dist/index.js +117 -5
- package/dist/phases/architecture-diagram/index.d.ts +15 -0
- package/dist/phases/architecture-diagram/index.js +51 -0
- package/dist/phases/class-diagram/index.d.ts +14 -0
- package/dist/phases/class-diagram/index.js +76 -0
- package/dist/phases/data-flow/index.d.ts +2 -2
- package/dist/phases/data-flow/index.js +36 -36
- package/dist/phases/data-flow/mcp-server.d.ts +1 -1
- package/dist/phases/data-flow/mcp-server.js +2 -2
- package/dist/phases/data-flow/types.d.ts +1 -1
- package/dist/phases/data-flow/types.js +1 -1
- package/dist/phases/diagram-shared/clone-repos.d.ts +63 -0
- package/dist/phases/diagram-shared/clone-repos.js +153 -0
- package/dist/phases/diagram-shared/generate.d.ts +42 -0
- package/dist/phases/diagram-shared/generate.js +162 -0
- package/dist/phases/diagram-shared/graph.d.ts +62 -0
- package/dist/phases/diagram-shared/graph.js +169 -0
- package/dist/phases/diagram-shared/mcp.d.ts +35 -0
- package/dist/phases/diagram-shared/mcp.js +68 -0
- package/dist/phases/diagram-shared/prompts.d.ts +23 -0
- package/dist/phases/diagram-shared/prompts.js +35 -0
- package/dist/phases/er-diagram/index.d.ts +28 -0
- package/dist/phases/er-diagram/index.js +290 -0
- package/dist/phases/er-diagram/mcp-server.d.ts +77 -0
- package/dist/phases/er-diagram/mcp-server.js +144 -0
- package/dist/phases/er-diagram/prompts.d.ts +14 -0
- package/dist/phases/er-diagram/prompts.js +36 -0
- package/dist/phases/er-diagram/types.d.ts +76 -0
- package/dist/phases/er-diagram/types.js +84 -0
- package/dist/phases/flowchart/index.d.ts +15 -0
- package/dist/phases/flowchart/index.js +50 -0
- package/dist/phases/output-contracts.js +178 -2
- package/dist/phases/screen-flow/index.d.ts +3 -3
- package/dist/phases/screen-flow/index.js +43 -43
- package/dist/phases/screen-flow/mcp-server.js +2 -2
- package/dist/phases/sequence-diagram/index.d.ts +30 -0
- package/dist/phases/sequence-diagram/index.js +290 -0
- package/dist/phases/sequence-diagram/mcp-server.d.ts +64 -0
- package/dist/phases/sequence-diagram/mcp-server.js +134 -0
- package/dist/phases/sequence-diagram/prompts.d.ts +14 -0
- package/dist/phases/sequence-diagram/prompts.js +36 -0
- package/dist/phases/sequence-diagram/types.d.ts +52 -0
- package/dist/phases/sequence-diagram/types.js +93 -0
- package/dist/phases/state-diagram/index.d.ts +15 -0
- package/dist/phases/state-diagram/index.js +53 -0
- package/dist/skills/phase/architecture-diagram/SKILL.md +41 -0
- package/dist/skills/phase/class-diagram/SKILL.md +44 -0
- package/dist/skills/phase/er-diagram/SKILL.md +71 -0
- package/dist/skills/phase/flowchart/SKILL.md +38 -0
- package/dist/skills/phase/sequence-diagram/SKILL.md +67 -0
- package/dist/skills/phase/state-diagram/SKILL.md +38 -0
- package/dist/workspace/session-workspace.d.ts +2 -2
- package/dist/workspace/session-workspace.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
// ============================================================================
|
|
15
|
+
// Runtime validation for AI-produced extraction
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const ENTITY_KINDS = new Set([
|
|
18
|
+
'entity',
|
|
19
|
+
'view',
|
|
20
|
+
'enum',
|
|
21
|
+
'junction',
|
|
22
|
+
]);
|
|
23
|
+
const RELATION_KINDS = new Set([
|
|
24
|
+
'one-to-one',
|
|
25
|
+
'one-to-many',
|
|
26
|
+
'many-to-many',
|
|
27
|
+
'inherits',
|
|
28
|
+
]);
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return typeof value === 'object' && value !== null;
|
|
31
|
+
}
|
|
32
|
+
function isErEntity(value) {
|
|
33
|
+
if (!isRecord(value)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value.slug !== 'string' || value.slug.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value.name !== 'string' || value.name.length === 0) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value.kind !== 'string' ||
|
|
43
|
+
!ENTITY_KINDS.has(value.kind)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function isErRelation(value) {
|
|
49
|
+
if (!isRecord(value)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value.fromSlug !== 'string') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value.toSlug !== 'string') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value.kind !== 'string' ||
|
|
59
|
+
!RELATION_KINDS.has(value.kind)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
export function isErDiagramExtraction(value) {
|
|
65
|
+
if (!isRecord(value)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value.summary !== 'string') {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(value.entities)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(value.relations)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (!value.entities.every(isErEntity)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (!value.relations.every(isErRelation)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flowchart phase: map the control flow of one process / function / algorithm
|
|
3
|
+
* as a flowchart — start/end, process steps, decisions (with branch labels),
|
|
4
|
+
* I/O, and subroutine calls. Persisted to the diagrams tables with
|
|
5
|
+
* `type = 'flowchart'`.
|
|
6
|
+
*/
|
|
7
|
+
import { type DiagramPhaseResult } from '../diagram-shared/generate.js';
|
|
8
|
+
export interface FlowchartPhaseOptions {
|
|
9
|
+
productId?: string;
|
|
10
|
+
repoId?: string;
|
|
11
|
+
diagramId: string;
|
|
12
|
+
guidance?: string;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function runFlowchartPhase(options: FlowchartPhaseOptions): Promise<DiagramPhaseResult>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flowchart phase: map the control flow of one process / function / algorithm
|
|
3
|
+
* as a flowchart — start/end, process steps, decisions (with branch labels),
|
|
4
|
+
* I/O, and subroutine calls. Persisted to the diagrams tables with
|
|
5
|
+
* `type = 'flowchart'`.
|
|
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 flowchartNode = z.object({
|
|
11
|
+
slug: z.string().min(1),
|
|
12
|
+
name: z.string().min(1),
|
|
13
|
+
kind: z.enum(['start', 'end', 'process', 'decision', 'io', 'subroutine']),
|
|
14
|
+
file: z.string().optional(),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
const flowchartEdge = z.object({
|
|
18
|
+
fromSlug: z.string().min(1),
|
|
19
|
+
toSlug: z.string().min(1),
|
|
20
|
+
kind: z.enum(['flow', 'branch']),
|
|
21
|
+
/** Branch label for edges out of a decision, e.g. "yes" / "no" / "error". */
|
|
22
|
+
label: z.string().optional(),
|
|
23
|
+
sourceFile: z.string().optional(),
|
|
24
|
+
});
|
|
25
|
+
export function runFlowchartPhase(options) {
|
|
26
|
+
return generateDiagram({
|
|
27
|
+
...options,
|
|
28
|
+
workspaceKey: 'flowchart',
|
|
29
|
+
fenceName: 'flowchart',
|
|
30
|
+
nounPlural: 'steps',
|
|
31
|
+
edgeNounPlural: 'arrows',
|
|
32
|
+
mcpConfig: {
|
|
33
|
+
name: 'flowchart',
|
|
34
|
+
toolName: 'flowchart',
|
|
35
|
+
summaryDescribe: '1-3 sentence narrative of the process this flowchart captures.',
|
|
36
|
+
nodesSchema: z.array(flowchartNode),
|
|
37
|
+
nodesDescribe: 'Every step: start / end / process / decision / io / subroutine. slug MUST be unique. Use exactly one `start`.',
|
|
38
|
+
edgesSchema: z.array(flowchartEdge),
|
|
39
|
+
edgesDescribe: 'Arrows. kind = flow (plain) or branch (out of a decision; set label to the branch condition like "yes"/"no"). Endpoints MUST reference emitted steps.',
|
|
40
|
+
},
|
|
41
|
+
buildSystemPrompt: (a) => buildDiagramSystemPrompt('phase/flowchart', 'flowchart', a),
|
|
42
|
+
buildUserPrompt: (a) => buildDiagramUserPrompt({
|
|
43
|
+
...a,
|
|
44
|
+
task: 'Map a flowchart for',
|
|
45
|
+
mcpName: 'flowchart',
|
|
46
|
+
toolName: 'flowchart',
|
|
47
|
+
process: 'Pick ONE important process / function / algorithm (guidance may name it; otherwise choose a central one — a request handler, a core algorithm, a job). Read it top to bottom and translate its control flow into steps: a single `start`, `process` steps for actions, `decision` diamonds for branches (each outgoing edge a `branch` with a "yes"/"no"/condition label), `io` for reads/writes, `subroutine` for significant calls, and `end` node(s) for returns/exits. Follow the happy path plus the important branches.',
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -917,7 +917,7 @@ to keep the user informed during long runs. This is observability only — it
|
|
|
917
917
|
does not affect the extraction.
|
|
918
918
|
|
|
919
919
|
ScreenSchema fields:
|
|
920
|
-
- \`slug\` (unique within the
|
|
920
|
+
- \`slug\` (unique within the diagram), \`name\`, \`route?\`, \`file?\`
|
|
921
921
|
- \`kind\`: one of \`page\`, \`modal\`, \`drawer\`, \`tab\`, \`state\`
|
|
922
922
|
- \`layout\`: one of \`centered\`, \`sidebar\`, \`split\`, \`list-detail\`, \`tabs\`, \`stacked\`
|
|
923
923
|
- \`header?\`: \`{ title, subtitle?, back?, actions?: [{ label, variant?, icon? }] }\`
|
|
@@ -971,7 +971,7 @@ to keep the user informed during long runs. This is observability only — it
|
|
|
971
971
|
does not affect the extraction.
|
|
972
972
|
|
|
973
973
|
DataNodeSchema fields:
|
|
974
|
-
- \`slug\` (unique within the
|
|
974
|
+
- \`slug\` (unique within the diagram), \`name\`, \`kind\`, \`file?\`
|
|
975
975
|
- \`kind\`: one of \`source\`, \`dataset\`, \`transform\`, \`sink\`, \`queue\`, \`model\`
|
|
976
976
|
- \`description?\`: one-sentence summary
|
|
977
977
|
- \`tech?\`: technology / format hint (e.g. \`postgres\`, \`parquet\`, \`kafka\`, \`openai-api\`)
|
|
@@ -1021,5 +1021,181 @@ submit_data_flow({
|
|
|
1021
1021
|
]
|
|
1022
1022
|
})
|
|
1023
1023
|
\`\`\`
|
|
1024
|
+
`,
|
|
1025
|
+
'er-diagram': `
|
|
1026
|
+
**CRITICAL — How to return the result**:
|
|
1027
|
+
|
|
1028
|
+
Return the extraction by calling the MCP tool
|
|
1029
|
+
\`mcp__er-diagram__submit_er_diagram\` **exactly once** with three arguments:
|
|
1030
|
+
|
|
1031
|
+
- \`summary\` — 1-3 sentence narrative of the data model (core entities + relationships)
|
|
1032
|
+
- \`entities\` — array of ErEntity objects (every table / view / enum / junction)
|
|
1033
|
+
- \`relations\` — array of ErRelation objects (foreign-key / inheritance edges)
|
|
1034
|
+
|
|
1035
|
+
The tool validates the arguments against the schema. If it returns an error,
|
|
1036
|
+
fix the issue it describes and call the tool again. After a successful call,
|
|
1037
|
+
end your turn — do not also paste the same data as a fenced text block.
|
|
1038
|
+
|
|
1039
|
+
You can also call \`mcp__er-diagram__record_progress({ phase, message })\` at
|
|
1040
|
+
each phase boundary (detection / enumeration / entities / relations / submission)
|
|
1041
|
+
to keep the user informed during long runs. This is observability only — it
|
|
1042
|
+
does not affect the extraction.
|
|
1043
|
+
|
|
1044
|
+
ErEntity fields:
|
|
1045
|
+
- \`slug\` (unique within the diagram), \`name\`, \`kind\`, \`file?\`
|
|
1046
|
+
- \`kind\`: one of \`entity\`, \`view\`, \`enum\`, \`junction\`
|
|
1047
|
+
- \`description?\`: one-sentence summary
|
|
1048
|
+
- \`columns?\`: array of \`{ name, type?, isPrimaryKey?, isForeignKey?, isNullable?, isUnique?, description?, references? }\`
|
|
1049
|
+
- \`references\`: for FKs, \`target-entity-slug.column\` (e.g. \`users.id\`)
|
|
1050
|
+
- \`stats?\`: array of \`{ label, value }\` (row counts, cardinality hints)
|
|
1051
|
+
|
|
1052
|
+
ErRelation fields:
|
|
1053
|
+
- \`fromSlug\` (child / FK holder), \`toSlug\` (parent / referenced) — both MUST appear in entities
|
|
1054
|
+
- \`kind\`: one of \`one-to-one\`, \`one-to-many\`, \`many-to-many\`, \`inherits\`
|
|
1055
|
+
- \`label?\`: free-form descriptor (e.g. \`placed by\`, \`belongs to\`)
|
|
1056
|
+
- \`sourceColumn?\` / \`targetColumn?\`: the FK column and the referenced column
|
|
1057
|
+
- \`sourceFile?\`: file containing the FK constraint / association
|
|
1058
|
+
|
|
1059
|
+
Direction convention: fromSlug holds the foreign key (child / "many" side),
|
|
1060
|
+
toSlug is the referenced entity (parent / "one" side). \`orders.user_id → users.id\`
|
|
1061
|
+
becomes \`{ fromSlug: "orders", toSlug: "users", kind: "one-to-many" }\`.
|
|
1062
|
+
|
|
1063
|
+
Schematic example of the tool call:
|
|
1064
|
+
|
|
1065
|
+
\`\`\`
|
|
1066
|
+
submit_er_diagram({
|
|
1067
|
+
summary: "Storefront schema: users place orders made of order-items that reference products; roles are linked to users through a junction table.",
|
|
1068
|
+
entities: [
|
|
1069
|
+
{ slug: "users", name: "users", kind: "entity",
|
|
1070
|
+
file: "db/migrations/0001_users.sql",
|
|
1071
|
+
columns: [
|
|
1072
|
+
{ name: "id", type: "uuid", isPrimaryKey: true },
|
|
1073
|
+
{ name: "email", type: "varchar(255)", isUnique: true },
|
|
1074
|
+
{ name: "created_at", type: "timestamptz" }
|
|
1075
|
+
] },
|
|
1076
|
+
{ slug: "orders", name: "orders", kind: "entity",
|
|
1077
|
+
file: "db/migrations/0002_orders.sql",
|
|
1078
|
+
columns: [
|
|
1079
|
+
{ name: "id", type: "uuid", isPrimaryKey: true },
|
|
1080
|
+
{ name: "user_id", type: "uuid", isForeignKey: true, references: "users.id" },
|
|
1081
|
+
{ name: "status", type: "order_status" }
|
|
1082
|
+
] },
|
|
1083
|
+
{ slug: "order-status", name: "order_status", kind: "enum",
|
|
1084
|
+
file: "db/migrations/0002_orders.sql" }
|
|
1085
|
+
],
|
|
1086
|
+
relations: [
|
|
1087
|
+
{ fromSlug: "orders", toSlug: "users", kind: "one-to-many",
|
|
1088
|
+
label: "placed by", sourceColumn: "user_id", targetColumn: "id",
|
|
1089
|
+
sourceFile: "db/migrations/0002_orders.sql" }
|
|
1090
|
+
]
|
|
1091
|
+
})
|
|
1092
|
+
\`\`\`
|
|
1093
|
+
`,
|
|
1094
|
+
'sequence-diagram': `
|
|
1095
|
+
**CRITICAL — How to return the result**:
|
|
1096
|
+
|
|
1097
|
+
Return the extraction by calling the MCP tool
|
|
1098
|
+
\`mcp__sequence-diagram__submit_sequence_diagram\` **exactly once** with three arguments:
|
|
1099
|
+
|
|
1100
|
+
- \`summary\` — 1-3 sentence narrative naming the scenario, the participants, and the outcome
|
|
1101
|
+
- \`participants\` — array of SequenceParticipant objects (every lifeline)
|
|
1102
|
+
- \`messages\` — array of SequenceMessage objects, each with an explicit \`order\`
|
|
1103
|
+
|
|
1104
|
+
The tool validates the arguments against the schema. If it returns an error,
|
|
1105
|
+
fix the issue it describes and call the tool again. After a successful call,
|
|
1106
|
+
end your turn — do not also paste the same data as a fenced text block.
|
|
1107
|
+
|
|
1108
|
+
You can also call \`mcp__sequence-diagram__record_progress({ phase, message })\`
|
|
1109
|
+
at each phase boundary (detection / enumeration / participants / messages /
|
|
1110
|
+
submission) to keep the user informed during long runs. This is observability
|
|
1111
|
+
only — it does not affect the extraction.
|
|
1112
|
+
|
|
1113
|
+
A sequence diagram captures ONE scenario. Map a single end-to-end flow of control.
|
|
1114
|
+
|
|
1115
|
+
SequenceParticipant fields:
|
|
1116
|
+
- \`slug\` (unique within the diagram), \`name\`, \`kind\`, \`file?\`
|
|
1117
|
+
- \`kind\`: one of \`actor\`, \`service\`, \`component\`, \`database\`, \`queue\`, \`external\`
|
|
1118
|
+
- \`description?\`: one-sentence role summary
|
|
1119
|
+
|
|
1120
|
+
SequenceMessage fields:
|
|
1121
|
+
- \`fromSlug\` (sender), \`toSlug\` (receiver) — both MUST appear in participants
|
|
1122
|
+
- \`kind\`: one of \`sync\`, \`async\`, \`return\`, \`self\`
|
|
1123
|
+
- \`order\`: **1-based integer** position in the execution timeline (do not skip or reuse numbers)
|
|
1124
|
+
- \`label?\`: the call / message text (\`POST /login\`, \`validateToken(jwt)\`, \`rows\`)
|
|
1125
|
+
- \`sourceFile?\`: file (+ line) where the call happens
|
|
1126
|
+
|
|
1127
|
+
Order convention: number messages in real execution order; a \`return\` comes
|
|
1128
|
+
after the \`sync\` call it answers. A \`self\` message has \`fromSlug === toSlug\`.
|
|
1129
|
+
|
|
1130
|
+
Schematic example of the tool call:
|
|
1131
|
+
|
|
1132
|
+
\`\`\`
|
|
1133
|
+
submit_sequence_diagram({
|
|
1134
|
+
summary: "Email/password login: the browser posts credentials to AuthService, which verifies them against the users table and issues a JWT.",
|
|
1135
|
+
participants: [
|
|
1136
|
+
{ slug: "user", name: "User", kind: "actor" },
|
|
1137
|
+
{ slug: "auth-service", name: "AuthService", kind: "service",
|
|
1138
|
+
file: "src/routes/auth.ts" },
|
|
1139
|
+
{ slug: "users", name: "users", kind: "database",
|
|
1140
|
+
file: "db/migrations/0001_users.sql" }
|
|
1141
|
+
],
|
|
1142
|
+
messages: [
|
|
1143
|
+
{ fromSlug: "user", toSlug: "auth-service", kind: "sync", order: 1,
|
|
1144
|
+
label: "POST /login {email, password}", sourceFile: "src/routes/auth.ts" },
|
|
1145
|
+
{ fromSlug: "auth-service", toSlug: "users", kind: "sync", order: 2,
|
|
1146
|
+
label: "SELECT … WHERE email = ?", sourceFile: "src/routes/auth.ts" },
|
|
1147
|
+
{ fromSlug: "users", toSlug: "auth-service", kind: "return", order: 3,
|
|
1148
|
+
label: "user row" },
|
|
1149
|
+
{ fromSlug: "auth-service", toSlug: "auth-service", kind: "self", order: 4,
|
|
1150
|
+
label: "verifyPassword()" },
|
|
1151
|
+
{ fromSlug: "auth-service", toSlug: "user", kind: "return", order: 5,
|
|
1152
|
+
label: "200 {token}" }
|
|
1153
|
+
]
|
|
1154
|
+
})
|
|
1155
|
+
\`\`\`
|
|
1156
|
+
`,
|
|
1157
|
+
'state-diagram': `
|
|
1158
|
+
**CRITICAL — How to return the result**:
|
|
1159
|
+
|
|
1160
|
+
Call \`mcp__state-diagram__submit_state_diagram\` **exactly once** with:
|
|
1161
|
+
- \`summary\` — what this state machine models
|
|
1162
|
+
- \`nodes\` — states: \`{ slug, name, kind, file?, description?, onEntry?, onExit?, activity? }\`; \`kind\` ∈ \`initial\` | \`state\` | \`final\` | \`choice\` | \`composite\` (exactly one \`initial\`)
|
|
1163
|
+
- \`edges\` — transitions: \`{ fromSlug, toSlug, kind: "transition", label?, sourceFile? }\`; put \`trigger [guard] / action\` in \`label\`
|
|
1164
|
+
|
|
1165
|
+
Every fromSlug / toSlug MUST reference a node slug. The tool validates; on error,
|
|
1166
|
+
fix and call again. You may call \`mcp__state-diagram__record_progress\` for status.
|
|
1167
|
+
`,
|
|
1168
|
+
'class-diagram': `
|
|
1169
|
+
**CRITICAL — How to return the result**:
|
|
1170
|
+
|
|
1171
|
+
Call \`mcp__class-diagram__submit_class_diagram\` **exactly once** with:
|
|
1172
|
+
- \`summary\` — the core types and how they relate
|
|
1173
|
+
- \`nodes\` — \`{ slug, name, kind, file?, description?, stereotype?, attributes?: [{ name, type?, visibility?, isStatic? }], methods?: [{ name, params?, returnType?, visibility?, isStatic?, isAbstract? }] }\`; \`kind\` ∈ \`class\` | \`interface\` | \`abstract\` | \`enum\`
|
|
1174
|
+
- \`edges\` — \`{ fromSlug, toSlug, kind, label?, sourceFile? }\`; \`kind\` ∈ \`inheritance\` | \`implementation\` | \`composition\` | \`aggregation\` | \`association\` | \`dependency\` (fromSlug = child/owner/dependent)
|
|
1175
|
+
|
|
1176
|
+
Every fromSlug / toSlug MUST reference a node slug. The tool validates; on error,
|
|
1177
|
+
fix and call again. You may call \`mcp__class-diagram__record_progress\` for status.
|
|
1178
|
+
`,
|
|
1179
|
+
'architecture-diagram': `
|
|
1180
|
+
**CRITICAL — How to return the result**:
|
|
1181
|
+
|
|
1182
|
+
Call \`mcp__architecture-diagram__submit_architecture_diagram\` **exactly once** with:
|
|
1183
|
+
- \`summary\` — the architecture and its main building blocks
|
|
1184
|
+
- \`nodes\` — components: \`{ slug, name, kind, file?, description?, tech?, responsibilities?: [string] }\`; \`kind\` ∈ \`module\` | \`service\` | \`package\` | \`ui\` | \`datastore\` | \`external\`
|
|
1185
|
+
- \`edges\` — dependencies: \`{ fromSlug, toSlug, kind, label?, sourceFile? }\`; \`kind\` ∈ \`depends-on\` | \`calls\` | \`imports\` | \`data\` (fromSlug = depender)
|
|
1186
|
+
|
|
1187
|
+
Every fromSlug / toSlug MUST reference a node slug. The tool validates; on error,
|
|
1188
|
+
fix and call again. You may call \`mcp__architecture-diagram__record_progress\` for status.
|
|
1189
|
+
`,
|
|
1190
|
+
flowchart: `
|
|
1191
|
+
**CRITICAL — How to return the result**:
|
|
1192
|
+
|
|
1193
|
+
Call \`mcp__flowchart__submit_flowchart\` **exactly once** with:
|
|
1194
|
+
- \`summary\` — the process this flowchart captures
|
|
1195
|
+
- \`nodes\` — steps: \`{ slug, name, kind, file?, description? }\`; \`kind\` ∈ \`start\` | \`end\` | \`process\` | \`decision\` | \`io\` | \`subroutine\` (exactly one \`start\`)
|
|
1196
|
+
- \`edges\` — \`{ fromSlug, toSlug, kind, label?, sourceFile? }\`; \`kind\` ∈ \`flow\` | \`branch\` (branch edges out of a \`decision\` set \`label\` to "yes"/"no"/condition)
|
|
1197
|
+
|
|
1198
|
+
Every fromSlug / toSlug MUST reference a node slug. The tool validates; on error,
|
|
1199
|
+
fix and call again. You may call \`mcp__flowchart__record_progress\` for status.
|
|
1024
1200
|
`,
|
|
1025
1201
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* screen-flow phase: clone the product's repo, ask Claude to map every
|
|
3
3
|
* user-facing screen and the transitions between them into a structured
|
|
4
|
-
* ScreenFlowExtraction, then persist the result to
|
|
5
|
-
*
|
|
4
|
+
* ScreenFlowExtraction, then persist the result to diagrams / diagram_nodes /
|
|
5
|
+
* diagram_edges (rows tagged `type = 'screen'`) via the Supabase SDK.
|
|
6
6
|
*
|
|
7
7
|
* Companion to find-architecture / find-bugs / find-features. Same workspace
|
|
8
8
|
* pattern, but writes to its own tables rather than filing issues.
|
|
@@ -12,7 +12,7 @@ export interface ScreenFlowOptions {
|
|
|
12
12
|
productId?: string;
|
|
13
13
|
/** Repo-only flow: a single repositories row, no product context. */
|
|
14
14
|
repoId?: string;
|
|
15
|
-
|
|
15
|
+
diagramId: string;
|
|
16
16
|
guidance?: string;
|
|
17
17
|
verbose?: boolean;
|
|
18
18
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* screen-flow phase: clone the product's repo, ask Claude to map every
|
|
3
3
|
* user-facing screen and the transitions between them into a structured
|
|
4
|
-
* ScreenFlowExtraction, then persist the result to
|
|
5
|
-
*
|
|
4
|
+
* ScreenFlowExtraction, then persist the result to diagrams / diagram_nodes /
|
|
5
|
+
* diagram_edges (rows tagged `type = 'screen'`) via the Supabase SDK.
|
|
6
6
|
*
|
|
7
7
|
* Companion to find-architecture / find-bugs / find-features. Same workspace
|
|
8
8
|
* pattern, but writes to its own tables rather than filing issues.
|
|
@@ -14,7 +14,7 @@ import { getSupabase } from '../../supabase/client.js';
|
|
|
14
14
|
import { logError, logInfo, logSuccess, logWarning } from '../../utils/logger.js';
|
|
15
15
|
import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
|
|
16
16
|
import { fetchProductBasics } from '../find-shared/mcp.js';
|
|
17
|
-
import {
|
|
17
|
+
import { cloneDiagramRepos, describeRepoScope } from '../diagram-shared/clone-repos.js';
|
|
18
18
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
19
19
|
import { createScreenFlowCaptureState, createScreenFlowMcpServer, validateConsistency, } from './mcp-server.js';
|
|
20
20
|
import { createScreenFlowSystemPrompt, createScreenFlowUserPrompt, } from './prompts.js';
|
|
@@ -27,7 +27,7 @@ const COLUMN_WIDTH = 380;
|
|
|
27
27
|
const ROW_HEIGHT = 480;
|
|
28
28
|
const COLUMNS = 4;
|
|
29
29
|
export async function runScreenFlowPhase(options) {
|
|
30
|
-
const { productId, repoId,
|
|
30
|
+
const { productId, repoId, diagramId, guidance, verbose } = options;
|
|
31
31
|
const repoOnly = !productId && Boolean(repoId);
|
|
32
32
|
if (productId) {
|
|
33
33
|
logInfo(`Starting screen-flow generation for product ${productId}`);
|
|
@@ -36,9 +36,9 @@ export async function runScreenFlowPhase(options) {
|
|
|
36
36
|
logInfo(`Starting screen-flow generation for repository ${repoId}`);
|
|
37
37
|
}
|
|
38
38
|
const supabase = getSupabase();
|
|
39
|
-
await
|
|
40
|
-
const repositoryIds = await
|
|
41
|
-
const cloneResult = await
|
|
39
|
+
await markDiagramRunning(supabase, diagramId);
|
|
40
|
+
const repositoryIds = await getDiagramRepositoryIds(supabase, diagramId);
|
|
41
|
+
const cloneResult = await cloneDiagramRepos({
|
|
42
42
|
productId,
|
|
43
43
|
repoId,
|
|
44
44
|
repositoryIds,
|
|
@@ -46,7 +46,7 @@ export async function runScreenFlowPhase(options) {
|
|
|
46
46
|
verbose,
|
|
47
47
|
});
|
|
48
48
|
if (!cloneResult.ok) {
|
|
49
|
-
await
|
|
49
|
+
await markDiagramFailed(supabase, diagramId, cloneResult.message);
|
|
50
50
|
return { status: 'error', message: cloneResult.message };
|
|
51
51
|
}
|
|
52
52
|
const { projectDir, cleanupDir, repos } = cloneResult;
|
|
@@ -104,17 +104,17 @@ export async function runScreenFlowPhase(options) {
|
|
|
104
104
|
}
|
|
105
105
|
if (!extraction) {
|
|
106
106
|
const msg = 'Screen flow extraction failed: agent did not call submit_screen_flow and no parseable screen_flow block was found in the response';
|
|
107
|
-
await
|
|
107
|
+
await markDiagramFailed(supabase, diagramId, msg);
|
|
108
108
|
return { status: 'error', message: msg };
|
|
109
109
|
}
|
|
110
110
|
logInfo(`Extraction produced ${extraction.nodes.length} screens / ${extraction.edges.length} transitions`);
|
|
111
111
|
const theme = resolveTheme(extraction.theme, repos);
|
|
112
112
|
if (Object.keys(theme).length > 0) {
|
|
113
113
|
logInfo(`Theme: ${Object.entries(theme).map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
114
|
-
await persistTheme(supabase,
|
|
114
|
+
await persistTheme(supabase, diagramId, theme);
|
|
115
115
|
}
|
|
116
|
-
const { nodesCreated, edgesCreated } = await
|
|
117
|
-
await
|
|
116
|
+
const { nodesCreated, edgesCreated } = await persistDiagram(supabase, diagramId, extraction);
|
|
117
|
+
await markDiagramSuccess(supabase, diagramId, extraction.summary);
|
|
118
118
|
succeeded = true;
|
|
119
119
|
logSuccess(`Screen flow generated: ${nodesCreated} screens, ${edgesCreated} transitions`);
|
|
120
120
|
return {
|
|
@@ -128,7 +128,7 @@ export async function runScreenFlowPhase(options) {
|
|
|
128
128
|
catch (error) {
|
|
129
129
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
130
130
|
logError(`Screen flow failed: ${errorMessage}`);
|
|
131
|
-
await
|
|
131
|
+
await markDiagramFailed(supabase, diagramId, errorMessage);
|
|
132
132
|
return { status: 'error', message: errorMessage };
|
|
133
133
|
}
|
|
134
134
|
finally {
|
|
@@ -169,12 +169,12 @@ async function resolveRepoBasics(repositoryId, repos) {
|
|
|
169
169
|
description: basics?.description ?? undefined,
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
|
-
/** Read the ordered repo set a
|
|
173
|
-
async function
|
|
172
|
+
/** Read the ordered repo set a diagram was scoped to (may be empty). */
|
|
173
|
+
async function getDiagramRepositoryIds(supabase, diagramId) {
|
|
174
174
|
const { data } = await supabase
|
|
175
|
-
.from('
|
|
175
|
+
.from('diagrams')
|
|
176
176
|
.select('repository_ids')
|
|
177
|
-
.eq('id',
|
|
177
|
+
.eq('id', diagramId)
|
|
178
178
|
.single();
|
|
179
179
|
return (data?.repository_ids ?? []).filter(Boolean);
|
|
180
180
|
}
|
|
@@ -234,31 +234,31 @@ function tryFallbackParse(resultMessage, assistantText) {
|
|
|
234
234
|
// ============================================================================
|
|
235
235
|
// Persistence
|
|
236
236
|
// ============================================================================
|
|
237
|
-
async function
|
|
237
|
+
async function markDiagramRunning(supabase, diagramId) {
|
|
238
238
|
const { error } = await supabase
|
|
239
|
-
.from('
|
|
239
|
+
.from('diagrams')
|
|
240
240
|
.update({ status: 'running', error: null })
|
|
241
|
-
.eq('id',
|
|
241
|
+
.eq('id', diagramId);
|
|
242
242
|
if (error) {
|
|
243
|
-
logWarning(`Could not mark
|
|
243
|
+
logWarning(`Could not mark diagram as running: ${error.message}`);
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
async function
|
|
246
|
+
async function markDiagramFailed(supabase, diagramId, errorMessage) {
|
|
247
247
|
await supabase
|
|
248
|
-
.from('
|
|
248
|
+
.from('diagrams')
|
|
249
249
|
.update({
|
|
250
250
|
status: 'failed',
|
|
251
251
|
error: errorMessage,
|
|
252
252
|
completed_at: new Date().toISOString(),
|
|
253
253
|
})
|
|
254
|
-
.eq('id',
|
|
254
|
+
.eq('id', diagramId);
|
|
255
255
|
}
|
|
256
|
-
async function persistTheme(supabase,
|
|
256
|
+
async function persistTheme(supabase, diagramId, theme) {
|
|
257
257
|
// Theme is screen-flow-specific; stash it inside the generic options JSONB.
|
|
258
258
|
const { data, error: readError } = await supabase
|
|
259
|
-
.from('
|
|
259
|
+
.from('diagrams')
|
|
260
260
|
.select('options')
|
|
261
|
-
.eq('id',
|
|
261
|
+
.eq('id', diagramId)
|
|
262
262
|
.single();
|
|
263
263
|
if (readError) {
|
|
264
264
|
logWarning(`Could not read flow options: ${readError.message}`);
|
|
@@ -269,34 +269,34 @@ async function persistTheme(supabase, flowId, theme) {
|
|
|
269
269
|
theme,
|
|
270
270
|
};
|
|
271
271
|
const { error } = await supabase
|
|
272
|
-
.from('
|
|
272
|
+
.from('diagrams')
|
|
273
273
|
.update({ options: nextOptions })
|
|
274
|
-
.eq('id',
|
|
274
|
+
.eq('id', diagramId);
|
|
275
275
|
if (error) {
|
|
276
276
|
logWarning(`Could not persist extracted theme: ${error.message}`);
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
|
-
async function
|
|
279
|
+
async function markDiagramSuccess(supabase, diagramId, summary) {
|
|
280
280
|
await supabase
|
|
281
|
-
.from('
|
|
281
|
+
.from('diagrams')
|
|
282
282
|
.update({
|
|
283
283
|
status: 'success',
|
|
284
284
|
summary,
|
|
285
285
|
error: null,
|
|
286
286
|
completed_at: new Date().toISOString(),
|
|
287
287
|
})
|
|
288
|
-
.eq('id',
|
|
288
|
+
.eq('id', diagramId);
|
|
289
289
|
}
|
|
290
|
-
async function
|
|
290
|
+
async function persistDiagram(supabase, diagramId, extraction) {
|
|
291
291
|
// Re-runs replace prior content for the same flow row.
|
|
292
|
-
await supabase.from('
|
|
293
|
-
await supabase.from('
|
|
292
|
+
await supabase.from('diagram_edges').delete().eq('diagram_id', diagramId);
|
|
293
|
+
await supabase.from('diagram_nodes').delete().eq('diagram_id', diagramId);
|
|
294
294
|
if (extraction.nodes.length === 0) {
|
|
295
295
|
return { nodesCreated: 0, edgesCreated: 0 };
|
|
296
296
|
}
|
|
297
|
-
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(
|
|
297
|
+
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(diagramId, n, i));
|
|
298
298
|
const { data: insertedNodes, error: nodesError } = await supabase
|
|
299
|
-
.from('
|
|
299
|
+
.from('diagram_nodes')
|
|
300
300
|
.insert(nodeRows)
|
|
301
301
|
.select('id, slug');
|
|
302
302
|
if (nodesError) {
|
|
@@ -304,11 +304,11 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
304
304
|
}
|
|
305
305
|
const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
|
|
306
306
|
const edgeRows = extraction.edges
|
|
307
|
-
.map((e) => buildEdgeRow(
|
|
307
|
+
.map((e) => buildEdgeRow(diagramId, e, slugToId))
|
|
308
308
|
.filter((e) => e !== null);
|
|
309
309
|
if (edgeRows.length > 0) {
|
|
310
310
|
const { error: edgesError } = await supabase
|
|
311
|
-
.from('
|
|
311
|
+
.from('diagram_edges')
|
|
312
312
|
.insert(edgeRows);
|
|
313
313
|
if (edgesError) {
|
|
314
314
|
throw new Error(`Failed to insert edges: ${edgesError.message}`);
|
|
@@ -319,9 +319,9 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
319
319
|
edgesCreated: edgeRows.length,
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
|
-
function buildNodeRow(
|
|
322
|
+
function buildNodeRow(diagramId, node, index) {
|
|
323
323
|
return {
|
|
324
|
-
|
|
324
|
+
diagram_id: diagramId,
|
|
325
325
|
slug: node.slug,
|
|
326
326
|
name: node.name,
|
|
327
327
|
kind: node.kind,
|
|
@@ -330,14 +330,14 @@ function buildNodeRow(flowId, node, index) {
|
|
|
330
330
|
position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
|
-
function buildEdgeRow(
|
|
333
|
+
function buildEdgeRow(diagramId, edge, slugToId) {
|
|
334
334
|
const fromId = slugToId.get(edge.fromSlug);
|
|
335
335
|
const toId = slugToId.get(edge.toSlug);
|
|
336
336
|
if (!fromId || !toId) {
|
|
337
337
|
return null;
|
|
338
338
|
}
|
|
339
339
|
return {
|
|
340
|
-
|
|
340
|
+
diagram_id: diagramId,
|
|
341
341
|
from_node_id: fromId,
|
|
342
342
|
to_node_id: toId,
|
|
343
343
|
label: edge.triggerLabel,
|
|
@@ -166,7 +166,7 @@ export function validateConsistency(extraction) {
|
|
|
166
166
|
for (const node of extraction.nodes) {
|
|
167
167
|
if (slugs.has(node.slug)) {
|
|
168
168
|
return {
|
|
169
|
-
error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the
|
|
169
|
+
error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the diagram. Re-call submit_screen_flow with deduplicated nodes.`,
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
slugs.add(node.slug);
|
|
@@ -208,7 +208,7 @@ export function createSubmitScreenFlowTool(state) {
|
|
|
208
208
|
.describe('1-3 sentence narrative of what kind of app this is and its primary user flows.'),
|
|
209
209
|
nodes: z
|
|
210
210
|
.array(screenNodeSchema)
|
|
211
|
-
.describe('Every user-facing screen, modal, drawer, tab, or named state. node.slug MUST be unique within the
|
|
211
|
+
.describe('Every user-facing screen, modal, drawer, tab, or named state. node.slug MUST be unique within the diagram.'),
|
|
212
212
|
edges: z
|
|
213
213
|
.array(screenEdgeSchema)
|
|
214
214
|
.describe('Transitions between screens. Every fromSlug / toSlug MUST reference a slug present in nodes; drop edges whose endpoints you did not emit.'),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequence-diagram phase: clone the product's repo, ask Claude to trace one
|
|
3
|
+
* scenario through the code and map the participants (actor / service /
|
|
4
|
+
* component / database / queue / external) and the ordered messages between
|
|
5
|
+
* them into a structured SequenceDiagramExtraction, then persist the result
|
|
6
|
+
* to diagrams / diagram_nodes / diagram_edges (rows tagged `type = 'sequence'`) via the
|
|
7
|
+
* Supabase SDK.
|
|
8
|
+
*
|
|
9
|
+
* Companion to data-flow / screen-flow / er-diagram: same generation pattern
|
|
10
|
+
* (workspace clone + Claude Agent SDK + in-process MCP server), same storage
|
|
11
|
+
* tables, different domain. Message ordering is persisted in the edge's
|
|
12
|
+
* `metadata.order`.
|
|
13
|
+
*/
|
|
14
|
+
export interface SequenceDiagramPhaseOptions {
|
|
15
|
+
/** Product-scoped diagram. Mutually exclusive with `repoId`. */
|
|
16
|
+
productId?: string;
|
|
17
|
+
/** Repo-only diagram: a single repositories row, no product context. */
|
|
18
|
+
repoId?: string;
|
|
19
|
+
diagramId: string;
|
|
20
|
+
guidance?: string;
|
|
21
|
+
verbose?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SequenceDiagramPhaseResult {
|
|
24
|
+
status: 'success' | 'error';
|
|
25
|
+
message: string;
|
|
26
|
+
nodesCreated?: number;
|
|
27
|
+
edgesCreated?: number;
|
|
28
|
+
summary?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function runSequenceDiagramPhase(options: SequenceDiagramPhaseOptions): Promise<SequenceDiagramPhaseResult>;
|