archbyte 0.1.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/README.md +282 -0
- package/bin/archbyte.js +213 -0
- package/dist/agents/core/component-detector.d.ts +2 -0
- package/dist/agents/core/component-detector.js +57 -0
- package/dist/agents/core/connection-mapper.d.ts +2 -0
- package/dist/agents/core/connection-mapper.js +77 -0
- package/dist/agents/core/doc-parser.d.ts +2 -0
- package/dist/agents/core/doc-parser.js +64 -0
- package/dist/agents/core/env-detector.d.ts +2 -0
- package/dist/agents/core/env-detector.js +51 -0
- package/dist/agents/core/event-detector.d.ts +2 -0
- package/dist/agents/core/event-detector.js +59 -0
- package/dist/agents/core/infra-analyzer.d.ts +2 -0
- package/dist/agents/core/infra-analyzer.js +72 -0
- package/dist/agents/core/structure-scanner.d.ts +2 -0
- package/dist/agents/core/structure-scanner.js +55 -0
- package/dist/agents/core/validator.d.ts +2 -0
- package/dist/agents/core/validator.js +74 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.js +73 -0
- package/dist/agents/llm/index.d.ts +8 -0
- package/dist/agents/llm/index.js +185 -0
- package/dist/agents/llm/prompt-builder.d.ts +3 -0
- package/dist/agents/llm/prompt-builder.js +251 -0
- package/dist/agents/llm/response-parser.d.ts +6 -0
- package/dist/agents/llm/response-parser.js +174 -0
- package/dist/agents/llm/types.d.ts +31 -0
- package/dist/agents/llm/types.js +2 -0
- package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
- package/dist/agents/pipeline/agents/component-identifier.js +102 -0
- package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
- package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
- package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
- package/dist/agents/pipeline/agents/flow-detector.js +101 -0
- package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
- package/dist/agents/pipeline/agents/service-describer.js +100 -0
- package/dist/agents/pipeline/agents/validator.d.ts +3 -0
- package/dist/agents/pipeline/agents/validator.js +102 -0
- package/dist/agents/pipeline/index.d.ts +13 -0
- package/dist/agents/pipeline/index.js +128 -0
- package/dist/agents/pipeline/merger.d.ts +7 -0
- package/dist/agents/pipeline/merger.js +212 -0
- package/dist/agents/pipeline/response-parser.d.ts +5 -0
- package/dist/agents/pipeline/response-parser.js +43 -0
- package/dist/agents/pipeline/types.d.ts +92 -0
- package/dist/agents/pipeline/types.js +3 -0
- package/dist/agents/prompt-data.d.ts +1 -0
- package/dist/agents/prompt-data.js +15 -0
- package/dist/agents/prompts-encode.d.ts +9 -0
- package/dist/agents/prompts-encode.js +26 -0
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.js +30 -0
- package/dist/agents/providers/anthropic.d.ts +10 -0
- package/dist/agents/providers/anthropic.js +117 -0
- package/dist/agents/providers/google.d.ts +10 -0
- package/dist/agents/providers/google.js +136 -0
- package/dist/agents/providers/ollama.d.ts +9 -0
- package/dist/agents/providers/ollama.js +162 -0
- package/dist/agents/providers/openai.d.ts +9 -0
- package/dist/agents/providers/openai.js +142 -0
- package/dist/agents/providers/router.d.ts +7 -0
- package/dist/agents/providers/router.js +55 -0
- package/dist/agents/runtime/orchestrator.d.ts +34 -0
- package/dist/agents/runtime/orchestrator.js +193 -0
- package/dist/agents/runtime/registry.d.ts +23 -0
- package/dist/agents/runtime/registry.js +56 -0
- package/dist/agents/runtime/types.d.ts +117 -0
- package/dist/agents/runtime/types.js +29 -0
- package/dist/agents/static/code-sampler.d.ts +3 -0
- package/dist/agents/static/code-sampler.js +153 -0
- package/dist/agents/static/component-detector.d.ts +3 -0
- package/dist/agents/static/component-detector.js +404 -0
- package/dist/agents/static/connection-mapper.d.ts +3 -0
- package/dist/agents/static/connection-mapper.js +280 -0
- package/dist/agents/static/doc-parser.d.ts +3 -0
- package/dist/agents/static/doc-parser.js +358 -0
- package/dist/agents/static/env-detector.d.ts +3 -0
- package/dist/agents/static/env-detector.js +73 -0
- package/dist/agents/static/event-detector.d.ts +3 -0
- package/dist/agents/static/event-detector.js +70 -0
- package/dist/agents/static/file-tree-collector.d.ts +3 -0
- package/dist/agents/static/file-tree-collector.js +51 -0
- package/dist/agents/static/index.d.ts +19 -0
- package/dist/agents/static/index.js +307 -0
- package/dist/agents/static/infra-analyzer.d.ts +3 -0
- package/dist/agents/static/infra-analyzer.js +208 -0
- package/dist/agents/static/structure-scanner.d.ts +3 -0
- package/dist/agents/static/structure-scanner.js +195 -0
- package/dist/agents/static/types.d.ts +165 -0
- package/dist/agents/static/types.js +2 -0
- package/dist/agents/static/utils.d.ts +21 -0
- package/dist/agents/static/utils.js +146 -0
- package/dist/agents/static/validator.d.ts +2 -0
- package/dist/agents/static/validator.js +75 -0
- package/dist/agents/tools/claude-code.d.ts +38 -0
- package/dist/agents/tools/claude-code.js +129 -0
- package/dist/agents/tools/local-fs.d.ts +12 -0
- package/dist/agents/tools/local-fs.js +112 -0
- package/dist/agents/tools/tool-definitions.d.ts +6 -0
- package/dist/agents/tools/tool-definitions.js +66 -0
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +586 -0
- package/dist/cli/auth.d.ts +46 -0
- package/dist/cli/auth.js +397 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +177 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +144 -0
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +321 -0
- package/dist/cli/gate.d.ts +13 -0
- package/dist/cli/gate.js +131 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +213 -0
- package/dist/cli/license-gate.d.ts +27 -0
- package/dist/cli/license-gate.js +121 -0
- package/dist/cli/patrol.d.ts +15 -0
- package/dist/cli/patrol.js +212 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +24 -0
- package/dist/cli/serve.d.ts +9 -0
- package/dist/cli/serve.js +65 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +233 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +275 -0
- package/dist/cli/stats.d.ts +9 -0
- package/dist/cli/stats.js +158 -0
- package/dist/cli/ui.d.ts +18 -0
- package/dist/cli/ui.js +144 -0
- package/dist/cli/validate.d.ts +54 -0
- package/dist/cli/validate.js +315 -0
- package/dist/cli/workflow.d.ts +10 -0
- package/dist/cli/workflow.js +594 -0
- package/dist/server/src/generator/index.d.ts +123 -0
- package/dist/server/src/generator/index.js +254 -0
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.js +1311 -0
- package/package.json +62 -0
- package/ui/dist/assets/index-B66Til39.js +70 -0
- package/ui/dist/assets/index-BE2OWbzu.css +1 -0
- package/ui/dist/index.html +14 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// Static Analysis — Orchestrator
|
|
2
|
+
// Runs all scanners in dependency order and returns a complete StaticAnalysisResult
|
|
3
|
+
import { StaticToolkit } from "./utils.js";
|
|
4
|
+
import { scanStructure } from "./structure-scanner.js";
|
|
5
|
+
import { parseDocs } from "./doc-parser.js";
|
|
6
|
+
import { detectComponents } from "./component-detector.js";
|
|
7
|
+
import { analyzeInfra } from "./infra-analyzer.js";
|
|
8
|
+
import { detectEvents } from "./event-detector.js";
|
|
9
|
+
import { detectEnvironments } from "./env-detector.js";
|
|
10
|
+
import { mapConnections } from "./connection-mapper.js";
|
|
11
|
+
import { validateAnalysis } from "./validator.js";
|
|
12
|
+
import { collectFileTree } from "./file-tree-collector.js";
|
|
13
|
+
import { collectCodeSamples } from "./code-sampler.js";
|
|
14
|
+
export { validateAnalysis } from "./validator.js";
|
|
15
|
+
/**
|
|
16
|
+
* Run all static analysis scanners.
|
|
17
|
+
*
|
|
18
|
+
* Pipeline:
|
|
19
|
+
* 1. Parallel: structure, docs, infra, events, envs
|
|
20
|
+
* 2. Sequential: components (needs structure), connections (needs components+infra+events)
|
|
21
|
+
* 3. Validation + auto-repair
|
|
22
|
+
* 4. Gap detection — identify what the LLM should resolve
|
|
23
|
+
*/
|
|
24
|
+
export async function runStaticAnalysis(projectRoot, onProgress) {
|
|
25
|
+
const tk = new StaticToolkit(projectRoot);
|
|
26
|
+
// Phase 1: parallel scanners (no dependencies)
|
|
27
|
+
onProgress?.("Running parallel scanners...");
|
|
28
|
+
const [structure, docs, infra, events, envs] = await Promise.all([
|
|
29
|
+
scanStructure(tk),
|
|
30
|
+
parseDocs(tk),
|
|
31
|
+
analyzeInfra(tk),
|
|
32
|
+
detectEvents(tk),
|
|
33
|
+
detectEnvironments(tk),
|
|
34
|
+
]);
|
|
35
|
+
onProgress?.(`Detected: ${structure.language}, ${structure.framework ?? "no framework"}, monorepo=${structure.isMonorepo}`);
|
|
36
|
+
// Phase 2: component detection (needs structure for monorepo info)
|
|
37
|
+
onProgress?.("Detecting components...");
|
|
38
|
+
const components = await detectComponents(tk, structure);
|
|
39
|
+
onProgress?.(`Found ${components.components.length} component(s)`);
|
|
40
|
+
// Phase 3: connection mapping (needs components + infra + events)
|
|
41
|
+
onProgress?.("Mapping connections...");
|
|
42
|
+
const connections = await mapConnections(tk, components.components, infra, events);
|
|
43
|
+
onProgress?.(`Found ${connections.connections.length} connection(s)`);
|
|
44
|
+
// Assemble result
|
|
45
|
+
const analysis = {
|
|
46
|
+
structure,
|
|
47
|
+
docs,
|
|
48
|
+
components,
|
|
49
|
+
infra,
|
|
50
|
+
events,
|
|
51
|
+
envs,
|
|
52
|
+
connections,
|
|
53
|
+
validation: { valid: true, repairs: [], errors: [] },
|
|
54
|
+
gaps: [],
|
|
55
|
+
};
|
|
56
|
+
// Phase 4: validate + auto-repair
|
|
57
|
+
onProgress?.("Validating...");
|
|
58
|
+
analysis.validation = validateAnalysis(analysis);
|
|
59
|
+
if (analysis.validation.repairs.length > 0) {
|
|
60
|
+
onProgress?.(`Applied ${analysis.validation.repairs.length} auto-repair(s)`);
|
|
61
|
+
}
|
|
62
|
+
// Phase 5: identify gaps for LLM resolution
|
|
63
|
+
analysis.gaps = await collectGaps(analysis, tk);
|
|
64
|
+
if (analysis.gaps.length > 0) {
|
|
65
|
+
onProgress?.(`Identified ${analysis.gaps.length} gap(s) for LLM resolution`);
|
|
66
|
+
}
|
|
67
|
+
return analysis;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Analyze the static results and identify gaps the LLM should resolve.
|
|
71
|
+
* Fully generic — works for any project structure.
|
|
72
|
+
*/
|
|
73
|
+
async function collectGaps(analysis, tk) {
|
|
74
|
+
const gaps = [];
|
|
75
|
+
const componentIds = new Set(analysis.components.components.map((c) => c.id));
|
|
76
|
+
const connectedIds = new Set();
|
|
77
|
+
for (const c of analysis.connections.connections) {
|
|
78
|
+
connectedIds.add(c.from);
|
|
79
|
+
connectedIds.add(c.to);
|
|
80
|
+
}
|
|
81
|
+
// 1. Unknown primary language
|
|
82
|
+
if (analysis.structure.language === "unknown") {
|
|
83
|
+
gaps.push({
|
|
84
|
+
category: "unknown_primary_language",
|
|
85
|
+
description: "Could not determine the primary programming language from config files. Please infer from the component technologies and file structure.",
|
|
86
|
+
context: {
|
|
87
|
+
languages: analysis.structure.languages,
|
|
88
|
+
componentTechs: analysis.components.components.map((c) => ({
|
|
89
|
+
id: c.id,
|
|
90
|
+
technologies: c.technologies,
|
|
91
|
+
})),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// 2. Unresolved docker services — services that couldn't map to any component
|
|
96
|
+
if (analysis.infra.docker.composeFile) {
|
|
97
|
+
for (const svc of analysis.infra.docker.services) {
|
|
98
|
+
// Check if this service name or its build context maps to any component
|
|
99
|
+
const isConnected = analysis.connections.connections.some((c) => c.description.includes(svc.name));
|
|
100
|
+
const matchesComponent = analysis.components.components.some((c) => c.id === svc.name || c.path === svc.name ||
|
|
101
|
+
c.name.toLowerCase() === svc.name.toLowerCase());
|
|
102
|
+
if (!isConnected && !matchesComponent) {
|
|
103
|
+
gaps.push({
|
|
104
|
+
category: "unresolved_docker_service",
|
|
105
|
+
description: `Docker service "${svc.name}" could not be mapped to any detected component. What component does it belong to, or is it an external service (database, cache, queue)?`,
|
|
106
|
+
context: {
|
|
107
|
+
serviceName: svc.name,
|
|
108
|
+
image: svc.image,
|
|
109
|
+
buildContext: svc.buildContext,
|
|
110
|
+
ports: svc.ports,
|
|
111
|
+
dependsOn: svc.dependsOn,
|
|
112
|
+
environment: svc.environment,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// 3. Isolated components — no connections at all
|
|
119
|
+
for (const comp of analysis.components.components) {
|
|
120
|
+
if (!connectedIds.has(comp.id)) {
|
|
121
|
+
gaps.push({
|
|
122
|
+
category: "isolated_component",
|
|
123
|
+
description: `Component "${comp.name}" (${comp.id}) has no connections to other components. How does it communicate with the rest of the system?`,
|
|
124
|
+
relatedComponentIds: [comp.id],
|
|
125
|
+
context: {
|
|
126
|
+
type: comp.type,
|
|
127
|
+
path: comp.path,
|
|
128
|
+
technologies: comp.technologies,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// 4. Weak or missing descriptions
|
|
134
|
+
for (const comp of analysis.components.components) {
|
|
135
|
+
if (!comp.description || comp.description.length < 20) {
|
|
136
|
+
gaps.push({
|
|
137
|
+
category: "weak_description",
|
|
138
|
+
description: `Component "${comp.name}" has a weak or missing description. Please provide a 1-2 sentence description of its purpose and role in the system.`,
|
|
139
|
+
relatedComponentIds: [comp.id],
|
|
140
|
+
context: {
|
|
141
|
+
currentDescription: comp.description,
|
|
142
|
+
type: comp.type,
|
|
143
|
+
path: comp.path,
|
|
144
|
+
technologies: comp.technologies,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 5. Missing database — DB env vars found in components but no database component exists
|
|
150
|
+
const hasDbComponent = analysis.components.components.some((c) => c.type === "database" || c.layer === "data");
|
|
151
|
+
if (!hasDbComponent) {
|
|
152
|
+
// Check if any docker service looks like a database
|
|
153
|
+
const dbDockerServices = analysis.infra.docker.services.filter((s) => {
|
|
154
|
+
const img = (s.image ?? "").toLowerCase();
|
|
155
|
+
return img.includes("postgres") || img.includes("mysql") || img.includes("mongo") ||
|
|
156
|
+
img.includes("mariadb") || img.includes("redis") || img.includes("sqlite");
|
|
157
|
+
});
|
|
158
|
+
// Check if docs mention databases
|
|
159
|
+
const dbMentionsInDocs = analysis.docs.externalDependencies.filter((d) => /postgres|mysql|mongo|redis|sqlite|database|supabase|firebase/i.test(d));
|
|
160
|
+
// Check if any component has DB env vars (this was detected by connection-mapper)
|
|
161
|
+
const dbConnections = analysis.connections.connections.filter((c) => c.type === "database");
|
|
162
|
+
if (dbDockerServices.length > 0 || dbMentionsInDocs.length > 0 || dbConnections.length > 0) {
|
|
163
|
+
gaps.push({
|
|
164
|
+
category: "missing_database",
|
|
165
|
+
description: "Database references were found (Docker services, env vars, or docs) but no database component was detected. What databases does this project use?",
|
|
166
|
+
context: {
|
|
167
|
+
dockerDbServices: dbDockerServices.map((s) => ({ name: s.name, image: s.image })),
|
|
168
|
+
docMentions: dbMentionsInDocs,
|
|
169
|
+
dbConnectionCount: dbConnections.length,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// 6. External services mentioned in docs but not modeled
|
|
175
|
+
if (analysis.docs.externalDependencies.length > 0) {
|
|
176
|
+
const modeledExternals = new Set(analysis.components.components
|
|
177
|
+
.filter((c) => c.layer === "external")
|
|
178
|
+
.flatMap((c) => c.technologies.map((t) => t.toLowerCase())));
|
|
179
|
+
const unmodeledServices = analysis.docs.externalDependencies.filter((dep) => !modeledExternals.has(dep.toLowerCase()));
|
|
180
|
+
if (unmodeledServices.length > 0) {
|
|
181
|
+
gaps.push({
|
|
182
|
+
category: "unmodeled_external_service",
|
|
183
|
+
description: `External services mentioned in documentation but not modeled as components: ${unmodeledServices.join(", ")}. Which components use these services, and how?`,
|
|
184
|
+
context: { services: unmodeledServices },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 7. API endpoints found but not tied to connections
|
|
189
|
+
if (analysis.docs.apiEndpoints.length > 0 && analysis.connections.connections.length === 0) {
|
|
190
|
+
gaps.push({
|
|
191
|
+
category: "unmapped_api_endpoints",
|
|
192
|
+
description: `${analysis.docs.apiEndpoints.length} API endpoint(s) found in docs but no connections were detected. Which components expose and consume these APIs?`,
|
|
193
|
+
context: {
|
|
194
|
+
endpoints: analysis.docs.apiEndpoints.slice(0, 10),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// 8. Event-driven patterns detected but no event connections
|
|
199
|
+
if (analysis.events.hasEDA) {
|
|
200
|
+
const hasEventConnections = analysis.connections.connections.some((c) => c.type === "event" || c.type === "queue");
|
|
201
|
+
if (!hasEventConnections) {
|
|
202
|
+
gaps.push({
|
|
203
|
+
category: "event_pattern_no_connection",
|
|
204
|
+
description: `Event-driven patterns detected (${analysis.events.patterns.map((p) => p.technology).join(", ")}) but no event/queue connections were mapped. Which components publish and subscribe to events?`,
|
|
205
|
+
relatedComponentIds: analysis.components.components.map((c) => c.id),
|
|
206
|
+
context: {
|
|
207
|
+
patterns: analysis.events.patterns,
|
|
208
|
+
events: analysis.events.events.slice(0, 10),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 9. Ambiguous component types — heuristic: Flutter/React Native should be "frontend" not "service"
|
|
214
|
+
for (const comp of analysis.components.components) {
|
|
215
|
+
const techs = comp.technologies.map((t) => t.toLowerCase());
|
|
216
|
+
const isFrontendTech = techs.some((t) => ["flutter", "react native", "react", "vue", "angular", "svelte", "next.js", "nuxt"].includes(t));
|
|
217
|
+
if (isFrontendTech && comp.type !== "frontend") {
|
|
218
|
+
gaps.push({
|
|
219
|
+
category: "ambiguous_component_type",
|
|
220
|
+
description: `Component "${comp.name}" uses ${comp.technologies.join(", ")} (frontend technologies) but is typed as "${comp.type}". Should it be "frontend"?`,
|
|
221
|
+
relatedComponentIds: [comp.id],
|
|
222
|
+
context: {
|
|
223
|
+
currentType: comp.type,
|
|
224
|
+
technologies: comp.technologies,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// 10. Large components that may contain multiple logical modules
|
|
230
|
+
const skipForSubdirScan = new Set(["node_modules", "dist", "build", ".git", "coverage", "venv", "__pycache__", "target"]);
|
|
231
|
+
for (const comp of analysis.components.components) {
|
|
232
|
+
if (comp.path === ".")
|
|
233
|
+
continue;
|
|
234
|
+
try {
|
|
235
|
+
const entries = await tk.listDir(comp.path);
|
|
236
|
+
const subdirs = entries
|
|
237
|
+
.filter((e) => e.type === "directory" && !skipForSubdirScan.has(e.name) && !e.name.startsWith("."))
|
|
238
|
+
.map((e) => e.name);
|
|
239
|
+
if (subdirs.length >= 8) {
|
|
240
|
+
gaps.push({
|
|
241
|
+
category: "isolated_component",
|
|
242
|
+
description: `Component "${comp.name}" at ${comp.path}/ has ${subdirs.length} subdirectories that may be separate logical modules. Should any of these be their own component? List: ${subdirs.join(", ")}`,
|
|
243
|
+
relatedComponentIds: [comp.id],
|
|
244
|
+
context: {
|
|
245
|
+
subdirectories: subdirs,
|
|
246
|
+
count: subdirs.length,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Can't list dir, skip
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// 11. Top-level directories with many subdirectories not detected as components
|
|
256
|
+
// (e.g. skills/, apps/ with 50+ items that aren't workspace members)
|
|
257
|
+
const componentPaths = new Set(analysis.components.components.map((c) => c.path));
|
|
258
|
+
const topDirs = Object.keys(analysis.structure.directories);
|
|
259
|
+
for (const dir of topDirs) {
|
|
260
|
+
if (componentPaths.has(dir))
|
|
261
|
+
continue;
|
|
262
|
+
if (skipForSubdirScan.has(dir) || dir.startsWith("."))
|
|
263
|
+
continue;
|
|
264
|
+
try {
|
|
265
|
+
const entries = await tk.listDir(dir);
|
|
266
|
+
const subdirs = entries
|
|
267
|
+
.filter((e) => e.type === "directory" && !e.name.startsWith("."))
|
|
268
|
+
.map((e) => e.name);
|
|
269
|
+
if (subdirs.length >= 5) {
|
|
270
|
+
gaps.push({
|
|
271
|
+
category: "isolated_component",
|
|
272
|
+
description: `Directory "${dir}/" has ${subdirs.length} subdirectories but was not detected as a component. These may be plugins, extensions, skills, or other modules: ${subdirs.slice(0, 20).join(", ")}${subdirs.length > 20 ? ` (+${subdirs.length - 20} more)` : ""}`,
|
|
273
|
+
context: {
|
|
274
|
+
directory: dir,
|
|
275
|
+
subdirectories: subdirs.slice(0, 30),
|
|
276
|
+
count: subdirs.length,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Skip
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return gaps;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Collect raw static context from all 7 scanners.
|
|
289
|
+
* This runs ONLY fact-collectors (no component-detector, connection-mapper, or validator).
|
|
290
|
+
* Output is consumed by the pipeline LLM agents.
|
|
291
|
+
*/
|
|
292
|
+
export async function runStaticContextCollection(projectRoot, onProgress) {
|
|
293
|
+
const tk = new StaticToolkit(projectRoot);
|
|
294
|
+
onProgress?.("Collecting static context (7 scanners in parallel)...");
|
|
295
|
+
const [structure, docs, infra, events, envs, fileTree, codeSamples] = await Promise.all([
|
|
296
|
+
scanStructure(tk),
|
|
297
|
+
parseDocs(tk),
|
|
298
|
+
analyzeInfra(tk),
|
|
299
|
+
detectEvents(tk),
|
|
300
|
+
detectEnvironments(tk),
|
|
301
|
+
collectFileTree(tk),
|
|
302
|
+
collectCodeSamples(tk),
|
|
303
|
+
]);
|
|
304
|
+
onProgress?.(`Context: ${fileTree.totalFiles} files, ${fileTree.totalDirs} dirs, ${codeSamples.configFiles.length} configs, ${codeSamples.samples.length} samples`);
|
|
305
|
+
onProgress?.(`Detected: ${structure.language}, ${structure.framework ?? "no framework"}, monorepo=${structure.isMonorepo}`);
|
|
306
|
+
return { structure, docs, infra, events, envs, fileTree, codeSamples };
|
|
307
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Static Analysis — Infrastructure Analyzer
|
|
2
|
+
// Parses Docker, Kubernetes, CI/CD, and cloud infrastructure via js-yaml
|
|
3
|
+
export async function analyzeInfra(tk) {
|
|
4
|
+
const result = {
|
|
5
|
+
docker: { services: [], composeFile: false },
|
|
6
|
+
kubernetes: { resources: [] },
|
|
7
|
+
cloud: { provider: null, services: [], iac: null },
|
|
8
|
+
ci: { platform: null, pipelines: [] },
|
|
9
|
+
};
|
|
10
|
+
await Promise.all([
|
|
11
|
+
parseDockerCompose(tk, result),
|
|
12
|
+
parseKubernetes(tk, result),
|
|
13
|
+
detectCI(tk, result),
|
|
14
|
+
detectCloud(tk, result),
|
|
15
|
+
]);
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
async function parseDockerCompose(tk, result) {
|
|
19
|
+
// Try common compose file names at root and in subdirectories
|
|
20
|
+
const composeNames = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
|
|
21
|
+
let composeData = null;
|
|
22
|
+
let composeDir = "";
|
|
23
|
+
// Check root first
|
|
24
|
+
for (const name of composeNames) {
|
|
25
|
+
composeData = await tk.readYAML(name);
|
|
26
|
+
if (composeData) {
|
|
27
|
+
composeDir = "";
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// If not at root, search subdirectories
|
|
32
|
+
if (!composeData) {
|
|
33
|
+
const rootEntries = await tk.listDir(".");
|
|
34
|
+
const dirs = rootEntries.filter((e) => e.type === "directory").map((e) => e.name);
|
|
35
|
+
for (const dir of dirs) {
|
|
36
|
+
if (dir.startsWith(".") || dir === "node_modules" || dir === "dist")
|
|
37
|
+
continue;
|
|
38
|
+
for (const name of composeNames) {
|
|
39
|
+
composeData = await tk.readYAML(`${dir}/${name}`);
|
|
40
|
+
if (composeData) {
|
|
41
|
+
composeDir = dir;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (composeData)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!composeData || typeof composeData !== "object")
|
|
50
|
+
return;
|
|
51
|
+
result.docker.composeFile = true;
|
|
52
|
+
result.docker.composeFilePath = composeDir || undefined;
|
|
53
|
+
const compose = composeData;
|
|
54
|
+
const services = compose.services;
|
|
55
|
+
if (!services)
|
|
56
|
+
return;
|
|
57
|
+
for (const [name, svc] of Object.entries(services)) {
|
|
58
|
+
const ports = svc.ports?.map(String) ?? [];
|
|
59
|
+
const dependsOn = Array.isArray(svc.depends_on)
|
|
60
|
+
? svc.depends_on
|
|
61
|
+
: svc.depends_on && typeof svc.depends_on === "object"
|
|
62
|
+
? Object.keys(svc.depends_on)
|
|
63
|
+
: [];
|
|
64
|
+
const envObj = svc.environment;
|
|
65
|
+
const environment = {};
|
|
66
|
+
if (Array.isArray(envObj)) {
|
|
67
|
+
for (const e of envObj) {
|
|
68
|
+
const [k, v] = e.split("=");
|
|
69
|
+
if (k)
|
|
70
|
+
environment[k] = v ?? "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (envObj && typeof envObj === "object") {
|
|
74
|
+
Object.assign(environment, envObj);
|
|
75
|
+
}
|
|
76
|
+
// Extract build context
|
|
77
|
+
let buildContext;
|
|
78
|
+
if (svc.build) {
|
|
79
|
+
if (typeof svc.build === "string") {
|
|
80
|
+
buildContext = svc.build;
|
|
81
|
+
}
|
|
82
|
+
else if (typeof svc.build === "object") {
|
|
83
|
+
const buildObj = svc.build;
|
|
84
|
+
buildContext = buildObj.context;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
result.docker.services.push({
|
|
88
|
+
name,
|
|
89
|
+
image: svc.image,
|
|
90
|
+
buildContext,
|
|
91
|
+
ports,
|
|
92
|
+
dependsOn,
|
|
93
|
+
environment,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function parseKubernetes(tk, result) {
|
|
98
|
+
const k8sFiles = await tk.globFiles("k8s/**/*.{yml,yaml}");
|
|
99
|
+
const k8sRoot = await tk.globFiles("*.{yml,yaml}");
|
|
100
|
+
// Also check common k8s dirs
|
|
101
|
+
const moreDirs = await Promise.all([
|
|
102
|
+
tk.globFiles("kubernetes/**/*.{yml,yaml}"),
|
|
103
|
+
tk.globFiles("deploy/**/*.{yml,yaml}"),
|
|
104
|
+
tk.globFiles("manifests/**/*.{yml,yaml}"),
|
|
105
|
+
]);
|
|
106
|
+
const allFiles = [...k8sFiles, ...moreDirs.flat()];
|
|
107
|
+
// Filter out docker-compose and CI files
|
|
108
|
+
const k8sManifests = allFiles.filter((f) => !f.includes("docker-compose") && !f.includes("compose.y"));
|
|
109
|
+
for (const file of k8sManifests.slice(0, 20)) {
|
|
110
|
+
const docs = await tk.readYAMLAll(file);
|
|
111
|
+
for (const doc of docs) {
|
|
112
|
+
if (!doc || typeof doc !== "object")
|
|
113
|
+
continue;
|
|
114
|
+
const resource = doc;
|
|
115
|
+
if (resource.kind && resource.metadata) {
|
|
116
|
+
const meta = resource.metadata;
|
|
117
|
+
result.kubernetes.resources.push({
|
|
118
|
+
kind: resource.kind,
|
|
119
|
+
name: meta.name ?? "unknown",
|
|
120
|
+
namespace: meta.namespace,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function detectCI(tk, result) {
|
|
127
|
+
// Check for CI configs in parallel
|
|
128
|
+
const [ghWorkflows, gitlabCI, circleci, jenkinsfile] = await Promise.all([
|
|
129
|
+
tk.globFiles(".github/workflows/*.{yml,yaml}"),
|
|
130
|
+
tk.readFileSafe(".gitlab-ci.yml"),
|
|
131
|
+
tk.readFileSafe(".circleci/config.yml"),
|
|
132
|
+
tk.readFileSafe("Jenkinsfile"),
|
|
133
|
+
]);
|
|
134
|
+
if (ghWorkflows.length > 0) {
|
|
135
|
+
result.ci.platform = "GitHub Actions";
|
|
136
|
+
result.ci.pipelines = ghWorkflows.map((f) => f.replace(".github/workflows/", "").replace(/\.ya?ml$/, ""));
|
|
137
|
+
}
|
|
138
|
+
else if (gitlabCI) {
|
|
139
|
+
result.ci.platform = "GitLab CI";
|
|
140
|
+
result.ci.pipelines = ["gitlab-ci"];
|
|
141
|
+
}
|
|
142
|
+
else if (circleci) {
|
|
143
|
+
result.ci.platform = "CircleCI";
|
|
144
|
+
result.ci.pipelines = ["circleci"];
|
|
145
|
+
}
|
|
146
|
+
else if (jenkinsfile) {
|
|
147
|
+
result.ci.platform = "Jenkins";
|
|
148
|
+
result.ci.pipelines = ["Jenkinsfile"];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function detectCloud(tk, result) {
|
|
152
|
+
// Check for IaC and cloud config in parallel
|
|
153
|
+
const [terraformFiles, wranglerToml, vercelJson, netlifyToml, awsCdk, samTemplate, serverlessYml,] = await Promise.all([
|
|
154
|
+
tk.globFiles("**/*.tf"),
|
|
155
|
+
tk.readFileSafe("wrangler.toml"),
|
|
156
|
+
tk.readJSON("vercel.json"),
|
|
157
|
+
tk.readFileSafe("netlify.toml"),
|
|
158
|
+
tk.readJSON("cdk.json"),
|
|
159
|
+
tk.readYAML("template.yaml"), // AWS SAM
|
|
160
|
+
tk.readYAML("serverless.yml"),
|
|
161
|
+
]);
|
|
162
|
+
if (terraformFiles.length > 0) {
|
|
163
|
+
result.cloud.iac = "Terraform";
|
|
164
|
+
// Try to detect provider from tf files
|
|
165
|
+
for (const tf of terraformFiles.slice(0, 5)) {
|
|
166
|
+
const content = await tk.readFileSafe(tf);
|
|
167
|
+
if (content?.includes("aws_")) {
|
|
168
|
+
result.cloud.provider = "AWS";
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (content?.includes("google_")) {
|
|
172
|
+
result.cloud.provider = "GCP";
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
if (content?.includes("azurerm_")) {
|
|
176
|
+
result.cloud.provider = "Azure";
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (content?.includes("digitalocean_")) {
|
|
180
|
+
result.cloud.provider = "DigitalOcean";
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (wranglerToml) {
|
|
186
|
+
result.cloud.provider = result.cloud.provider ?? "Cloudflare";
|
|
187
|
+
result.cloud.services.push("Cloudflare Workers");
|
|
188
|
+
}
|
|
189
|
+
if (vercelJson) {
|
|
190
|
+
result.cloud.provider = result.cloud.provider ?? "Vercel";
|
|
191
|
+
result.cloud.services.push("Vercel");
|
|
192
|
+
}
|
|
193
|
+
if (netlifyToml) {
|
|
194
|
+
result.cloud.provider = result.cloud.provider ?? "Netlify";
|
|
195
|
+
result.cloud.services.push("Netlify");
|
|
196
|
+
}
|
|
197
|
+
if (awsCdk) {
|
|
198
|
+
result.cloud.iac = "AWS CDK";
|
|
199
|
+
result.cloud.provider = result.cloud.provider ?? "AWS";
|
|
200
|
+
}
|
|
201
|
+
if (samTemplate) {
|
|
202
|
+
result.cloud.iac = "AWS SAM";
|
|
203
|
+
result.cloud.provider = result.cloud.provider ?? "AWS";
|
|
204
|
+
}
|
|
205
|
+
if (serverlessYml) {
|
|
206
|
+
result.cloud.iac = "Serverless Framework";
|
|
207
|
+
}
|
|
208
|
+
}
|