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.
- 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/discover/index.d.ts +14 -0
- package/dist/commands/discover/index.js +29 -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 +139 -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 +37 -37
- 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/discover-services/index.d.ts +29 -0
- package/dist/phases/discover-services/index.js +528 -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 +47 -45
- 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.
|
|
@@ -11,10 +11,10 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
|
11
11
|
import { getRepositoryBasics } from '../../api/github.js';
|
|
12
12
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
13
13
|
import { getSupabase } from '../../supabase/client.js';
|
|
14
|
-
import { logError, logInfo, logSuccess, logWarning } from '../../utils/logger.js';
|
|
14
|
+
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
15
15
|
import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
|
|
16
|
+
import { cloneDiagramRepos, describeRepoScope, } from '../diagram-shared/clone-repos.js';
|
|
16
17
|
import { fetchProductBasics } from '../find-shared/mcp.js';
|
|
17
|
-
import { cloneFlowRepos, describeRepoScope } from '../flow-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,19 @@ 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
|
-
logInfo(`Theme: ${Object.entries(theme)
|
|
114
|
-
|
|
113
|
+
logInfo(`Theme: ${Object.entries(theme)
|
|
114
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
115
|
+
.join(', ')}`);
|
|
116
|
+
await persistTheme(supabase, diagramId, theme);
|
|
115
117
|
}
|
|
116
|
-
const { nodesCreated, edgesCreated } = await
|
|
117
|
-
await
|
|
118
|
+
const { nodesCreated, edgesCreated } = await persistDiagram(supabase, diagramId, extraction);
|
|
119
|
+
await markDiagramSuccess(supabase, diagramId, extraction.summary);
|
|
118
120
|
succeeded = true;
|
|
119
121
|
logSuccess(`Screen flow generated: ${nodesCreated} screens, ${edgesCreated} transitions`);
|
|
120
122
|
return {
|
|
@@ -128,7 +130,7 @@ export async function runScreenFlowPhase(options) {
|
|
|
128
130
|
catch (error) {
|
|
129
131
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
130
132
|
logError(`Screen flow failed: ${errorMessage}`);
|
|
131
|
-
await
|
|
133
|
+
await markDiagramFailed(supabase, diagramId, errorMessage);
|
|
132
134
|
return { status: 'error', message: errorMessage };
|
|
133
135
|
}
|
|
134
136
|
finally {
|
|
@@ -169,12 +171,12 @@ async function resolveRepoBasics(repositoryId, repos) {
|
|
|
169
171
|
description: basics?.description ?? undefined,
|
|
170
172
|
};
|
|
171
173
|
}
|
|
172
|
-
/** Read the ordered repo set a
|
|
173
|
-
async function
|
|
174
|
+
/** Read the ordered repo set a diagram was scoped to (may be empty). */
|
|
175
|
+
async function getDiagramRepositoryIds(supabase, diagramId) {
|
|
174
176
|
const { data } = await supabase
|
|
175
|
-
.from('
|
|
177
|
+
.from('diagrams')
|
|
176
178
|
.select('repository_ids')
|
|
177
|
-
.eq('id',
|
|
179
|
+
.eq('id', diagramId)
|
|
178
180
|
.single();
|
|
179
181
|
return (data?.repository_ids ?? []).filter(Boolean);
|
|
180
182
|
}
|
|
@@ -234,31 +236,31 @@ function tryFallbackParse(resultMessage, assistantText) {
|
|
|
234
236
|
// ============================================================================
|
|
235
237
|
// Persistence
|
|
236
238
|
// ============================================================================
|
|
237
|
-
async function
|
|
239
|
+
async function markDiagramRunning(supabase, diagramId) {
|
|
238
240
|
const { error } = await supabase
|
|
239
|
-
.from('
|
|
241
|
+
.from('diagrams')
|
|
240
242
|
.update({ status: 'running', error: null })
|
|
241
|
-
.eq('id',
|
|
243
|
+
.eq('id', diagramId);
|
|
242
244
|
if (error) {
|
|
243
|
-
logWarning(`Could not mark
|
|
245
|
+
logWarning(`Could not mark diagram as running: ${error.message}`);
|
|
244
246
|
}
|
|
245
247
|
}
|
|
246
|
-
async function
|
|
248
|
+
async function markDiagramFailed(supabase, diagramId, errorMessage) {
|
|
247
249
|
await supabase
|
|
248
|
-
.from('
|
|
250
|
+
.from('diagrams')
|
|
249
251
|
.update({
|
|
250
252
|
status: 'failed',
|
|
251
253
|
error: errorMessage,
|
|
252
254
|
completed_at: new Date().toISOString(),
|
|
253
255
|
})
|
|
254
|
-
.eq('id',
|
|
256
|
+
.eq('id', diagramId);
|
|
255
257
|
}
|
|
256
|
-
async function persistTheme(supabase,
|
|
258
|
+
async function persistTheme(supabase, diagramId, theme) {
|
|
257
259
|
// Theme is screen-flow-specific; stash it inside the generic options JSONB.
|
|
258
260
|
const { data, error: readError } = await supabase
|
|
259
|
-
.from('
|
|
261
|
+
.from('diagrams')
|
|
260
262
|
.select('options')
|
|
261
|
-
.eq('id',
|
|
263
|
+
.eq('id', diagramId)
|
|
262
264
|
.single();
|
|
263
265
|
if (readError) {
|
|
264
266
|
logWarning(`Could not read flow options: ${readError.message}`);
|
|
@@ -269,34 +271,34 @@ async function persistTheme(supabase, flowId, theme) {
|
|
|
269
271
|
theme,
|
|
270
272
|
};
|
|
271
273
|
const { error } = await supabase
|
|
272
|
-
.from('
|
|
274
|
+
.from('diagrams')
|
|
273
275
|
.update({ options: nextOptions })
|
|
274
|
-
.eq('id',
|
|
276
|
+
.eq('id', diagramId);
|
|
275
277
|
if (error) {
|
|
276
278
|
logWarning(`Could not persist extracted theme: ${error.message}`);
|
|
277
279
|
}
|
|
278
280
|
}
|
|
279
|
-
async function
|
|
281
|
+
async function markDiagramSuccess(supabase, diagramId, summary) {
|
|
280
282
|
await supabase
|
|
281
|
-
.from('
|
|
283
|
+
.from('diagrams')
|
|
282
284
|
.update({
|
|
283
285
|
status: 'success',
|
|
284
286
|
summary,
|
|
285
287
|
error: null,
|
|
286
288
|
completed_at: new Date().toISOString(),
|
|
287
289
|
})
|
|
288
|
-
.eq('id',
|
|
290
|
+
.eq('id', diagramId);
|
|
289
291
|
}
|
|
290
|
-
async function
|
|
292
|
+
async function persistDiagram(supabase, diagramId, extraction) {
|
|
291
293
|
// Re-runs replace prior content for the same flow row.
|
|
292
|
-
await supabase.from('
|
|
293
|
-
await supabase.from('
|
|
294
|
+
await supabase.from('diagram_edges').delete().eq('diagram_id', diagramId);
|
|
295
|
+
await supabase.from('diagram_nodes').delete().eq('diagram_id', diagramId);
|
|
294
296
|
if (extraction.nodes.length === 0) {
|
|
295
297
|
return { nodesCreated: 0, edgesCreated: 0 };
|
|
296
298
|
}
|
|
297
|
-
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(
|
|
299
|
+
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(diagramId, n, i));
|
|
298
300
|
const { data: insertedNodes, error: nodesError } = await supabase
|
|
299
|
-
.from('
|
|
301
|
+
.from('diagram_nodes')
|
|
300
302
|
.insert(nodeRows)
|
|
301
303
|
.select('id, slug');
|
|
302
304
|
if (nodesError) {
|
|
@@ -304,11 +306,11 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
304
306
|
}
|
|
305
307
|
const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
|
|
306
308
|
const edgeRows = extraction.edges
|
|
307
|
-
.map((e) => buildEdgeRow(
|
|
309
|
+
.map((e) => buildEdgeRow(diagramId, e, slugToId))
|
|
308
310
|
.filter((e) => e !== null);
|
|
309
311
|
if (edgeRows.length > 0) {
|
|
310
312
|
const { error: edgesError } = await supabase
|
|
311
|
-
.from('
|
|
313
|
+
.from('diagram_edges')
|
|
312
314
|
.insert(edgeRows);
|
|
313
315
|
if (edgesError) {
|
|
314
316
|
throw new Error(`Failed to insert edges: ${edgesError.message}`);
|
|
@@ -319,9 +321,9 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
319
321
|
edgesCreated: edgeRows.length,
|
|
320
322
|
};
|
|
321
323
|
}
|
|
322
|
-
function buildNodeRow(
|
|
324
|
+
function buildNodeRow(diagramId, node, index) {
|
|
323
325
|
return {
|
|
324
|
-
|
|
326
|
+
diagram_id: diagramId,
|
|
325
327
|
slug: node.slug,
|
|
326
328
|
name: node.name,
|
|
327
329
|
kind: node.kind,
|
|
@@ -330,14 +332,14 @@ function buildNodeRow(flowId, node, index) {
|
|
|
330
332
|
position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
|
|
331
333
|
};
|
|
332
334
|
}
|
|
333
|
-
function buildEdgeRow(
|
|
335
|
+
function buildEdgeRow(diagramId, edge, slugToId) {
|
|
334
336
|
const fromId = slugToId.get(edge.fromSlug);
|
|
335
337
|
const toId = slugToId.get(edge.toSlug);
|
|
336
338
|
if (!fromId || !toId) {
|
|
337
339
|
return null;
|
|
338
340
|
}
|
|
339
341
|
return {
|
|
340
|
-
|
|
342
|
+
diagram_id: diagramId,
|
|
341
343
|
from_node_id: fromId,
|
|
342
344
|
to_node_id: toId,
|
|
343
345
|
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>;
|