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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared multi-repo cloning for flow generation (screen-flow / data-flow).
|
|
3
|
+
*
|
|
4
|
+
* A flow can be scoped to one or several repositories (the user picks them in
|
|
5
|
+
* the desktop UI; the chosen set is stored on `diagrams.repository_ids`). This
|
|
6
|
+
* helper resolves that set, clones each repo into a per-flow parent workspace
|
|
7
|
+
* directory, and returns the directory the agent should run against:
|
|
8
|
+
* - single repo → the repo's own clone dir
|
|
9
|
+
* - many repos → the parent dir holding every clone as a subdirectory,
|
|
10
|
+
* so the agent can explore them all and produce one unified
|
|
11
|
+
* flow.
|
|
12
|
+
*
|
|
13
|
+
* Falls back to the product's primary repo when `repository_ids` is empty
|
|
14
|
+
* (older diagrams, or single-repo products).
|
|
15
|
+
*/
|
|
16
|
+
export interface ClonedRepo {
|
|
17
|
+
fullName: string;
|
|
18
|
+
owner: string;
|
|
19
|
+
repo: string;
|
|
20
|
+
dir: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CloneFlowReposSuccess {
|
|
23
|
+
ok: true;
|
|
24
|
+
/** Directory to point the agent at (parent dir for multi-repo). */
|
|
25
|
+
projectDir: string;
|
|
26
|
+
/** Directory to clean up afterwards (always the per-flow parent). */
|
|
27
|
+
cleanupDir: string;
|
|
28
|
+
repos: ClonedRepo[];
|
|
29
|
+
}
|
|
30
|
+
export interface CloneFlowReposFailure {
|
|
31
|
+
ok: false;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function safeDirName(fullName: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the repositories a flow targets (by id, preserving the stored
|
|
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`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveTargetRepos(productId: string | undefined, repositoryIds: string[], fallback?: {
|
|
43
|
+
owner: string;
|
|
44
|
+
repo: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
fullName: string;
|
|
47
|
+
owner: string;
|
|
48
|
+
repo: string;
|
|
49
|
+
}[]>;
|
|
50
|
+
export declare function cloneDiagramRepos(opts: {
|
|
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;
|
|
55
|
+
repositoryIds: string[];
|
|
56
|
+
workspaceKey: string;
|
|
57
|
+
verbose?: boolean;
|
|
58
|
+
}): Promise<CloneFlowReposSuccess | CloneFlowReposFailure>;
|
|
59
|
+
/**
|
|
60
|
+
* Build a short note describing the repo scope, appended to the agent's user
|
|
61
|
+
* prompt so it knows whether to map one repo or unify several.
|
|
62
|
+
*/
|
|
63
|
+
export declare function describeRepoScope(repos: ClonedRepo[]): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared multi-repo cloning for flow generation (screen-flow / data-flow).
|
|
3
|
+
*
|
|
4
|
+
* A flow can be scoped to one or several repositories (the user picks them in
|
|
5
|
+
* the desktop UI; the chosen set is stored on `diagrams.repository_ids`). This
|
|
6
|
+
* helper resolves that set, clones each repo into a per-flow parent workspace
|
|
7
|
+
* directory, and returns the directory the agent should run against:
|
|
8
|
+
* - single repo → the repo's own clone dir
|
|
9
|
+
* - many repos → the parent dir holding every clone as a subdirectory,
|
|
10
|
+
* so the agent can explore them all and produce one unified
|
|
11
|
+
* flow.
|
|
12
|
+
*
|
|
13
|
+
* Falls back to the product's primary repo when `repository_ids` is empty
|
|
14
|
+
* (older diagrams, or single-repo products).
|
|
15
|
+
*/
|
|
16
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
17
|
+
import { getSupabase } from '../../supabase/client.js';
|
|
18
|
+
import { logInfo, logWarning } from '../../utils/logger.js';
|
|
19
|
+
import { cloneIssueRepo, ensureWorkspaceDir, getIssueRepoPath, } from '../../workspace/workspace-manager.js';
|
|
20
|
+
export function safeDirName(fullName) {
|
|
21
|
+
return fullName.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the repositories a flow targets (by id, preserving the stored
|
|
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`.
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveTargetRepos(productId, repositoryIds, fallback) {
|
|
31
|
+
if (repositoryIds.length === 0) {
|
|
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 [];
|
|
42
|
+
}
|
|
43
|
+
const supabase = getSupabase();
|
|
44
|
+
const { data } = await supabase
|
|
45
|
+
.from('repositories')
|
|
46
|
+
.select('id, full_name')
|
|
47
|
+
.in('id', repositoryIds);
|
|
48
|
+
const byId = new Map((data ?? []).map((r) => [r.id, r.full_name]));
|
|
49
|
+
// Preserve the caller's order (diagrams.repository_ids is ordered).
|
|
50
|
+
const resolved = [];
|
|
51
|
+
for (const id of repositoryIds) {
|
|
52
|
+
const fullName = byId.get(id);
|
|
53
|
+
if (!fullName) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const [owner, repo] = fullName.split('/');
|
|
57
|
+
if (!owner || !repo) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
resolved.push({ fullName, owner, repo });
|
|
61
|
+
}
|
|
62
|
+
// If none resolved (deleted repos / RLS), fall back to the primary repo so
|
|
63
|
+
// generation still produces something useful (product mode only).
|
|
64
|
+
if (resolved.length === 0 && fallback) {
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
fullName: `${fallback.owner}/${fallback.repo}`,
|
|
68
|
+
owner: fallback.owner,
|
|
69
|
+
repo: fallback.repo,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
export async function cloneDiagramRepos(opts) {
|
|
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);
|
|
83
|
+
if (!gh.configured || !gh.token || !gh.owner || !gh.repo) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
message: gh.message ||
|
|
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.',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const workspaceRoot = ensureWorkspaceDir();
|
|
102
|
+
const parentDir = getIssueRepoPath(workspaceRoot, `${workspaceKey}-${repoOnly ? `repo-${repoId}` : productId}`);
|
|
103
|
+
const repos = [];
|
|
104
|
+
for (const target of targets) {
|
|
105
|
+
try {
|
|
106
|
+
// The product-level token (installation or user PAT/OAuth) is reused for
|
|
107
|
+
// every repo; if it can't access one, that clone fails and we skip it
|
|
108
|
+
// rather than aborting the whole generation.
|
|
109
|
+
const { repoPath } = cloneIssueRepo(parentDir, safeDirName(target.fullName), target.owner, target.repo, gh.token);
|
|
110
|
+
repos.push({
|
|
111
|
+
fullName: target.fullName,
|
|
112
|
+
owner: target.owner,
|
|
113
|
+
repo: target.repo,
|
|
114
|
+
dir: repoPath,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
logWarning(`Skipping ${target.fullName}: clone failed (${err instanceof Error ? err.message : String(err)})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (repos.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
message: 'Failed to clone any of the selected repositories.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (repos.length > 1) {
|
|
128
|
+
logInfo(`Cloned ${repos.length} repos for ${workspaceKey}: ${repos.map((r) => r.fullName).join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
// Single repo: run directly in its dir. Multi-repo: run in the parent so
|
|
133
|
+
// the agent sees every clone as a subdirectory.
|
|
134
|
+
projectDir: repos.length === 1 ? repos[0].dir : parentDir,
|
|
135
|
+
cleanupDir: parentDir,
|
|
136
|
+
repos,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build a short note describing the repo scope, appended to the agent's user
|
|
141
|
+
* prompt so it knows whether to map one repo or unify several.
|
|
142
|
+
*/
|
|
143
|
+
export function describeRepoScope(repos) {
|
|
144
|
+
if (repos.length <= 1) {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
const list = repos.map((r) => `- ${r.fullName} (subdirectory: ${safeDirName(r.fullName)})`);
|
|
148
|
+
return [
|
|
149
|
+
`This product spans ${repos.length} repositories, each cloned into its own subdirectory of the working directory:`,
|
|
150
|
+
...list,
|
|
151
|
+
'Explore all of them and produce a single unified flow that spans the repositories.',
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared diagram generation runner.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the common pipeline every node/edge diagram phase uses: clone
|
|
5
|
+
* the repo(s), resolve product/repo basics, build prompts, run the Claude
|
|
6
|
+
* Agent SDK loop with an in-process MCP capture server, fall back to a fenced
|
|
7
|
+
* block if the agent forgot the tool, then persist to the diagrams tables and
|
|
8
|
+
* flip status. Each lean phase just supplies its domain MCP config + prompts.
|
|
9
|
+
*/
|
|
10
|
+
import { type DiagramMcpConfig } from './mcp.js';
|
|
11
|
+
export interface GenerateDiagramOptions {
|
|
12
|
+
productId?: string;
|
|
13
|
+
repoId?: string;
|
|
14
|
+
diagramId: string;
|
|
15
|
+
guidance?: string;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
/** Workspace clone key, e.g. 'state-diagram'. */
|
|
18
|
+
workspaceKey: string;
|
|
19
|
+
/** Fenced block name for the fallback parser, e.g. 'state_diagram'. */
|
|
20
|
+
fenceName: string;
|
|
21
|
+
/** Plural noun for log/result messages, e.g. 'states', 'classes'. */
|
|
22
|
+
nounPlural: string;
|
|
23
|
+
edgeNounPlural: string;
|
|
24
|
+
mcpConfig: DiagramMcpConfig;
|
|
25
|
+
buildSystemPrompt: (args: {
|
|
26
|
+
projectDir: string;
|
|
27
|
+
hasCodebase: boolean;
|
|
28
|
+
}) => Promise<string>;
|
|
29
|
+
buildUserPrompt: (args: {
|
|
30
|
+
productName: string;
|
|
31
|
+
productDescription?: string;
|
|
32
|
+
guidance?: string;
|
|
33
|
+
}) => string;
|
|
34
|
+
}
|
|
35
|
+
export interface DiagramPhaseResult {
|
|
36
|
+
status: 'success' | 'error';
|
|
37
|
+
message: string;
|
|
38
|
+
nodesCreated?: number;
|
|
39
|
+
edgesCreated?: number;
|
|
40
|
+
summary?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function generateDiagram(options: GenerateDiagramOptions): Promise<DiagramPhaseResult>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared diagram generation runner.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the common pipeline every node/edge diagram phase uses: clone
|
|
5
|
+
* the repo(s), resolve product/repo basics, build prompts, run the Claude
|
|
6
|
+
* Agent SDK loop with an in-process MCP capture server, fall back to a fenced
|
|
7
|
+
* block if the agent forgot the tool, then persist to the diagrams tables and
|
|
8
|
+
* flip status. Each lean phase just supplies its domain MCP config + prompts.
|
|
9
|
+
*/
|
|
10
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
11
|
+
import { getRepositoryBasics } from '../../api/github.js';
|
|
12
|
+
import { DEFAULT_MODEL } from '../../constants.js';
|
|
13
|
+
import { getSupabase } from '../../supabase/client.js';
|
|
14
|
+
import { logError, logInfo, logSuccess, logWarning } from '../../utils/logger.js';
|
|
15
|
+
import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
|
|
16
|
+
import { fetchProductBasics } from '../find-shared/mcp.js';
|
|
17
|
+
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
18
|
+
import { cloneDiagramRepos, describeRepoScope } from './clone-repos.js';
|
|
19
|
+
import { getDiagramRepositoryIds, isDiagramExtraction, markDiagramFailed, markDiagramRunning, markDiagramSuccess, persistDiagramGraph, validateGraphConsistency, } from './graph.js';
|
|
20
|
+
import { createDiagramCaptureState, createDiagramMcpServer, } from './mcp.js';
|
|
21
|
+
const MAX_TURNS = 150;
|
|
22
|
+
export async function generateDiagram(options) {
|
|
23
|
+
const { productId, repoId, diagramId, guidance, verbose } = options;
|
|
24
|
+
const repoOnly = !productId && Boolean(repoId);
|
|
25
|
+
const label = options.mcpConfig.name;
|
|
26
|
+
logInfo(productId
|
|
27
|
+
? `Starting ${label} generation for product ${productId}`
|
|
28
|
+
: `Starting ${label} generation for repository ${repoId}`);
|
|
29
|
+
const supabase = getSupabase();
|
|
30
|
+
await markDiagramRunning(supabase, diagramId);
|
|
31
|
+
const repositoryIds = await getDiagramRepositoryIds(supabase, diagramId);
|
|
32
|
+
const cloneResult = await cloneDiagramRepos({
|
|
33
|
+
productId,
|
|
34
|
+
repoId,
|
|
35
|
+
repositoryIds,
|
|
36
|
+
workspaceKey: options.workspaceKey,
|
|
37
|
+
verbose,
|
|
38
|
+
});
|
|
39
|
+
if (!cloneResult.ok) {
|
|
40
|
+
await markDiagramFailed(supabase, diagramId, cloneResult.message);
|
|
41
|
+
return { status: 'error', message: cloneResult.message };
|
|
42
|
+
}
|
|
43
|
+
const { projectDir, cleanupDir, repos } = cloneResult;
|
|
44
|
+
let succeeded = false;
|
|
45
|
+
try {
|
|
46
|
+
const product = repoOnly
|
|
47
|
+
? await resolveRepoBasics(repoId, repos)
|
|
48
|
+
: await fetchProductBasics(productId);
|
|
49
|
+
const systemPrompt = await options.buildSystemPrompt({
|
|
50
|
+
projectDir,
|
|
51
|
+
hasCodebase: true,
|
|
52
|
+
});
|
|
53
|
+
const repoScope = describeRepoScope(repos);
|
|
54
|
+
const userPrompt = options.buildUserPrompt({
|
|
55
|
+
productName: product.name,
|
|
56
|
+
productDescription: product.description,
|
|
57
|
+
guidance: [guidance, repoScope].filter(Boolean).join('\n\n') || undefined,
|
|
58
|
+
});
|
|
59
|
+
logInfo(`Running Claude ${label} extraction...`);
|
|
60
|
+
const captureState = createDiagramCaptureState();
|
|
61
|
+
const mcpServer = createDiagramMcpServer(options.mcpConfig, captureState, {
|
|
62
|
+
onProgress: ({ phase, message }) => logInfo(`[${phase}] ${message}`),
|
|
63
|
+
});
|
|
64
|
+
let assistantBuffer = '';
|
|
65
|
+
let extraction = null;
|
|
66
|
+
const stream = query({
|
|
67
|
+
prompt: createPromptGenerator(userPrompt),
|
|
68
|
+
options: {
|
|
69
|
+
systemPrompt: {
|
|
70
|
+
type: 'preset',
|
|
71
|
+
preset: 'claude_code',
|
|
72
|
+
append: systemPrompt,
|
|
73
|
+
},
|
|
74
|
+
model: DEFAULT_MODEL,
|
|
75
|
+
maxTurns: MAX_TURNS,
|
|
76
|
+
permissionMode: 'bypassPermissions',
|
|
77
|
+
cwd: projectDir,
|
|
78
|
+
mcpServers: { [options.mcpConfig.name]: mcpServer },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
for await (const rawMessage of stream) {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
const message = rawMessage;
|
|
84
|
+
if (message.type === 'assistant') {
|
|
85
|
+
assistantBuffer += extractTextFromContent(message.message?.content ?? [], verbose);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (message.type === 'user' && verbose) {
|
|
89
|
+
const userContent = message.message?.content;
|
|
90
|
+
if (Array.isArray(userContent)) {
|
|
91
|
+
extractTextFromContent(userContent, verbose);
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (message.type !== 'result') {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (captureState.captured) {
|
|
99
|
+
extraction = captureState.captured;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const fallback = tryFallbackParse(message, assistantBuffer, options.fenceName);
|
|
103
|
+
if (fallback) {
|
|
104
|
+
logWarning(`Agent emitted a fenced ${options.fenceName} block instead of calling the submit tool; using the parsed text as a fallback.`);
|
|
105
|
+
extraction = fallback;
|
|
106
|
+
}
|
|
107
|
+
else if (message.subtype !== 'success') {
|
|
108
|
+
logError(`Extraction incomplete: ${message.subtype}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!extraction) {
|
|
112
|
+
const msg = `${label} extraction failed: agent did not call the submit tool and no parseable ${options.fenceName} block was found.`;
|
|
113
|
+
await markDiagramFailed(supabase, diagramId, msg);
|
|
114
|
+
return { status: 'error', message: msg };
|
|
115
|
+
}
|
|
116
|
+
logInfo(`Extraction produced ${extraction.nodes.length} ${options.nounPlural} / ${extraction.edges.length} ${options.edgeNounPlural}`);
|
|
117
|
+
const { nodesCreated, edgesCreated } = await persistDiagramGraph(supabase, diagramId, extraction);
|
|
118
|
+
await markDiagramSuccess(supabase, diagramId, extraction.summary);
|
|
119
|
+
succeeded = true;
|
|
120
|
+
logSuccess(`${label} generated: ${nodesCreated} ${options.nounPlural}, ${edgesCreated} ${options.edgeNounPlural}`);
|
|
121
|
+
return {
|
|
122
|
+
status: 'success',
|
|
123
|
+
message: `${label} generated (${nodesCreated} ${options.nounPlural}, ${edgesCreated} ${options.edgeNounPlural})`,
|
|
124
|
+
nodesCreated,
|
|
125
|
+
edgesCreated,
|
|
126
|
+
summary: extraction.summary,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
131
|
+
logError(`${label} failed: ${errorMessage}`);
|
|
132
|
+
await markDiagramFailed(supabase, diagramId, errorMessage);
|
|
133
|
+
return { status: 'error', message: errorMessage };
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if (succeeded) {
|
|
137
|
+
cleanupIssueRepo(cleanupDir);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function resolveRepoBasics(repositoryId, repos) {
|
|
142
|
+
const basics = await getRepositoryBasics(repositoryId).catch(() => null);
|
|
143
|
+
return {
|
|
144
|
+
name: basics?.fullName ?? repos[0]?.fullName ?? repositoryId,
|
|
145
|
+
description: basics?.description ?? undefined,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function tryFallbackParse(resultMessage, assistantText, fenceName) {
|
|
149
|
+
const responseText = resultMessage.subtype === 'success'
|
|
150
|
+
? resultMessage.result || assistantText
|
|
151
|
+
: assistantText;
|
|
152
|
+
const parsed = tryExtractResult(responseText, fenceName);
|
|
153
|
+
if (!isDiagramExtraction(parsed)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const { error } = validateGraphConsistency(parsed);
|
|
157
|
+
if (error) {
|
|
158
|
+
logWarning(`Fallback extraction failed consistency check: ${error}`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return parsed;
|
|
162
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared graph model + persistence for node/edge diagram types.
|
|
3
|
+
*
|
|
4
|
+
* Every diagram type (screen / data / er / sequence and the lean types built
|
|
5
|
+
* on this framework — state / class / architecture / flowchart) is a graph of
|
|
6
|
+
* nodes + edges stored in the generic `diagrams` / `diagram_nodes` /
|
|
7
|
+
* `diagram_edges` tables, discriminated by `diagrams.type`. This module owns
|
|
8
|
+
* the type-agnostic extraction shape, its consistency validation, and the
|
|
9
|
+
* persistence + status-transition helpers so each phase only has to describe
|
|
10
|
+
* its own domain schema and prompts.
|
|
11
|
+
*/
|
|
12
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
13
|
+
/** One node in a diagram. Domain-specific fields ride along and are stored
|
|
14
|
+
* verbatim in `diagram_nodes.schema`. */
|
|
15
|
+
export interface GraphNode {
|
|
16
|
+
/** Stable slug, unique within the diagram. */
|
|
17
|
+
slug: string;
|
|
18
|
+
name: string;
|
|
19
|
+
kind: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
/** One directed edge. fromSlug → toSlug must reference emitted node slugs. */
|
|
23
|
+
export interface GraphEdge {
|
|
24
|
+
fromSlug: string;
|
|
25
|
+
toSlug: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
/** File (jump-to-code) for the connection, stored as source_anchor. */
|
|
29
|
+
sourceFile?: string;
|
|
30
|
+
/** Extra per-edge data stored in diagram_edges.metadata (order, columns…). */
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export interface DiagramExtraction {
|
|
34
|
+
summary: string;
|
|
35
|
+
nodes: GraphNode[];
|
|
36
|
+
edges: GraphEdge[];
|
|
37
|
+
}
|
|
38
|
+
export declare function isGraphNode(value: unknown): value is GraphNode;
|
|
39
|
+
export declare function isGraphEdge(value: unknown): value is GraphEdge;
|
|
40
|
+
export declare function isDiagramExtraction(value: unknown): value is DiagramExtraction;
|
|
41
|
+
/**
|
|
42
|
+
* Cross-field checks: unique node slugs and every edge endpoint references a
|
|
43
|
+
* node that was emitted. Returns a human-readable instruction on failure so
|
|
44
|
+
* the agent can self-correct and re-submit.
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateGraphConsistency(extraction: DiagramExtraction): {
|
|
47
|
+
error: string | null;
|
|
48
|
+
};
|
|
49
|
+
export declare function markDiagramRunning(supabase: SupabaseClient, diagramId: string): Promise<void>;
|
|
50
|
+
export declare function markDiagramFailed(supabase: SupabaseClient, diagramId: string, errorMessage: string): Promise<void>;
|
|
51
|
+
export declare function markDiagramSuccess(supabase: SupabaseClient, diagramId: string, summary: string): Promise<void>;
|
|
52
|
+
/** Read the ordered repo set a diagram was scoped to (may be empty). */
|
|
53
|
+
export declare function getDiagramRepositoryIds(supabase: SupabaseClient, diagramId: string): Promise<string[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Replace the diagram's nodes and edges with a freshly extracted graph.
|
|
56
|
+
* Slugs are resolved to node ids for the edge foreign keys; edges whose
|
|
57
|
+
* endpoints weren't emitted are dropped.
|
|
58
|
+
*/
|
|
59
|
+
export declare function persistDiagramGraph(supabase: SupabaseClient, diagramId: string, extraction: DiagramExtraction): Promise<{
|
|
60
|
+
nodesCreated: number;
|
|
61
|
+
edgesCreated: number;
|
|
62
|
+
}>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared graph model + persistence for node/edge diagram types.
|
|
3
|
+
*
|
|
4
|
+
* Every diagram type (screen / data / er / sequence and the lean types built
|
|
5
|
+
* on this framework — state / class / architecture / flowchart) is a graph of
|
|
6
|
+
* nodes + edges stored in the generic `diagrams` / `diagram_nodes` /
|
|
7
|
+
* `diagram_edges` tables, discriminated by `diagrams.type`. This module owns
|
|
8
|
+
* the type-agnostic extraction shape, its consistency validation, and the
|
|
9
|
+
* persistence + status-transition helpers so each phase only has to describe
|
|
10
|
+
* its own domain schema and prompts.
|
|
11
|
+
*/
|
|
12
|
+
import { logWarning } from '../../utils/logger.js';
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null;
|
|
15
|
+
}
|
|
16
|
+
export function isGraphNode(value) {
|
|
17
|
+
return (isRecord(value) &&
|
|
18
|
+
typeof value.slug === 'string' &&
|
|
19
|
+
value.slug.length > 0 &&
|
|
20
|
+
typeof value.name === 'string' &&
|
|
21
|
+
value.name.length > 0 &&
|
|
22
|
+
typeof value.kind === 'string' &&
|
|
23
|
+
value.kind.length > 0);
|
|
24
|
+
}
|
|
25
|
+
export function isGraphEdge(value) {
|
|
26
|
+
return (isRecord(value) &&
|
|
27
|
+
typeof value.fromSlug === 'string' &&
|
|
28
|
+
typeof value.toSlug === 'string' &&
|
|
29
|
+
typeof value.kind === 'string');
|
|
30
|
+
}
|
|
31
|
+
export function isDiagramExtraction(value) {
|
|
32
|
+
return (isRecord(value) &&
|
|
33
|
+
typeof value.summary === 'string' &&
|
|
34
|
+
Array.isArray(value.nodes) &&
|
|
35
|
+
Array.isArray(value.edges) &&
|
|
36
|
+
value.nodes.every(isGraphNode) &&
|
|
37
|
+
value.edges.every(isGraphEdge));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Cross-field checks: unique node slugs and every edge endpoint references a
|
|
41
|
+
* node that was emitted. Returns a human-readable instruction on failure so
|
|
42
|
+
* the agent can self-correct and re-submit.
|
|
43
|
+
*/
|
|
44
|
+
export function validateGraphConsistency(extraction) {
|
|
45
|
+
const slugs = new Set();
|
|
46
|
+
for (const node of extraction.nodes) {
|
|
47
|
+
if (slugs.has(node.slug)) {
|
|
48
|
+
return {
|
|
49
|
+
error: `Duplicate node slug "${node.slug}". Each node slug MUST be unique within the diagram. Re-submit with deduplicated nodes.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
slugs.add(node.slug);
|
|
53
|
+
}
|
|
54
|
+
for (const edge of extraction.edges) {
|
|
55
|
+
if (!slugs.has(edge.fromSlug)) {
|
|
56
|
+
return {
|
|
57
|
+
error: `Edge "${edge.fromSlug}" → "${edge.toSlug}": fromSlug does not match any node slug. Add the missing node or drop the edge, then re-submit.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (!slugs.has(edge.toSlug)) {
|
|
61
|
+
return {
|
|
62
|
+
error: `Edge "${edge.fromSlug}" → "${edge.toSlug}": toSlug does not match any node slug. Add the missing node or drop the edge, then re-submit.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { error: null };
|
|
67
|
+
}
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Persistence
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Auto-layout grid; users drag afterwards and positions persist.
|
|
72
|
+
const COLUMN_WIDTH = 320;
|
|
73
|
+
const ROW_HEIGHT = 240;
|
|
74
|
+
const COLUMNS = 4;
|
|
75
|
+
export async function markDiagramRunning(supabase, diagramId) {
|
|
76
|
+
const { error } = await supabase
|
|
77
|
+
.from('diagrams')
|
|
78
|
+
.update({ status: 'running', error: null })
|
|
79
|
+
.eq('id', diagramId);
|
|
80
|
+
if (error) {
|
|
81
|
+
logWarning(`Could not mark diagram as running: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function markDiagramFailed(supabase, diagramId, errorMessage) {
|
|
85
|
+
await supabase
|
|
86
|
+
.from('diagrams')
|
|
87
|
+
.update({
|
|
88
|
+
status: 'failed',
|
|
89
|
+
error: errorMessage,
|
|
90
|
+
completed_at: new Date().toISOString(),
|
|
91
|
+
})
|
|
92
|
+
.eq('id', diagramId);
|
|
93
|
+
}
|
|
94
|
+
export async function markDiagramSuccess(supabase, diagramId, summary) {
|
|
95
|
+
await supabase
|
|
96
|
+
.from('diagrams')
|
|
97
|
+
.update({
|
|
98
|
+
status: 'success',
|
|
99
|
+
summary,
|
|
100
|
+
error: null,
|
|
101
|
+
completed_at: new Date().toISOString(),
|
|
102
|
+
})
|
|
103
|
+
.eq('id', diagramId);
|
|
104
|
+
}
|
|
105
|
+
/** Read the ordered repo set a diagram was scoped to (may be empty). */
|
|
106
|
+
export async function getDiagramRepositoryIds(supabase, diagramId) {
|
|
107
|
+
const { data } = await supabase
|
|
108
|
+
.from('diagrams')
|
|
109
|
+
.select('repository_ids')
|
|
110
|
+
.eq('id', diagramId)
|
|
111
|
+
.single();
|
|
112
|
+
return (data?.repository_ids ?? []).filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Replace the diagram's nodes and edges with a freshly extracted graph.
|
|
116
|
+
* Slugs are resolved to node ids for the edge foreign keys; edges whose
|
|
117
|
+
* endpoints weren't emitted are dropped.
|
|
118
|
+
*/
|
|
119
|
+
export async function persistDiagramGraph(supabase, diagramId, extraction) {
|
|
120
|
+
await supabase.from('diagram_edges').delete().eq('diagram_id', diagramId);
|
|
121
|
+
await supabase.from('diagram_nodes').delete().eq('diagram_id', diagramId);
|
|
122
|
+
if (extraction.nodes.length === 0) {
|
|
123
|
+
return { nodesCreated: 0, edgesCreated: 0 };
|
|
124
|
+
}
|
|
125
|
+
const nodeRows = extraction.nodes.map((node, i) => ({
|
|
126
|
+
diagram_id: diagramId,
|
|
127
|
+
slug: node.slug,
|
|
128
|
+
name: node.name,
|
|
129
|
+
kind: node.kind,
|
|
130
|
+
schema: node,
|
|
131
|
+
position_x: (i % COLUMNS) * COLUMN_WIDTH,
|
|
132
|
+
position_y: Math.floor(i / COLUMNS) * ROW_HEIGHT,
|
|
133
|
+
}));
|
|
134
|
+
const { data: insertedNodes, error: nodesError } = await supabase
|
|
135
|
+
.from('diagram_nodes')
|
|
136
|
+
.insert(nodeRows)
|
|
137
|
+
.select('id, slug');
|
|
138
|
+
if (nodesError) {
|
|
139
|
+
throw new Error(`Failed to insert nodes: ${nodesError.message}`);
|
|
140
|
+
}
|
|
141
|
+
const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
|
|
142
|
+
const edgeRows = extraction.edges
|
|
143
|
+
.map((edge) => {
|
|
144
|
+
const fromId = slugToId.get(edge.fromSlug);
|
|
145
|
+
const toId = slugToId.get(edge.toSlug);
|
|
146
|
+
if (!fromId || !toId) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
diagram_id: diagramId,
|
|
151
|
+
from_node_id: fromId,
|
|
152
|
+
to_node_id: toId,
|
|
153
|
+
label: edge.label ?? null,
|
|
154
|
+
source_anchor: edge.sourceFile ?? null,
|
|
155
|
+
kind: edge.kind,
|
|
156
|
+
metadata: edge.metadata ?? {},
|
|
157
|
+
};
|
|
158
|
+
})
|
|
159
|
+
.filter((e) => e !== null);
|
|
160
|
+
if (edgeRows.length > 0) {
|
|
161
|
+
const { error: edgesError } = await supabase
|
|
162
|
+
.from('diagram_edges')
|
|
163
|
+
.insert(edgeRows);
|
|
164
|
+
if (edgesError) {
|
|
165
|
+
throw new Error(`Failed to insert edges: ${edgesError.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { nodesCreated: nodeRows.length, edgesCreated: edgeRows.length };
|
|
169
|
+
}
|