archbyte 0.2.8 → 0.3.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/agents/pipeline/agents/component-identifier.js +67 -8
- package/dist/agents/pipeline/agents/connection-mapper.js +33 -8
- package/dist/agents/pipeline/agents/flow-detector.js +61 -8
- package/dist/agents/pipeline/agents/service-describer.js +36 -8
- package/dist/agents/pipeline/agents/validator.js +38 -9
- package/dist/agents/pipeline/index.d.ts +1 -0
- package/dist/agents/pipeline/index.js +123 -7
- package/dist/agents/pipeline/types.d.ts +11 -0
- package/dist/agents/providers/openai.js +6 -6
- package/dist/agents/runtime/types.js +2 -2
- package/dist/cli/analyze.js +68 -23
- package/dist/cli/auth.js +2 -2
- package/dist/cli/incremental.d.ts +21 -0
- package/dist/cli/incremental.js +117 -1
- package/dist/cli/serve.js +89 -13
- package/dist/cli/setup.js +119 -28
- package/dist/cli/yaml-io.d.ts +2 -0
- package/package.json +1 -1
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
import { extractJSON } from "../response-parser.js";
|
|
4
4
|
import { getPrompt } from "../../prompts.js";
|
|
5
5
|
import "../../prompt-data.js";
|
|
6
|
-
function formatFileTree(ctx) {
|
|
6
|
+
function formatFileTree(ctx, affectedPaths) {
|
|
7
7
|
// Show directories + key files only (not all 1000+ files) to stay within context limits
|
|
8
8
|
const lines = [];
|
|
9
|
-
const MAX_LINES = 300;
|
|
9
|
+
const MAX_LINES = affectedPaths ? 150 : 300;
|
|
10
|
+
function isAffectedSubtree(dirPath) {
|
|
11
|
+
if (!affectedPaths)
|
|
12
|
+
return true;
|
|
13
|
+
for (const p of affectedPaths) {
|
|
14
|
+
if (p.startsWith(dirPath + "/") || dirPath.startsWith(p + "/") || dirPath === p)
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
10
19
|
function walk(entries, indent) {
|
|
11
20
|
for (const e of entries) {
|
|
12
21
|
if (lines.length >= MAX_LINES)
|
|
@@ -14,6 +23,11 @@ function formatFileTree(ctx) {
|
|
|
14
23
|
if (e.type === "directory") {
|
|
15
24
|
const fileCount = e.children?.filter((c) => c.type === "file").length ?? 0;
|
|
16
25
|
const dirCount = e.children?.filter((c) => c.type === "directory").length ?? 0;
|
|
26
|
+
if (affectedPaths && indent >= 1 && !isAffectedSubtree(e.path)) {
|
|
27
|
+
// Collapse unchanged directories
|
|
28
|
+
lines.push(`${" ".repeat(indent)}${e.path}/ ... (${fileCount} files, unchanged)`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
17
31
|
lines.push(`${" ".repeat(indent)}${e.path}/ (${fileCount} files, ${dirCount} dirs)`);
|
|
18
32
|
if (e.children)
|
|
19
33
|
walk(e.children, indent + 1);
|
|
@@ -48,8 +62,49 @@ export const componentIdentifier = {
|
|
|
48
62
|
buildPrompt(ctx, priorResults) {
|
|
49
63
|
const system = getPrompt("pipeline/component-identifier");
|
|
50
64
|
const incremental = priorResults?.["__incremental__"];
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
// Build set of affected paths for tree filtering
|
|
66
|
+
let affectedPaths;
|
|
67
|
+
if (incremental) {
|
|
68
|
+
affectedPaths = new Set();
|
|
69
|
+
// Include paths of affected components
|
|
70
|
+
for (const compId of incremental.affectedComponents) {
|
|
71
|
+
const comp = incremental.existingSpec.components[compId];
|
|
72
|
+
if (comp?.path)
|
|
73
|
+
affectedPaths.add(comp.path);
|
|
74
|
+
}
|
|
75
|
+
// Include paths of neighbor components
|
|
76
|
+
for (const compId of incremental.neighborComponents) {
|
|
77
|
+
const comp = incremental.existingSpec.components[compId];
|
|
78
|
+
if (comp?.path)
|
|
79
|
+
affectedPaths.add(comp.path);
|
|
80
|
+
}
|
|
81
|
+
// Include directories of changed files
|
|
82
|
+
for (const f of incremental.changedFiles) {
|
|
83
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
84
|
+
affectedPaths.add(dir);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const fileTree = formatFileTree(ctx, affectedPaths);
|
|
88
|
+
// Filter configs: in incremental mode, only include configs from affected dirs + root configs
|
|
89
|
+
let configs;
|
|
90
|
+
if (incremental && affectedPaths) {
|
|
91
|
+
const filteredConfigs = ctx.codeSamples.configFiles.filter((cf) => {
|
|
92
|
+
const dir = cf.path.includes("/") ? cf.path.substring(0, cf.path.lastIndexOf("/")) : ".";
|
|
93
|
+
// Always include root configs
|
|
94
|
+
if (!cf.path.includes("/"))
|
|
95
|
+
return true;
|
|
96
|
+
// Include configs from affected directories
|
|
97
|
+
for (const p of affectedPaths) {
|
|
98
|
+
if (dir.startsWith(p) || p.startsWith(dir))
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
configs = filteredConfigs.map((cf) => `--- ${cf.path} ---\n${cf.content}`).join("\n\n");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
configs = formatConfigs(ctx);
|
|
107
|
+
}
|
|
53
108
|
const structureInfo = [
|
|
54
109
|
`Project: ${ctx.structure.projectName || "(unknown)"}`,
|
|
55
110
|
`Language: ${ctx.structure.language}`,
|
|
@@ -62,9 +117,13 @@ export const componentIdentifier = {
|
|
|
62
117
|
const docNotes = ctx.docs.architectureNotes.length > 0
|
|
63
118
|
? `\n\nArchitecture notes from docs:\n${ctx.docs.architectureNotes.join("\n")}`
|
|
64
119
|
: "";
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
120
|
+
// In incremental mode, only include Docker info if infra files changed
|
|
121
|
+
let dockerInfo = "";
|
|
122
|
+
if (ctx.infra.docker.composeFile) {
|
|
123
|
+
if (!incremental || incremental.categories.infraFiles.length > 0) {
|
|
124
|
+
dockerInfo = `\n\nDocker Compose services:\n${ctx.infra.docker.services.map((s) => `- ${s.name}${s.image ? ` (image: ${s.image})` : ""}${s.buildContext ? ` (build: ${s.buildContext})` : ""}${s.dependsOn?.length ? ` depends_on: ${s.dependsOn.join(", ")}` : ""}`).join("\n")}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
68
127
|
let incrementalInfo = "";
|
|
69
128
|
if (incremental) {
|
|
70
129
|
const existing = Object.entries(incremental.existingSpec.components)
|
|
@@ -76,7 +135,7 @@ ${existing}
|
|
|
76
135
|
Changed files: ${incremental.changedFiles.join(", ")}
|
|
77
136
|
Affected components: ${incremental.affectedComponents.length > 0 ? incremental.affectedComponents.join(", ") : "none (check for new components)"}
|
|
78
137
|
|
|
79
|
-
IMPORTANT: Return ALL components — both existing unaffected ones AND updated/new ones.
|
|
138
|
+
IMPORTANT: Return ALL components — both existing unaffected ones AND updated/new ones. Focus: re-evaluate components in [${[...new Set([...incremental.affectedComponents, ...incremental.neighborComponents])].join(", ")}]. Return other components as-is.`;
|
|
80
139
|
}
|
|
81
140
|
const user = `Analyze this project and identify ALL architecturally significant components.
|
|
82
141
|
|
|
@@ -47,10 +47,36 @@ export const connectionMapper = {
|
|
|
47
47
|
parts.push(`- ${ar.method} ${ar.path} (${ar.handlerFile})`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
// Import map
|
|
51
|
-
const
|
|
50
|
+
// Import map — filter to affected + neighbor component dirs in incremental mode
|
|
51
|
+
const incremental = priorResults?.["__incremental__"];
|
|
52
|
+
let affectedDirs;
|
|
53
|
+
if (incremental) {
|
|
54
|
+
affectedDirs = new Set();
|
|
55
|
+
for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
|
|
56
|
+
const comp = incremental.existingSpec.components[compId];
|
|
57
|
+
if (comp?.path)
|
|
58
|
+
affectedDirs.add(comp.path);
|
|
59
|
+
}
|
|
60
|
+
for (const f of incremental.changedFiles) {
|
|
61
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
62
|
+
affectedDirs.add(dir);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let importEntries = Object.entries(ctx.codeSamples.importMap);
|
|
66
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
67
|
+
importEntries = importEntries.filter(([file]) => {
|
|
68
|
+
const dir = file.includes("/") ? file.substring(0, file.lastIndexOf("/")) : ".";
|
|
69
|
+
for (const ad of affectedDirs) {
|
|
70
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
52
76
|
if (importEntries.length > 0) {
|
|
53
|
-
|
|
77
|
+
const totalEntries = Object.keys(ctx.codeSamples.importMap).length;
|
|
78
|
+
const label = affectedDirs ? `${importEntries.length} of ${totalEntries} files, filtered to affected dirs` : `${totalEntries} files`;
|
|
79
|
+
parts.push(`\n## Import Graph (${label})`);
|
|
54
80
|
// Summarize by directory to keep it concise
|
|
55
81
|
const dirImports = {};
|
|
56
82
|
for (const [file, imports] of importEntries) {
|
|
@@ -69,8 +95,8 @@ export const connectionMapper = {
|
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
97
|
}
|
|
72
|
-
// Docker depends_on
|
|
73
|
-
if (ctx.infra.docker.composeFile) {
|
|
98
|
+
// Docker depends_on — only if infra changed (or full scan)
|
|
99
|
+
if (ctx.infra.docker.composeFile && (!incremental || incremental.categories.infraFiles.length > 0)) {
|
|
74
100
|
const deps = ctx.infra.docker.services
|
|
75
101
|
.filter((s) => s.dependsOn?.length)
|
|
76
102
|
.map((s) => `${s.name} → ${s.dependsOn.join(", ")}`);
|
|
@@ -78,15 +104,14 @@ export const connectionMapper = {
|
|
|
78
104
|
parts.push(`\n## Docker depends_on\n${deps.join("\n")}`);
|
|
79
105
|
}
|
|
80
106
|
}
|
|
81
|
-
// Kubernetes resources
|
|
82
|
-
if (ctx.infra.kubernetes.resources.length > 0) {
|
|
107
|
+
// Kubernetes resources — only if infra changed (or full scan)
|
|
108
|
+
if (ctx.infra.kubernetes.resources.length > 0 && (!incremental || incremental.categories.infraFiles.length > 0)) {
|
|
83
109
|
parts.push("\n## Kubernetes Resources");
|
|
84
110
|
for (const r of ctx.infra.kubernetes.resources) {
|
|
85
111
|
parts.push(`- ${r.kind}: ${r.name}${r.namespace ? ` (ns=${r.namespace})` : ""}`);
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
114
|
// Incremental context
|
|
89
|
-
const incremental = priorResults?.["__incremental__"];
|
|
90
115
|
if (incremental) {
|
|
91
116
|
const existingConns = incremental.existingSpec.connections
|
|
92
117
|
.map((c) => `- ${c.from} → ${c.to} (${c.type}${c.async ? ", async" : ""}): ${c.description}`)
|
|
@@ -11,6 +11,20 @@ export const flowDetector = {
|
|
|
11
11
|
buildPrompt(ctx, priorResults) {
|
|
12
12
|
const system = getPrompt("pipeline/flow-detector");
|
|
13
13
|
const incremental = priorResults?.["__incremental__"];
|
|
14
|
+
// Build set of affected directories for filtering
|
|
15
|
+
let affectedDirs;
|
|
16
|
+
if (incremental) {
|
|
17
|
+
affectedDirs = new Set();
|
|
18
|
+
for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
|
|
19
|
+
const comp = incremental.existingSpec.components[compId];
|
|
20
|
+
if (comp?.path)
|
|
21
|
+
affectedDirs.add(comp.path);
|
|
22
|
+
}
|
|
23
|
+
for (const f of incremental.changedFiles) {
|
|
24
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
25
|
+
affectedDirs.add(dir);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
const parts = [];
|
|
15
29
|
// Event patterns from static scanner
|
|
16
30
|
if (ctx.events.hasEDA) {
|
|
@@ -18,8 +32,23 @@ export const flowDetector = {
|
|
|
18
32
|
for (const p of ctx.events.patterns) {
|
|
19
33
|
parts.push(` Technology: ${p.technology} (dep: ${p.dependency})`);
|
|
20
34
|
}
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
// Filter events to affected directories in incremental mode
|
|
36
|
+
let events = ctx.events.events;
|
|
37
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
38
|
+
events = events.filter((e) => {
|
|
39
|
+
const dir = e.file.includes("/") ? e.file.substring(0, e.file.lastIndexOf("/")) : ".";
|
|
40
|
+
for (const ad of affectedDirs) {
|
|
41
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
parts.push(`\nEvent occurrences in affected files (${events.length} of ${ctx.events.events.length} total):`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
parts.push("\nEvent occurrences in code:");
|
|
50
|
+
}
|
|
51
|
+
for (const e of events.slice(0, 30)) {
|
|
23
52
|
parts.push(` [${e.type}] ${e.file}: ${e.pattern}`);
|
|
24
53
|
}
|
|
25
54
|
}
|
|
@@ -33,24 +62,48 @@ export const flowDetector = {
|
|
|
33
62
|
parts.push(` ${ep.method} ${ep.path} — ${ep.description}`);
|
|
34
63
|
}
|
|
35
64
|
}
|
|
36
|
-
// Route file samples
|
|
37
|
-
|
|
65
|
+
// Route file samples — filter to affected directories in incremental mode
|
|
66
|
+
let routeSamples = ctx.codeSamples.samples.filter((s) => s.category === "route-file");
|
|
67
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
68
|
+
const filtered = routeSamples.filter((s) => {
|
|
69
|
+
const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
|
|
70
|
+
for (const ad of affectedDirs) {
|
|
71
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
if (filtered.length > 0)
|
|
77
|
+
routeSamples = filtered;
|
|
78
|
+
}
|
|
38
79
|
if (routeSamples.length > 0) {
|
|
39
80
|
parts.push("\nRoute file excerpts:");
|
|
40
81
|
for (const s of routeSamples.slice(0, 5)) {
|
|
41
82
|
parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
|
|
42
83
|
}
|
|
43
84
|
}
|
|
44
|
-
// Entry points
|
|
45
|
-
|
|
85
|
+
// Entry points — filter to affected directories in incremental mode
|
|
86
|
+
let entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
|
|
87
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
88
|
+
const filtered = entrySamples.filter((s) => {
|
|
89
|
+
const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
|
|
90
|
+
for (const ad of affectedDirs) {
|
|
91
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
if (filtered.length > 0)
|
|
97
|
+
entrySamples = filtered;
|
|
98
|
+
}
|
|
46
99
|
if (entrySamples.length > 0) {
|
|
47
100
|
parts.push("\nEntry point excerpts:");
|
|
48
101
|
for (const s of entrySamples.slice(0, 3)) {
|
|
49
102
|
parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
|
|
50
103
|
}
|
|
51
104
|
}
|
|
52
|
-
// Docker info for queue/broker identification
|
|
53
|
-
if (ctx.infra.docker.composeFile) {
|
|
105
|
+
// Docker info for queue/broker identification — only if event-related files changed
|
|
106
|
+
if (ctx.infra.docker.composeFile && (!incremental || incremental.categories.eventFiles.length > 0 || incremental.categories.infraFiles.length > 0)) {
|
|
54
107
|
const queueServices = ctx.infra.docker.services.filter((s) => {
|
|
55
108
|
const img = (s.image ?? "").toLowerCase();
|
|
56
109
|
return img.includes("rabbit") || img.includes("kafka") || img.includes("redis") ||
|
|
@@ -11,6 +11,22 @@ export const serviceDescriber = {
|
|
|
11
11
|
buildPrompt(ctx, priorResults) {
|
|
12
12
|
const system = getPrompt("pipeline/service-describer");
|
|
13
13
|
const incremental = priorResults?.["__incremental__"];
|
|
14
|
+
// Build affected dirs for filtering
|
|
15
|
+
let affectedDirs;
|
|
16
|
+
if (incremental) {
|
|
17
|
+
affectedDirs = new Set();
|
|
18
|
+
for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
|
|
19
|
+
const comp = incremental.existingSpec.components[compId];
|
|
20
|
+
if (comp?.path)
|
|
21
|
+
affectedDirs.add(comp.path);
|
|
22
|
+
}
|
|
23
|
+
for (const f of incremental.changedFiles) {
|
|
24
|
+
const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
|
|
25
|
+
affectedDirs.add(dir);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const hasInfraChanges = incremental ? incremental.categories.infraFiles.length > 0 : true;
|
|
29
|
+
const hasConfigChanges = incremental ? incremental.categories.configFiles.length > 0 : true;
|
|
14
30
|
const parts = [];
|
|
15
31
|
// Structure info
|
|
16
32
|
parts.push(`Project: ${ctx.structure.projectName || "(unknown)"}`);
|
|
@@ -24,8 +40,8 @@ export const serviceDescriber = {
|
|
|
24
40
|
if (ctx.docs.externalDependencies.length > 0) {
|
|
25
41
|
parts.push(`\nExternal dependencies mentioned: ${ctx.docs.externalDependencies.join(", ")}`);
|
|
26
42
|
}
|
|
27
|
-
// Docker services
|
|
28
|
-
if (ctx.infra.docker.composeFile) {
|
|
43
|
+
// Docker services — only include if infra/config files changed (or full scan)
|
|
44
|
+
if (ctx.infra.docker.composeFile && (hasInfraChanges || hasConfigChanges)) {
|
|
29
45
|
const svcInfo = ctx.infra.docker.services.map((s) => {
|
|
30
46
|
const details = [s.name];
|
|
31
47
|
if (s.image)
|
|
@@ -40,22 +56,34 @@ export const serviceDescriber = {
|
|
|
40
56
|
});
|
|
41
57
|
parts.push(`\nDocker services:\n${svcInfo.join("\n")}`);
|
|
42
58
|
}
|
|
43
|
-
// Env vars
|
|
44
|
-
if (ctx.envs.environments.length > 0) {
|
|
59
|
+
// Env vars — only include if infra/config files changed (or full scan)
|
|
60
|
+
if (ctx.envs.environments.length > 0 && (hasInfraChanges || hasConfigChanges)) {
|
|
45
61
|
for (const env of ctx.envs.environments) {
|
|
46
62
|
parts.push(`\nEnv file "${env.name}" variables: ${env.variables.join(", ")}`);
|
|
47
63
|
}
|
|
48
64
|
}
|
|
49
|
-
// Code samples (entry points)
|
|
50
|
-
|
|
65
|
+
// Code samples (entry points) — filter to affected dirs in incremental mode
|
|
66
|
+
let entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
|
|
67
|
+
if (affectedDirs && affectedDirs.size > 0) {
|
|
68
|
+
const filtered = entrySamples.filter((s) => {
|
|
69
|
+
const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
|
|
70
|
+
for (const ad of affectedDirs) {
|
|
71
|
+
if (dir.startsWith(ad) || ad.startsWith(dir))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
if (filtered.length > 0)
|
|
77
|
+
entrySamples = filtered;
|
|
78
|
+
}
|
|
51
79
|
if (entrySamples.length > 0) {
|
|
52
80
|
parts.push("\nEntry point excerpts:");
|
|
53
81
|
for (const s of entrySamples.slice(0, 5)) {
|
|
54
82
|
parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
|
|
55
83
|
}
|
|
56
84
|
}
|
|
57
|
-
// Cloud info
|
|
58
|
-
if (ctx.infra.cloud.provider) {
|
|
85
|
+
// Cloud info — only if infra changed (or full scan)
|
|
86
|
+
if (ctx.infra.cloud.provider && hasInfraChanges) {
|
|
59
87
|
parts.push(`\nCloud: ${ctx.infra.cloud.provider}, services: ${ctx.infra.cloud.services.join(", ")}`);
|
|
60
88
|
}
|
|
61
89
|
// Incremental context
|
|
@@ -10,15 +10,36 @@ export const validator = {
|
|
|
10
10
|
phase: "sequential",
|
|
11
11
|
buildPrompt(ctx, priorResults) {
|
|
12
12
|
const system = getPrompt("pipeline/validator");
|
|
13
|
+
const incremental = priorResults?.["__incremental__"];
|
|
14
|
+
// Build set of relevant component IDs for filtering
|
|
15
|
+
let relevantComponents;
|
|
16
|
+
if (incremental) {
|
|
17
|
+
relevantComponents = new Set([...incremental.affectedComponents, ...incremental.neighborComponents]);
|
|
18
|
+
}
|
|
13
19
|
const parts = [];
|
|
14
|
-
// Components
|
|
20
|
+
// Components — in incremental mode, show affected + neighbors in full, others as summary
|
|
15
21
|
const components = priorResults?.["component-identifier"];
|
|
16
22
|
if (components?.components) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
parts.push(
|
|
21
|
-
|
|
23
|
+
if (relevantComponents && relevantComponents.size > 0) {
|
|
24
|
+
const relevant = components.components.filter((c) => relevantComponents.has(c.id));
|
|
25
|
+
const others = components.components.length - relevant.length;
|
|
26
|
+
parts.push("## Components (affected + neighbors)");
|
|
27
|
+
for (const c of relevant) {
|
|
28
|
+
parts.push(`- ${c.id}: ${c.name} (type=${c.type}, layer=${c.layer}, path=${c.path})`);
|
|
29
|
+
parts.push(` description: ${c.description || "(none)"}`);
|
|
30
|
+
parts.push(` tech: [${(c.technologies || []).join(", ")}]`);
|
|
31
|
+
}
|
|
32
|
+
if (others > 0) {
|
|
33
|
+
parts.push(`\n(${others} other unchanged components omitted)`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
parts.push("## Components");
|
|
38
|
+
for (const c of components.components) {
|
|
39
|
+
parts.push(`- ${c.id}: ${c.name} (type=${c.type}, layer=${c.layer}, path=${c.path})`);
|
|
40
|
+
parts.push(` description: ${c.description || "(none)"}`);
|
|
41
|
+
parts.push(` tech: [${(c.technologies || []).join(", ")}]`);
|
|
42
|
+
}
|
|
22
43
|
}
|
|
23
44
|
}
|
|
24
45
|
// Services
|
|
@@ -35,11 +56,19 @@ export const validator = {
|
|
|
35
56
|
parts.push(`- ${svc.id}: ${svc.name} (${svc.type}) — ${svc.description}`);
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
|
-
// Connections
|
|
59
|
+
// Connections — in incremental mode, only show connections involving affected/neighbor components
|
|
39
60
|
const connections = priorResults?.["connection-mapper"];
|
|
40
61
|
if (connections?.connections) {
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
let conns = connections.connections;
|
|
63
|
+
if (relevantComponents && relevantComponents.size > 0) {
|
|
64
|
+
conns = conns.filter((c) => relevantComponents.has(c.from) || relevantComponents.has(c.to));
|
|
65
|
+
const omitted = connections.connections.length - conns.length;
|
|
66
|
+
parts.push(`\n## Connections (involving affected components${omitted > 0 ? `, ${omitted} others omitted` : ""})`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
parts.push("\n## Connections");
|
|
70
|
+
}
|
|
71
|
+
for (const c of conns) {
|
|
43
72
|
parts.push(`- ${c.from} → ${c.to} (${c.type}) ${c.async ? "[async]" : ""}: ${c.description}`);
|
|
44
73
|
}
|
|
45
74
|
}
|
|
@@ -15,6 +15,79 @@ function isAuthError(err) {
|
|
|
15
15
|
const msg = err instanceof Error ? err.message : String(err);
|
|
16
16
|
return /401|authentication_error|invalid.*api.?key|unauthorized|invalid.*key/i.test(msg);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Determine if an agent can be skipped based on what changed.
|
|
20
|
+
* Returns a skip reason string if skippable, or null if the agent must run.
|
|
21
|
+
*/
|
|
22
|
+
function shouldSkipAgent(agentId, inc) {
|
|
23
|
+
// Never skip if there are unmapped files — potential new components
|
|
24
|
+
if (inc.hasUnmappedFiles)
|
|
25
|
+
return null;
|
|
26
|
+
const cat = inc.categories;
|
|
27
|
+
const hasInfra = cat.infraFiles.length > 0;
|
|
28
|
+
const hasConfig = cat.configFiles.length > 0;
|
|
29
|
+
const hasEntryPoints = cat.entryPoints.length > 0;
|
|
30
|
+
const hasRoutes = cat.routeFiles.length > 0;
|
|
31
|
+
const hasEvents = cat.eventFiles.length > 0;
|
|
32
|
+
switch (agentId) {
|
|
33
|
+
case "service-describer":
|
|
34
|
+
// Skip when no infra/config/entry-point changes
|
|
35
|
+
if (!hasInfra && !hasConfig && !hasEntryPoints) {
|
|
36
|
+
return "no infra/config/entry-point changes";
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
case "flow-detector":
|
|
40
|
+
// Skip when no route/event/entry-point changes
|
|
41
|
+
if (!hasRoutes && !hasEvents && !hasEntryPoints) {
|
|
42
|
+
return "no route/event/entry-point changes";
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
case "validator":
|
|
46
|
+
// Skip when ≤1 affected component and no infra changes
|
|
47
|
+
if (inc.affectedComponents.length <= 1 && !hasInfra) {
|
|
48
|
+
return `only ${inc.affectedComponents.length} component affected, no infra changes`;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
// component-identifier and connection-mapper never skip
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Build fallback data for a skipped agent from existing spec */
|
|
57
|
+
function getFallbackData(agentId, inc) {
|
|
58
|
+
switch (agentId) {
|
|
59
|
+
case "service-describer": {
|
|
60
|
+
const dbs = Object.entries(inc.existingSpec.databases).map(([id, d]) => ({
|
|
61
|
+
id, name: d.name, type: d.type, description: d.description, usedBy: [],
|
|
62
|
+
}));
|
|
63
|
+
const exts = Object.entries(inc.existingSpec["external-services"]).map(([id, s]) => ({
|
|
64
|
+
id, name: s.name, type: s.type, description: s.description, usedBy: [],
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
projectDescription: "",
|
|
68
|
+
primaryLanguage: "",
|
|
69
|
+
databases: dbs,
|
|
70
|
+
externalServices: exts,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
case "flow-detector":
|
|
74
|
+
return {
|
|
75
|
+
eventConnections: [],
|
|
76
|
+
apiRoutes: [],
|
|
77
|
+
};
|
|
78
|
+
case "validator":
|
|
79
|
+
return {
|
|
80
|
+
componentTypeCorrections: {},
|
|
81
|
+
componentDescriptions: {},
|
|
82
|
+
addedConnections: [],
|
|
83
|
+
removedConnectionKeys: [],
|
|
84
|
+
confidence: 0.8,
|
|
85
|
+
issues: [],
|
|
86
|
+
};
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
18
91
|
/**
|
|
19
92
|
* Run the multi-agent pipeline: 3 parallel fast agents → 2 sequential agents.
|
|
20
93
|
* Each agent gets a single chat() call with pre-collected static context.
|
|
@@ -22,6 +95,7 @@ function isAuthError(err) {
|
|
|
22
95
|
export async function runPipeline(ctx, provider, config, onProgress, incrementalContext) {
|
|
23
96
|
const agentResults = {};
|
|
24
97
|
const agentMeta = [];
|
|
98
|
+
const skippedAgents = [];
|
|
25
99
|
// Pass incremental context to agents via priorResults
|
|
26
100
|
if (incrementalContext) {
|
|
27
101
|
agentResults["__incremental__"] = incrementalContext;
|
|
@@ -30,10 +104,27 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
|
|
|
30
104
|
onProgress?.(`Phase 1: Running ${PARALLEL_AGENTS.length} agents in parallel...`);
|
|
31
105
|
// Pass agentResults to parallel agents too (contains __incremental__ if set)
|
|
32
106
|
const parallelPrior = incrementalContext ? agentResults : undefined;
|
|
33
|
-
|
|
107
|
+
// Check for agent skipping
|
|
108
|
+
const parallelTasks = PARALLEL_AGENTS.map((agent) => ({
|
|
109
|
+
agent,
|
|
110
|
+
skipReason: incrementalContext ? shouldSkipAgent(agent.id, incrementalContext) : null,
|
|
111
|
+
}));
|
|
112
|
+
const parallelResults = await Promise.allSettled(parallelTasks.map(({ agent, skipReason }) => {
|
|
113
|
+
if (skipReason) {
|
|
114
|
+
// Skip this agent — use fallback data
|
|
115
|
+
skippedAgents.push(agent.id);
|
|
116
|
+
onProgress?.(` ${agent.name}: skipped (${skipReason})`);
|
|
117
|
+
const fallback = getFallbackData(agent.id, incrementalContext);
|
|
118
|
+
agentResults[agent.id] = fallback;
|
|
119
|
+
return Promise.resolve(null);
|
|
120
|
+
}
|
|
121
|
+
return runAgent(agent, ctx, provider, config, parallelPrior, onProgress);
|
|
122
|
+
}));
|
|
34
123
|
let authFailed = false;
|
|
35
|
-
for (let i = 0; i <
|
|
36
|
-
const agent =
|
|
124
|
+
for (let i = 0; i < parallelTasks.length; i++) {
|
|
125
|
+
const { agent, skipReason } = parallelTasks[i];
|
|
126
|
+
if (skipReason)
|
|
127
|
+
continue; // Already handled
|
|
37
128
|
const result = parallelResults[i];
|
|
38
129
|
if (result.status === "fulfilled" && result.value) {
|
|
39
130
|
agentResults[agent.id] = result.value.data;
|
|
@@ -55,6 +146,15 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
|
|
|
55
146
|
// === Phase 2: Sequential agents ===
|
|
56
147
|
onProgress?.(`Phase 2: Running ${SEQUENTIAL_AGENTS.length} agents sequentially...`);
|
|
57
148
|
for (const agent of SEQUENTIAL_AGENTS) {
|
|
149
|
+
// Check for agent skipping
|
|
150
|
+
const skipReason = incrementalContext ? shouldSkipAgent(agent.id, incrementalContext) : null;
|
|
151
|
+
if (skipReason) {
|
|
152
|
+
skippedAgents.push(agent.id);
|
|
153
|
+
onProgress?.(` ${agent.name}: skipped (${skipReason})`);
|
|
154
|
+
const fallback = getFallbackData(agent.id, incrementalContext);
|
|
155
|
+
agentResults[agent.id] = fallback;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
58
158
|
try {
|
|
59
159
|
const result = await runAgent(agent, ctx, provider, config, agentResults, onProgress);
|
|
60
160
|
if (result) {
|
|
@@ -82,13 +182,29 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
|
|
|
82
182
|
if (merged.validation.repairs.length > 0) {
|
|
83
183
|
onProgress?.(`Applied ${merged.validation.repairs.length} deterministic repair(s)`);
|
|
84
184
|
}
|
|
85
|
-
// Log token usage
|
|
185
|
+
// Log per-agent + total token usage
|
|
86
186
|
const totalInput = agentMeta.reduce((s, a) => s + (a.tokenUsage?.input ?? 0), 0);
|
|
87
187
|
const totalOutput = agentMeta.reduce((s, a) => s + (a.tokenUsage?.output ?? 0), 0);
|
|
88
|
-
if (totalInput > 0) {
|
|
89
|
-
|
|
188
|
+
if (totalInput > 0 || skippedAgents.length > 0) {
|
|
189
|
+
// Log to stderr so progress bar doesn't clobber it
|
|
190
|
+
console.error("\n Token usage breakdown:");
|
|
191
|
+
for (const a of agentMeta) {
|
|
192
|
+
const inp = a.tokenUsage?.input ?? 0;
|
|
193
|
+
const out = a.tokenUsage?.output ?? 0;
|
|
194
|
+
const dur = (a.duration / 1000).toFixed(1);
|
|
195
|
+
console.error(` ${a.agentId.padEnd(22)} ${String(inp).padStart(6)} in ${String(out).padStart(6)} out ${dur}s`);
|
|
196
|
+
}
|
|
197
|
+
for (const id of skippedAgents) {
|
|
198
|
+
console.error(` ${id.padEnd(22)} 0 in 0 out SKIPPED`);
|
|
199
|
+
}
|
|
200
|
+
console.error(` ${"TOTAL".padEnd(22)} ${String(totalInput).padStart(6)} in ${String(totalOutput).padStart(6)} out`);
|
|
201
|
+
if (skippedAgents.length > 0) {
|
|
202
|
+
console.error(` Skipped ${skippedAgents.length} agent(s): ${skippedAgents.join(", ")}`);
|
|
203
|
+
}
|
|
204
|
+
console.error();
|
|
205
|
+
onProgress?.(`Token usage: ${totalInput} in / ${totalOutput} out${skippedAgents.length > 0 ? ` (${skippedAgents.length} skipped)` : ""}`);
|
|
90
206
|
}
|
|
91
|
-
return { ...merged, tokenUsage: { input: totalInput, output: totalOutput } };
|
|
207
|
+
return { ...merged, tokenUsage: { input: totalInput, output: totalOutput }, skippedAgents: skippedAgents.length > 0 ? skippedAgents : undefined };
|
|
92
208
|
}
|
|
93
209
|
// Agents that produce large structured output (lists of components/connections) need more tokens
|
|
94
210
|
const MAX_TOKENS = {
|
|
@@ -82,6 +82,14 @@ export interface ValidatorOutput {
|
|
|
82
82
|
confidence: number;
|
|
83
83
|
issues: string[];
|
|
84
84
|
}
|
|
85
|
+
export interface CategorizedChanges {
|
|
86
|
+
entryPoints: string[];
|
|
87
|
+
routeFiles: string[];
|
|
88
|
+
configFiles: string[];
|
|
89
|
+
infraFiles: string[];
|
|
90
|
+
eventFiles: string[];
|
|
91
|
+
sourceFiles: string[];
|
|
92
|
+
}
|
|
85
93
|
export interface IncrementalContext {
|
|
86
94
|
existingSpec: {
|
|
87
95
|
components: Record<string, {
|
|
@@ -123,6 +131,9 @@ export interface IncrementalContext {
|
|
|
123
131
|
};
|
|
124
132
|
changedFiles: string[];
|
|
125
133
|
affectedComponents: string[];
|
|
134
|
+
categories: CategorizedChanges;
|
|
135
|
+
neighborComponents: string[];
|
|
136
|
+
hasUnmappedFiles: boolean;
|
|
126
137
|
}
|
|
127
138
|
export interface PipelineAgentResult {
|
|
128
139
|
agentId: string;
|
|
@@ -7,12 +7,12 @@ export class OpenAIProvider {
|
|
|
7
7
|
}
|
|
8
8
|
async chat(params) {
|
|
9
9
|
const messages = [
|
|
10
|
-
{ role: "
|
|
10
|
+
{ role: "developer", content: params.system },
|
|
11
11
|
...params.messages.map((m) => this.toOpenAIMessage(m)),
|
|
12
12
|
];
|
|
13
13
|
const response = await this.client.chat.completions.create({
|
|
14
|
-
model: params.model ?? "gpt-
|
|
15
|
-
|
|
14
|
+
model: params.model ?? "gpt-5.2",
|
|
15
|
+
max_completion_tokens: params.maxTokens ?? 8192,
|
|
16
16
|
messages,
|
|
17
17
|
...(params.tools?.length
|
|
18
18
|
? {
|
|
@@ -54,12 +54,12 @@ export class OpenAIProvider {
|
|
|
54
54
|
}
|
|
55
55
|
async *stream(params) {
|
|
56
56
|
const messages = [
|
|
57
|
-
{ role: "
|
|
57
|
+
{ role: "developer", content: params.system },
|
|
58
58
|
...params.messages.map((m) => this.toOpenAIMessage(m)),
|
|
59
59
|
];
|
|
60
60
|
const stream = await this.client.chat.completions.create({
|
|
61
|
-
model: params.model ?? "gpt-
|
|
62
|
-
|
|
61
|
+
model: params.model ?? "gpt-5.2",
|
|
62
|
+
max_completion_tokens: params.maxTokens ?? 8192,
|
|
63
63
|
messages,
|
|
64
64
|
stream: true,
|
|
65
65
|
...(params.tools?.length
|