edsger 0.68.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 +23 -0
- package/dist/api/github.js +61 -0
- 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 +8 -6
- package/dist/commands/data-flow/index.js +21 -12
- 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/quality-benchmark/index.js +43 -9
- package/dist/commands/recipes/index.d.ts +3 -1
- package/dist/commands/recipes/index.js +10 -4
- package/dist/commands/screen-flow/index.d.ts +8 -6
- package/dist/commands/screen-flow/index.js +21 -12
- 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 +144 -14
- 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 +6 -3
- package/dist/phases/data-flow/index.js +59 -38
- 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/flow-shared/clone-repos.d.ts +8 -2
- package/dist/phases/flow-shared/clone-repos.js +36 -18
- 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/recipes/index.d.ts +8 -1
- package/dist/phases/recipes/index.js +74 -17
- package/dist/phases/recipes/mcp-server.d.ts +4 -1
- package/dist/phases/recipes/mcp-server.js +43 -18
- package/dist/phases/screen-flow/index.d.ts +7 -4
- package/dist/phases/screen-flow/index.js +66 -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
|
+
}
|
|
@@ -35,8 +35,11 @@ export declare function safeDirName(fullName: string): string;
|
|
|
35
35
|
/**
|
|
36
36
|
* Resolve the repositories a flow targets (by id, preserving the stored
|
|
37
37
|
* order), falling back to the product's primary repo.
|
|
38
|
+
*
|
|
39
|
+
* In repo-only mode there is no product, so no `fallback` is provided: the
|
|
40
|
+
* set is resolved purely from `repositoryIds`.
|
|
38
41
|
*/
|
|
39
|
-
export declare function resolveTargetRepos(productId: string, repositoryIds: string[], fallback
|
|
42
|
+
export declare function resolveTargetRepos(productId: string | undefined, repositoryIds: string[], fallback?: {
|
|
40
43
|
owner: string;
|
|
41
44
|
repo: string;
|
|
42
45
|
}): Promise<{
|
|
@@ -45,7 +48,10 @@ export declare function resolveTargetRepos(productId: string, repositoryIds: str
|
|
|
45
48
|
repo: string;
|
|
46
49
|
}[]>;
|
|
47
50
|
export declare function cloneFlowRepos(opts: {
|
|
48
|
-
|
|
51
|
+
/** Product-scoped flow. Mutually exclusive with `repoId`. */
|
|
52
|
+
productId?: string;
|
|
53
|
+
/** Repo-only flow: a single repositories row, no product context. */
|
|
54
|
+
repoId?: string;
|
|
49
55
|
repositoryIds: string[];
|
|
50
56
|
workspaceKey: string;
|
|
51
57
|
verbose?: boolean;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Falls back to the product's primary repo when `repository_ids` is empty
|
|
14
14
|
* (older flows, or single-repo products).
|
|
15
15
|
*/
|
|
16
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
16
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
17
17
|
import { getSupabase } from '../../supabase/client.js';
|
|
18
18
|
import { logInfo, logWarning } from '../../utils/logger.js';
|
|
19
19
|
import { cloneIssueRepo, ensureWorkspaceDir, getIssueRepoPath, } from '../../workspace/workspace-manager.js';
|
|
@@ -23,16 +23,22 @@ export function safeDirName(fullName) {
|
|
|
23
23
|
/**
|
|
24
24
|
* Resolve the repositories a flow targets (by id, preserving the stored
|
|
25
25
|
* order), falling back to the product's primary repo.
|
|
26
|
+
*
|
|
27
|
+
* In repo-only mode there is no product, so no `fallback` is provided: the
|
|
28
|
+
* set is resolved purely from `repositoryIds`.
|
|
26
29
|
*/
|
|
27
30
|
export async function resolveTargetRepos(productId, repositoryIds, fallback) {
|
|
28
31
|
if (repositoryIds.length === 0) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
if (fallback) {
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
fullName: `${fallback.owner}/${fallback.repo}`,
|
|
36
|
+
owner: fallback.owner,
|
|
37
|
+
repo: fallback.repo,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
36
42
|
}
|
|
37
43
|
const supabase = getSupabase();
|
|
38
44
|
const { data } = await supabase
|
|
@@ -54,8 +60,8 @@ export async function resolveTargetRepos(productId, repositoryIds, fallback) {
|
|
|
54
60
|
resolved.push({ fullName, owner, repo });
|
|
55
61
|
}
|
|
56
62
|
// If none resolved (deleted repos / RLS), fall back to the primary repo so
|
|
57
|
-
// generation still produces something useful.
|
|
58
|
-
if (resolved.length === 0) {
|
|
63
|
+
// generation still produces something useful (product mode only).
|
|
64
|
+
if (resolved.length === 0 && fallback) {
|
|
59
65
|
return [
|
|
60
66
|
{
|
|
61
67
|
fullName: `${fallback.owner}/${fallback.repo}`,
|
|
@@ -67,21 +73,33 @@ export async function resolveTargetRepos(productId, repositoryIds, fallback) {
|
|
|
67
73
|
return resolved;
|
|
68
74
|
}
|
|
69
75
|
export async function cloneFlowRepos(opts) {
|
|
70
|
-
const { productId, repositoryIds, workspaceKey, verbose } = opts;
|
|
71
|
-
const
|
|
76
|
+
const { productId, repoId, repositoryIds, workspaceKey, verbose } = opts;
|
|
77
|
+
const repoOnly = !productId && Boolean(repoId);
|
|
78
|
+
// Resolve the auth token. Product mode reuses the product's installation /
|
|
79
|
+
// PAT for every repo; repo-only mode resolves it from the first (only) repo.
|
|
80
|
+
const gh = repoOnly
|
|
81
|
+
? await getGitHubConfigByRepository(repositoryIds[0] ?? repoId, verbose)
|
|
82
|
+
: await getGitHubConfigByProduct(productId, verbose);
|
|
72
83
|
if (!gh.configured || !gh.token || !gh.owner || !gh.repo) {
|
|
73
84
|
return {
|
|
74
85
|
ok: false,
|
|
75
86
|
message: gh.message ||
|
|
76
|
-
|
|
87
|
+
(repoOnly
|
|
88
|
+
? 'GitHub repository not configured. Connect the repo first.'
|
|
89
|
+
: 'GitHub repository not configured for this product. Connect a repo first.'),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// In repo-only mode there is no product primary-repo fallback; targets come
|
|
93
|
+
// purely from repositoryIds.
|
|
94
|
+
const targets = await resolveTargetRepos(productId, repositoryIds, repoOnly ? undefined : { owner: gh.owner, repo: gh.repo });
|
|
95
|
+
if (targets.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
message: 'No repositories resolved for this flow.',
|
|
77
99
|
};
|
|
78
100
|
}
|
|
79
|
-
const targets = await resolveTargetRepos(productId, repositoryIds, {
|
|
80
|
-
owner: gh.owner,
|
|
81
|
-
repo: gh.repo,
|
|
82
|
-
});
|
|
83
101
|
const workspaceRoot = ensureWorkspaceDir();
|
|
84
|
-
const parentDir = getIssueRepoPath(workspaceRoot, `${workspaceKey}-${productId}`);
|
|
102
|
+
const parentDir = getIssueRepoPath(workspaceRoot, `${workspaceKey}-${repoOnly ? `repo-${repoId}` : productId}`);
|
|
85
103
|
const repos = [];
|
|
86
104
|
for (const target of targets) {
|
|
87
105
|
try {
|
|
@@ -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
|
};
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
20
20
|
import type { RecipeSummary } from './types.js';
|
|
21
21
|
export interface RecipesPhaseOptions {
|
|
22
|
-
|
|
22
|
+
/** Product-scoped scan. Mutually exclusive with `repoId`. */
|
|
23
|
+
productId?: string;
|
|
24
|
+
/** Repo-only scan: a single repositories row, no product context. */
|
|
25
|
+
repoId?: string;
|
|
23
26
|
scanId: string;
|
|
24
27
|
guidance?: string;
|
|
25
28
|
verbose?: boolean;
|
|
@@ -44,6 +47,10 @@ export declare function listProductRecipeLinks(supabase: SupabaseClient, product
|
|
|
44
47
|
recipe_id: string;
|
|
45
48
|
name: string;
|
|
46
49
|
}[]>;
|
|
50
|
+
export declare function listRepositoryRecipeLinks(supabase: SupabaseClient, repositoryId: string): Promise<{
|
|
51
|
+
recipe_id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
}[]>;
|
|
47
54
|
/**
|
|
48
55
|
* Claim the row by flipping `pending` → `running`. Returns true on success
|
|
49
56
|
* (we won the claim) and false when the row has already moved on (e.g. user
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* progress survives even if the agent later errors out.
|
|
18
18
|
*/
|
|
19
19
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
20
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
20
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, getRepositoryBasics, } from '../../api/github.js';
|
|
21
21
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
22
22
|
import { getSupabase } from '../../supabase/client.js';
|
|
23
23
|
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
@@ -33,8 +33,14 @@ const MAX_TURNS = 200;
|
|
|
33
33
|
// flowing) lets the row go stale and the reader can mark it failed.
|
|
34
34
|
const HEARTBEAT_MIN_INTERVAL_MS = 15_000;
|
|
35
35
|
export async function runRecipesPhase(options) {
|
|
36
|
-
const { productId, scanId, guidance, verbose } = options;
|
|
37
|
-
|
|
36
|
+
const { productId, repoId, scanId, guidance, verbose } = options;
|
|
37
|
+
const repoOnly = !productId && Boolean(repoId);
|
|
38
|
+
if (productId) {
|
|
39
|
+
logInfo(`Starting recipes scan for product ${productId}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
logInfo(`Starting recipes scan for repository ${repoId}`);
|
|
43
|
+
}
|
|
38
44
|
const supabase = getSupabase();
|
|
39
45
|
const claimed = await markRunning(supabase, scanId);
|
|
40
46
|
if (!claimed) {
|
|
@@ -43,19 +49,42 @@ export async function runRecipesPhase(options) {
|
|
|
43
49
|
message: 'Recipe scan row is no longer in a runnable state (likely cancelled before the CLI started)',
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
// Resolve the team (recipes are team-scoped) + repo basics. In repo-only
|
|
53
|
+
// mode both come off the repositories row; in product mode from the product.
|
|
54
|
+
let teamId;
|
|
55
|
+
let repoBasics = {
|
|
56
|
+
fullName: null,
|
|
57
|
+
description: null,
|
|
58
|
+
};
|
|
59
|
+
if (repoOnly) {
|
|
60
|
+
const basics = await getRepositoryBasics(repoId);
|
|
61
|
+
teamId = basics.teamId;
|
|
62
|
+
repoBasics = { fullName: basics.fullName, description: basics.description };
|
|
63
|
+
if (!teamId) {
|
|
64
|
+
const msg = 'Repository is not associated with a team; recipes are team-scoped and require one.';
|
|
65
|
+
await markFailed(supabase, scanId, msg);
|
|
66
|
+
return { status: 'error', message: msg };
|
|
67
|
+
}
|
|
51
68
|
}
|
|
52
|
-
|
|
69
|
+
else {
|
|
70
|
+
teamId = await getProductTeamId(supabase, productId);
|
|
71
|
+
if (!teamId) {
|
|
72
|
+
const msg = 'Product is not associated with a team; recipes are team-scoped and require one.';
|
|
73
|
+
await markFailed(supabase, scanId, msg);
|
|
74
|
+
return { status: 'error', message: msg };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const githubConfig = repoOnly
|
|
78
|
+
? await getGitHubConfigByRepository(repoId, verbose)
|
|
79
|
+
: await getGitHubConfigByProduct(productId, verbose);
|
|
53
80
|
if (!githubConfig.configured ||
|
|
54
81
|
!githubConfig.token ||
|
|
55
82
|
!githubConfig.owner ||
|
|
56
83
|
!githubConfig.repo) {
|
|
57
84
|
const msg = githubConfig.message ||
|
|
58
|
-
|
|
85
|
+
(repoOnly
|
|
86
|
+
? 'GitHub repository not configured. Connect the repo first.'
|
|
87
|
+
: 'GitHub repository not configured for this product. Connect a repo first.');
|
|
59
88
|
await markFailed(supabase, scanId, msg);
|
|
60
89
|
return { status: 'error', message: msg };
|
|
61
90
|
}
|
|
@@ -63,13 +92,22 @@ export async function runRecipesPhase(options) {
|
|
|
63
92
|
let succeeded = false;
|
|
64
93
|
try {
|
|
65
94
|
const workspaceRoot = ensureWorkspaceDir();
|
|
66
|
-
const repoKey =
|
|
95
|
+
const repoKey = repoOnly
|
|
96
|
+
? `${WORKSPACE_KEY}-repo-${repoId}`
|
|
97
|
+
: `${WORKSPACE_KEY}-${productId}`;
|
|
67
98
|
({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, githubConfig.owner, githubConfig.repo, githubConfig.token));
|
|
68
|
-
const [
|
|
69
|
-
|
|
99
|
+
const [basics, scanMeta, teamRecipes, existingLinks] = await Promise.all([
|
|
100
|
+
repoOnly
|
|
101
|
+
? Promise.resolve({
|
|
102
|
+
name: repoBasics.fullName ?? `${githubConfig.owner}/${githubConfig.repo}`,
|
|
103
|
+
description: repoBasics.description ?? undefined,
|
|
104
|
+
})
|
|
105
|
+
: fetchProductBasics(productId),
|
|
70
106
|
getScanCreator(supabase, scanId),
|
|
71
107
|
listTeamRecipes(supabase, teamId),
|
|
72
|
-
|
|
108
|
+
repoOnly
|
|
109
|
+
? listRepositoryRecipeLinks(supabase, repoId)
|
|
110
|
+
: listProductRecipeLinks(supabase, productId),
|
|
73
111
|
]);
|
|
74
112
|
if (!scanMeta) {
|
|
75
113
|
const msg = 'recipe_scans row vanished mid-run; aborting';
|
|
@@ -78,8 +116,8 @@ export async function runRecipesPhase(options) {
|
|
|
78
116
|
}
|
|
79
117
|
const systemPrompt = createRecipesSystemPrompt();
|
|
80
118
|
const userPrompt = createRecipesUserPrompt({
|
|
81
|
-
productName:
|
|
82
|
-
productDescription:
|
|
119
|
+
productName: basics.name,
|
|
120
|
+
productDescription: basics.description,
|
|
83
121
|
guidance,
|
|
84
122
|
teamRecipes,
|
|
85
123
|
existingLinks: existingLinks.map((l) => ({
|
|
@@ -92,7 +130,8 @@ export async function runRecipesPhase(options) {
|
|
|
92
130
|
const mcpServer = createRecipesMcpServer({
|
|
93
131
|
supabase,
|
|
94
132
|
teamId,
|
|
95
|
-
productId,
|
|
133
|
+
productId: repoOnly ? undefined : productId,
|
|
134
|
+
repositoryId: repoOnly ? repoId : undefined,
|
|
96
135
|
createdBy: scanMeta.created_by,
|
|
97
136
|
}, counts, teamRecipes, existingLinkIds);
|
|
98
137
|
logInfo('Running Claude agent to identify recipes...');
|
|
@@ -229,6 +268,24 @@ export async function listProductRecipeLinks(supabase, productId) {
|
|
|
229
268
|
}
|
|
230
269
|
return out;
|
|
231
270
|
}
|
|
271
|
+
export async function listRepositoryRecipeLinks(supabase, repositoryId) {
|
|
272
|
+
const { data, error } = await supabase
|
|
273
|
+
.from('repository_recipes')
|
|
274
|
+
.select('recipe_id, recipes(name)')
|
|
275
|
+
.eq('repository_id', repositoryId);
|
|
276
|
+
if (error || !data) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const rows = data;
|
|
280
|
+
const out = [];
|
|
281
|
+
for (const r of rows) {
|
|
282
|
+
const recipe = Array.isArray(r.recipes) ? r.recipes[0] : r.recipes;
|
|
283
|
+
if (recipe) {
|
|
284
|
+
out.push({ recipe_id: r.recipe_id, name: recipe.name });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
232
289
|
/**
|
|
233
290
|
* Claim the row by flipping `pending` → `running`. Returns true on success
|
|
234
291
|
* (we won the claim) and false when the row has already moved on (e.g. user
|
|
@@ -22,7 +22,10 @@ import { type RecipeSummary } from './types.js';
|
|
|
22
22
|
export interface RecipesToolContext {
|
|
23
23
|
supabase: SupabaseClient;
|
|
24
24
|
teamId: string;
|
|
25
|
-
|
|
25
|
+
/** Set in product-scoped scans; links go to `product_recipes`. */
|
|
26
|
+
productId?: string;
|
|
27
|
+
/** Set in repo-only scans; links go to `repository_recipes`. */
|
|
28
|
+
repositoryId?: string;
|
|
26
29
|
createdBy: string;
|
|
27
30
|
}
|
|
28
31
|
/**
|