archbyte 0.4.0 → 0.4.2
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/bin/archbyte.js +2 -20
- package/dist/agents/pipeline/merger.d.ts +2 -2
- package/dist/agents/pipeline/merger.js +152 -27
- package/dist/agents/pipeline/types.d.ts +29 -1
- package/dist/agents/pipeline/types.js +0 -1
- package/dist/agents/providers/claude-sdk.js +32 -8
- package/dist/agents/runtime/types.d.ts +4 -0
- package/dist/agents/runtime/types.js +2 -2
- package/dist/agents/static/component-detector.js +35 -3
- package/dist/agents/static/connection-mapper.d.ts +1 -1
- package/dist/agents/static/connection-mapper.js +74 -1
- package/dist/agents/static/index.js +5 -2
- package/dist/agents/static/types.d.ts +26 -0
- package/dist/cli/analyze.js +62 -19
- package/dist/cli/arch-diff.d.ts +38 -0
- package/dist/cli/arch-diff.js +61 -0
- package/dist/cli/patrol.d.ts +5 -3
- package/dist/cli/patrol.js +417 -65
- package/dist/cli/setup.js +2 -7
- package/dist/cli/shared.d.ts +11 -0
- package/dist/cli/shared.js +61 -0
- package/dist/cli/validate.d.ts +0 -1
- package/dist/cli/validate.js +0 -16
- package/dist/server/src/index.js +537 -17
- package/package.json +1 -1
- package/templates/archbyte.yaml +8 -0
- package/ui/dist/assets/index-DDCNauh7.css +1 -0
- package/ui/dist/assets/index-DO4t5Xu1.js +72 -0
- package/ui/dist/index.html +2 -2
- package/dist/cli/mcp-server.d.ts +0 -1
- package/dist/cli/mcp-server.js +0 -443
- package/dist/cli/mcp.d.ts +0 -1
- package/dist/cli/mcp.js +0 -98
- package/ui/dist/assets/index-0_XpUUZQ.css +0 -1
- package/ui/dist/assets/index-DmO1qYan.js +0 -70
package/bin/archbyte.js
CHANGED
|
@@ -151,7 +151,6 @@ program
|
|
|
151
151
|
.option('-d, --diagram <path>', 'Path to architecture JSON (default: .archbyte/architecture.json)')
|
|
152
152
|
.option('-c, --config <path>', 'Path to archbyte.yaml config')
|
|
153
153
|
.option('--ci', 'Machine-readable JSON output for CI pipelines')
|
|
154
|
-
.option('-w, --watch', 'Watch for changes and re-validate')
|
|
155
154
|
.action(async (options) => {
|
|
156
155
|
await requireLicense('analyze');
|
|
157
156
|
await handleValidate(options);
|
|
@@ -186,7 +185,8 @@ program
|
|
|
186
185
|
.option('-c, --config <path>', 'Path to archbyte.yaml config')
|
|
187
186
|
.option('-i, --interval <duration>', 'Patrol interval: 30s, 5m, 1h (default: 5m)')
|
|
188
187
|
.option('--on-violation <action>', 'Action on new violations: log, json (default: log)')
|
|
189
|
-
.option('--
|
|
188
|
+
.option('--once', 'Run a single patrol cycle then exit')
|
|
189
|
+
.option('-w, --watch', 'Watch source files for changes instead of polling on interval')
|
|
190
190
|
.option('--history', 'Show patrol history dashboard')
|
|
191
191
|
.action(async (options) => {
|
|
192
192
|
await requireLicense('analyze');
|
|
@@ -256,24 +256,6 @@ program
|
|
|
256
256
|
await handleUpdate();
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
-
// — MCP server —
|
|
260
|
-
|
|
261
|
-
const mcpCmd = program
|
|
262
|
-
.command('mcp')
|
|
263
|
-
.description('Start MCP server for AI coding tools (Claude Code, Codex)')
|
|
264
|
-
.action(async () => {
|
|
265
|
-
const { startMcpServer } = await import('../dist/cli/mcp-server.js');
|
|
266
|
-
await startMcpServer();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
mcpCmd
|
|
270
|
-
.command('install')
|
|
271
|
-
.description('Auto-configure Claude Code and/or Codex CLI')
|
|
272
|
-
.action(async () => {
|
|
273
|
-
const { handleMcpInstall } = await import('../dist/cli/mcp.js');
|
|
274
|
-
await handleMcpInstall();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
259
|
// Default: show help
|
|
278
260
|
program
|
|
279
261
|
.action(() => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { StaticAnalysisResult, StaticContext } from "../static/types.js";
|
|
2
|
-
import type { ComponentIdentifierOutput, ServiceDescriberOutput, FlowDetectorOutput, ConnectionMapperOutput, ValidatorOutput, IncrementalContext } from "./types.js";
|
|
2
|
+
import type { ComponentIdentifierOutput, ServiceDescriberOutput, FlowDetectorOutput, ConnectionMapperOutput, ValidatorOutput, IncrementalContext, ArchitectureEnricherOutput } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Merge all pipeline agent outputs into a StaticAnalysisResult
|
|
5
5
|
* compatible with the existing buildAnalysisFromStatic() in cli/analyze.ts.
|
|
6
6
|
*/
|
|
7
|
-
export declare function mergeAgentOutputs(ctx: StaticContext, componentId: ComponentIdentifierOutput | null, serviceDesc: ServiceDescriberOutput | null, flowDet: FlowDetectorOutput | null, connMap: ConnectionMapperOutput | null, validatorOut: ValidatorOutput | null, incrementalContext?: IncrementalContext): StaticAnalysisResult;
|
|
7
|
+
export declare function mergeAgentOutputs(ctx: StaticContext, componentId: ComponentIdentifierOutput | null, serviceDesc: ServiceDescriberOutput | null, flowDet: FlowDetectorOutput | null, connMap: ConnectionMapperOutput | null, validatorOut: ValidatorOutput | null, incrementalContext?: IncrementalContext, enricherOut?: ArchitectureEnricherOutput | null): StaticAnalysisResult;
|
|
@@ -5,11 +5,97 @@ function sanitize(s) {
|
|
|
5
5
|
return s;
|
|
6
6
|
return s.replace(/\u2014/g, "-").replace(/\u2013/g, "-").replace(/\u2018|\u2019/g, "'").replace(/\u201C|\u201D/g, '"');
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Build a set of "evidence tokens" from the static context — things that concretely
|
|
10
|
+
* exist in the codebase (dependencies, env vars, docker images/services).
|
|
11
|
+
* Used to gate LLM-generated databases/external services against hallucination.
|
|
12
|
+
*/
|
|
13
|
+
function buildEvidenceTokens(ctx) {
|
|
14
|
+
const tokens = new Set();
|
|
15
|
+
// Package dependencies from import map (codeSamples.importMap: file → imported modules)
|
|
16
|
+
for (const imports of Object.values(ctx.codeSamples.importMap)) {
|
|
17
|
+
for (const imp of imports) {
|
|
18
|
+
tokens.add(imp.toLowerCase());
|
|
19
|
+
// Also add short name for scoped packages: @aws-sdk/client-s3 → client-s3, aws-sdk
|
|
20
|
+
if (imp.startsWith("@")) {
|
|
21
|
+
const parts = imp.split("/");
|
|
22
|
+
if (parts[1])
|
|
23
|
+
tokens.add(parts[1].toLowerCase());
|
|
24
|
+
tokens.add(parts[0].slice(1).toLowerCase());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Config files may contain dependency info (package.json deps etc.)
|
|
29
|
+
for (const cfg of ctx.codeSamples.configFiles) {
|
|
30
|
+
if (cfg.path.endsWith("package.json")) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(cfg.content);
|
|
33
|
+
for (const dep of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
34
|
+
tokens.add(dep.toLowerCase());
|
|
35
|
+
if (dep.startsWith("@")) {
|
|
36
|
+
const parts = dep.split("/");
|
|
37
|
+
if (parts[1])
|
|
38
|
+
tokens.add(parts[1].toLowerCase());
|
|
39
|
+
tokens.add(parts[0].slice(1).toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { /* ignore parse errors */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Environment variable names
|
|
47
|
+
for (const env of ctx.envs.environments) {
|
|
48
|
+
for (const v of env.variables) {
|
|
49
|
+
tokens.add(v.toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Docker compose service names and images
|
|
53
|
+
for (const svc of ctx.infra.docker.services) {
|
|
54
|
+
tokens.add(svc.name.toLowerCase());
|
|
55
|
+
if (svc.image)
|
|
56
|
+
tokens.add(svc.image.toLowerCase().split(":")[0]);
|
|
57
|
+
}
|
|
58
|
+
// Cloud services detected by infra scanner
|
|
59
|
+
for (const s of ctx.infra.cloud.services) {
|
|
60
|
+
tokens.add(s.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
// External dependencies mentioned in docs
|
|
63
|
+
for (const dep of ctx.docs.externalDependencies) {
|
|
64
|
+
tokens.add(dep.toLowerCase());
|
|
65
|
+
}
|
|
66
|
+
return tokens;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a service/database ID and type have concrete evidence in the static context.
|
|
70
|
+
* Uses fuzzy matching: checks if any evidence token contains or is contained by the service keywords.
|
|
71
|
+
*/
|
|
72
|
+
function hasEvidence(id, name, type, evidenceTokens) {
|
|
73
|
+
// Build candidate keywords from the service
|
|
74
|
+
const candidates = [
|
|
75
|
+
id.toLowerCase(),
|
|
76
|
+
name.toLowerCase(),
|
|
77
|
+
type.toLowerCase(),
|
|
78
|
+
// Split hyphenated IDs: "aws-sqs" → ["aws", "sqs"]
|
|
79
|
+
...id.toLowerCase().split("-"),
|
|
80
|
+
].filter(Boolean);
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
for (const token of evidenceTokens) {
|
|
83
|
+
// Direct match or substring match (in both directions)
|
|
84
|
+
if (token === candidate)
|
|
85
|
+
return true;
|
|
86
|
+
if (token.includes(candidate) && candidate.length >= 3)
|
|
87
|
+
return true;
|
|
88
|
+
if (candidate.includes(token) && token.length >= 3)
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
8
94
|
/**
|
|
9
95
|
* Merge all pipeline agent outputs into a StaticAnalysisResult
|
|
10
96
|
* compatible with the existing buildAnalysisFromStatic() in cli/analyze.ts.
|
|
11
97
|
*/
|
|
12
|
-
export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMap, validatorOut, incrementalContext) {
|
|
98
|
+
export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMap, validatorOut, incrementalContext, enricherOut) {
|
|
13
99
|
// Start with components from component-identifier
|
|
14
100
|
const components = [];
|
|
15
101
|
if (componentId?.components) {
|
|
@@ -41,38 +127,44 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
|
|
|
41
127
|
}
|
|
42
128
|
}
|
|
43
129
|
const componentIds = new Set(components.map((c) => c.id));
|
|
44
|
-
//
|
|
130
|
+
// Build evidence tokens for hallucination gating
|
|
131
|
+
const evidenceTokens = buildEvidenceTokens(ctx);
|
|
132
|
+
// Add databases from service-describer as components (evidence-gated)
|
|
45
133
|
if (serviceDesc?.databases) {
|
|
46
134
|
for (const db of serviceDesc.databases) {
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
135
|
+
if (componentIds.has(db.id))
|
|
136
|
+
continue;
|
|
137
|
+
if (!hasEvidence(db.id, db.name, db.type, evidenceTokens))
|
|
138
|
+
continue;
|
|
139
|
+
components.push({
|
|
140
|
+
id: db.id,
|
|
141
|
+
name: db.name,
|
|
142
|
+
type: "database",
|
|
143
|
+
layer: "data",
|
|
144
|
+
path: "",
|
|
145
|
+
description: sanitize(db.description),
|
|
146
|
+
technologies: [db.type],
|
|
147
|
+
});
|
|
148
|
+
componentIds.add(db.id);
|
|
59
149
|
}
|
|
60
150
|
}
|
|
61
|
-
// Add external services from service-describer as components
|
|
151
|
+
// Add external services from service-describer as components (evidence-gated)
|
|
62
152
|
if (serviceDesc?.externalServices) {
|
|
63
153
|
for (const svc of serviceDesc.externalServices) {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
154
|
+
if (componentIds.has(svc.id))
|
|
155
|
+
continue;
|
|
156
|
+
if (!hasEvidence(svc.id, svc.name, svc.type, evidenceTokens))
|
|
157
|
+
continue;
|
|
158
|
+
components.push({
|
|
159
|
+
id: svc.id,
|
|
160
|
+
name: sanitize(svc.name) ?? svc.name,
|
|
161
|
+
type: "service",
|
|
162
|
+
layer: "external",
|
|
163
|
+
path: "",
|
|
164
|
+
description: sanitize(svc.description),
|
|
165
|
+
technologies: [svc.type],
|
|
166
|
+
});
|
|
167
|
+
componentIds.add(svc.id);
|
|
76
168
|
}
|
|
77
169
|
}
|
|
78
170
|
// Incremental fallback: if service-describer returned nothing, restore from spec
|
|
@@ -254,6 +346,37 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
|
|
|
254
346
|
if (serviceDesc?.projectDescription && serviceDesc.projectDescription.length > (docs.projectDescription?.length ?? 0)) {
|
|
255
347
|
docs.projectDescription = sanitize(serviceDesc.projectDescription);
|
|
256
348
|
}
|
|
349
|
+
// Apply enricher overlays (non-destructive)
|
|
350
|
+
if (enricherOut) {
|
|
351
|
+
// Overlay coupling weight on connections
|
|
352
|
+
if (enricherOut.connectionEnrichments?.length) {
|
|
353
|
+
for (const ce of enricherOut.connectionEnrichments) {
|
|
354
|
+
const conn = filteredConnections.find((c) => c.from === ce.from && c.to === ce.to && c.type === ce.type);
|
|
355
|
+
if (conn) {
|
|
356
|
+
if (ce.weight != null)
|
|
357
|
+
conn.weight = ce.weight;
|
|
358
|
+
if (ce.verifiedByTool)
|
|
359
|
+
conn.confidence = Math.min(100, (conn.confidence ?? 80) + 10);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Overlay component enrichments
|
|
364
|
+
if (enricherOut.componentEnrichments?.length) {
|
|
365
|
+
for (const ce of enricherOut.componentEnrichments) {
|
|
366
|
+
const comp = components.find((c) => c.id === ce.id);
|
|
367
|
+
if (comp) {
|
|
368
|
+
if (ce.interfaceSurface != null)
|
|
369
|
+
comp.interfaceSurface = ce.interfaceSurface;
|
|
370
|
+
if (ce.hasBoundary != null)
|
|
371
|
+
comp.hasBoundary = ce.hasBoundary;
|
|
372
|
+
if (ce.boundaryType)
|
|
373
|
+
comp.boundaryType = ce.boundaryType;
|
|
374
|
+
if (ce.publicExports?.length)
|
|
375
|
+
comp.publicExports = ce.publicExports;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
257
380
|
return {
|
|
258
381
|
structure,
|
|
259
382
|
docs,
|
|
@@ -272,5 +395,7 @@ export function mergeAgentOutputs(ctx, componentId, serviceDesc, flowDet, connMa
|
|
|
272
395
|
},
|
|
273
396
|
validation: { valid: true, repairs: [], errors: [] },
|
|
274
397
|
gaps: [],
|
|
398
|
+
enrichmentInsights: enricherOut?.insights,
|
|
399
|
+
flowVerifications: enricherOut?.flowVerifications,
|
|
275
400
|
};
|
|
276
401
|
}
|
|
@@ -5,7 +5,11 @@ export interface PipelineAgent {
|
|
|
5
5
|
name: string;
|
|
6
6
|
modelTier: ModelTier;
|
|
7
7
|
phase: "parallel" | "sequential";
|
|
8
|
-
|
|
8
|
+
/** Tool names this agent can use: ["read_file", "grep", "glob"] */
|
|
9
|
+
tools?: string[];
|
|
10
|
+
/** Max tool loop iterations (default 1 = no tools) */
|
|
11
|
+
maxTurns?: number;
|
|
12
|
+
buildPrompt(ctx: StaticContext, priorResults?: Record<string, unknown>, toolsAvailable?: boolean): {
|
|
9
13
|
system: string;
|
|
10
14
|
user: string;
|
|
11
15
|
};
|
|
@@ -135,6 +139,30 @@ export interface IncrementalContext {
|
|
|
135
139
|
neighborComponents: string[];
|
|
136
140
|
hasUnmappedFiles: boolean;
|
|
137
141
|
}
|
|
142
|
+
export interface ArchitectureEnricherOutput {
|
|
143
|
+
connectionEnrichments: Array<{
|
|
144
|
+
from: string;
|
|
145
|
+
to: string;
|
|
146
|
+
type: string;
|
|
147
|
+
weight?: number;
|
|
148
|
+
verifiedByTool?: boolean;
|
|
149
|
+
}>;
|
|
150
|
+
componentEnrichments: Array<{
|
|
151
|
+
id: string;
|
|
152
|
+
interfaceSurface?: number;
|
|
153
|
+
hasBoundary?: boolean;
|
|
154
|
+
boundaryType?: string;
|
|
155
|
+
publicExports?: string[];
|
|
156
|
+
}>;
|
|
157
|
+
flowVerifications: Array<{
|
|
158
|
+
flowName: string;
|
|
159
|
+
verified: boolean;
|
|
160
|
+
stepsVerified: number;
|
|
161
|
+
stepsTotal: number;
|
|
162
|
+
issues?: string[];
|
|
163
|
+
}>;
|
|
164
|
+
insights: string[];
|
|
165
|
+
}
|
|
138
166
|
export interface PipelineAgentResult {
|
|
139
167
|
agentId: string;
|
|
140
168
|
data: unknown;
|
|
@@ -3,32 +3,56 @@ export class ClaudeSdkProvider {
|
|
|
3
3
|
async chat(params) {
|
|
4
4
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
5
5
|
const prompt = this.extractPrompt(params.messages);
|
|
6
|
+
// Dynamic tool config: empty = text-only, populated = tool-enabled
|
|
7
|
+
const sdkTools = params.sdkTools?.length ? params.sdkTools : [];
|
|
8
|
+
const maxTurns = params.sdkMaxTurns ?? 1;
|
|
6
9
|
const result = query({
|
|
7
10
|
prompt,
|
|
8
11
|
options: {
|
|
9
12
|
systemPrompt: params.system,
|
|
10
|
-
|
|
11
|
-
maxTurns
|
|
13
|
+
tools: sdkTools,
|
|
14
|
+
maxTurns,
|
|
12
15
|
...(params.model ? { model: params.model } : {}),
|
|
13
16
|
permissionMode: "bypassPermissions",
|
|
14
17
|
allowDangerouslySkipPermissions: true,
|
|
15
18
|
},
|
|
16
19
|
});
|
|
17
20
|
let resultText = "";
|
|
21
|
+
let assistantText = "";
|
|
18
22
|
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
19
23
|
for await (const message of result) {
|
|
20
|
-
if (message.type === "
|
|
24
|
+
if (message.type === "assistant") {
|
|
25
|
+
// Capture text from assistant content blocks as fallback
|
|
26
|
+
const msg = message.message;
|
|
27
|
+
if (msg && "content" in msg && Array.isArray(msg.content)) {
|
|
28
|
+
for (const block of msg.content) {
|
|
29
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
30
|
+
assistantText += block.text;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (message.type === "result") {
|
|
21
36
|
if (message.subtype === "success") {
|
|
22
37
|
resultText = message.result;
|
|
23
38
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
else {
|
|
40
|
+
// Error result — log the errors for debugging
|
|
41
|
+
const errors = "errors" in message ? message.errors : [];
|
|
42
|
+
console.error(`[claude-sdk] Result subtype="${message.subtype}" errors=${JSON.stringify(errors)}`);
|
|
43
|
+
}
|
|
44
|
+
if (message.usage) {
|
|
45
|
+
usage = {
|
|
46
|
+
inputTokens: message.usage.input_tokens ?? 0,
|
|
47
|
+
outputTokens: message.usage.output_tokens ?? 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
28
50
|
}
|
|
29
51
|
}
|
|
52
|
+
// Use result text if available, otherwise fall back to assistant message text
|
|
53
|
+
const text = resultText || assistantText;
|
|
30
54
|
return {
|
|
31
|
-
content: [{ type: "text", text
|
|
55
|
+
content: [{ type: "text", text }],
|
|
32
56
|
stopReason: "end_turn",
|
|
33
57
|
usage,
|
|
34
58
|
};
|
|
@@ -23,6 +23,10 @@ export interface ChatParams {
|
|
|
23
23
|
messages: Message[];
|
|
24
24
|
tools?: ToolDefinition[];
|
|
25
25
|
maxTokens?: number;
|
|
26
|
+
/** Claude SDK tool names: ["Read", "Grep", "Glob"] */
|
|
27
|
+
sdkTools?: string[];
|
|
28
|
+
/** Claude SDK max agentic turns (default 1) */
|
|
29
|
+
sdkMaxTurns?: number;
|
|
26
30
|
}
|
|
27
31
|
export interface LLMResponse {
|
|
28
32
|
content: ContentBlock[];
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// === Model Routing ===
|
|
3
3
|
export const MODEL_MAP = {
|
|
4
4
|
anthropic: {
|
|
5
|
-
fast: "claude-
|
|
5
|
+
fast: "claude-sonnet-4-5-20250929",
|
|
6
6
|
standard: "claude-sonnet-4-5-20250929",
|
|
7
7
|
advanced: "claude-opus-4-6",
|
|
8
8
|
},
|
|
9
9
|
"claude-sdk": {
|
|
10
|
-
fast: "
|
|
10
|
+
fast: "sonnet",
|
|
11
11
|
standard: "sonnet",
|
|
12
12
|
advanced: "opus",
|
|
13
13
|
},
|
|
@@ -74,15 +74,21 @@ export async function detectComponents(tk, structure) {
|
|
|
74
74
|
// Strategy 1: Monorepo workspaces
|
|
75
75
|
if (structure.isMonorepo) {
|
|
76
76
|
const components = await detectFromWorkspaces(tk, structure);
|
|
77
|
-
if (components.length > 0)
|
|
77
|
+
if (components.length > 0) {
|
|
78
|
+
await enrichWithFileMetrics(tk, components);
|
|
78
79
|
return { components };
|
|
80
|
+
}
|
|
79
81
|
}
|
|
80
82
|
// Strategy 2: Scan ALL top-level directories for build configs + conventional names
|
|
81
83
|
const components = await detectAllComponents(tk, structure);
|
|
82
|
-
if (components.length > 0)
|
|
84
|
+
if (components.length > 0) {
|
|
85
|
+
await enrichWithFileMetrics(tk, components);
|
|
83
86
|
return { components };
|
|
87
|
+
}
|
|
84
88
|
// Strategy 3: Single app fallback
|
|
85
|
-
|
|
89
|
+
const fallback = [buildSingleAppComponent(structure)];
|
|
90
|
+
await enrichWithFileMetrics(tk, fallback);
|
|
91
|
+
return { components: fallback };
|
|
86
92
|
}
|
|
87
93
|
async function detectFromWorkspaces(tk, structure) {
|
|
88
94
|
// Collect workspace patterns from package.json OR pnpm-workspace.yaml
|
|
@@ -383,6 +389,32 @@ function extractTechStack(pkg) {
|
|
|
383
389
|
function capitalize(s) {
|
|
384
390
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
385
391
|
}
|
|
392
|
+
const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i;
|
|
393
|
+
async function enrichWithFileMetrics(tk, components) {
|
|
394
|
+
await Promise.all(components.map(async (comp) => {
|
|
395
|
+
if (comp.path === ".")
|
|
396
|
+
return;
|
|
397
|
+
try {
|
|
398
|
+
const allFiles = await tk.globFiles(`**/*`, comp.path);
|
|
399
|
+
const codeFiles = allFiles.filter((f) => CODE_EXTENSIONS.test(f));
|
|
400
|
+
let totalLines = 0;
|
|
401
|
+
const filesToCount = codeFiles.slice(0, 150);
|
|
402
|
+
await Promise.all(filesToCount.map(async (file) => {
|
|
403
|
+
const content = await tk.readFileSafe(file);
|
|
404
|
+
if (content)
|
|
405
|
+
totalLines += content.split("\n").length;
|
|
406
|
+
}));
|
|
407
|
+
comp.fileMetrics = {
|
|
408
|
+
fileCount: allFiles.length,
|
|
409
|
+
totalLines,
|
|
410
|
+
codeFileCount: codeFiles.length,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
// skip
|
|
415
|
+
}
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
386
418
|
function extractFirstParagraph(readme) {
|
|
387
419
|
const lines = readme.split("\n");
|
|
388
420
|
let capturing = false;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ConnectionResult, StaticComponent, InfraResult, EventResult } from "./types.js";
|
|
2
2
|
import type { StaticToolkit } from "./utils.js";
|
|
3
|
-
export declare function mapConnections(tk: StaticToolkit, components: StaticComponent[], infra: InfraResult, events: EventResult): Promise<ConnectionResult>;
|
|
3
|
+
export declare function mapConnections(tk: StaticToolkit, components: StaticComponent[], infra: InfraResult, events: EventResult, importMap?: Record<string, string[]>): Promise<ConnectionResult>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Maps connections between components via imports, Docker, K8s, env vars, known SDKs
|
|
3
3
|
// Only deterministic matches — ambiguous mappings are left as gaps for the LLM
|
|
4
4
|
import { slugify } from "./utils.js";
|
|
5
|
-
export async function mapConnections(tk, components, infra, events) {
|
|
5
|
+
export async function mapConnections(tk, components, infra, events, importMap) {
|
|
6
6
|
const connections = [];
|
|
7
7
|
const componentIds = new Set(components.map((c) => c.id));
|
|
8
8
|
// Run all detection methods in parallel
|
|
@@ -10,6 +10,7 @@ export async function mapConnections(tk, components, infra, events) {
|
|
|
10
10
|
detectDockerDependencies(infra, components, connections),
|
|
11
11
|
detectK8sIngress(infra, components, connections),
|
|
12
12
|
detectImportConnections(tk, components, connections),
|
|
13
|
+
detectImportMapConnections(components, connections, importMap ?? {}),
|
|
13
14
|
detectDatabaseConnections(tk, components, connections),
|
|
14
15
|
detectServerServesUI(tk, components, connections),
|
|
15
16
|
detectKnownSDKConnections(components, connections),
|
|
@@ -225,6 +226,78 @@ async function detectKnownSDKConnections(components, connections) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Detect cross-component connections from the code-sampler import map.
|
|
231
|
+
* Resolves relative imports and workspace package names to component boundaries.
|
|
232
|
+
*/
|
|
233
|
+
async function detectImportMapConnections(components, connections, importMap) {
|
|
234
|
+
if (Object.keys(importMap).length === 0)
|
|
235
|
+
return;
|
|
236
|
+
// Build component path → id lookup, sorted longest-first for greedy matching
|
|
237
|
+
const compPathEntries = components
|
|
238
|
+
.filter((c) => c.path !== ".")
|
|
239
|
+
.map((c) => ({ path: c.path.endsWith("/") ? c.path : c.path + "/", id: c.id }))
|
|
240
|
+
.sort((a, b) => b.path.length - a.path.length);
|
|
241
|
+
// Build package name → component id lookup (for monorepo imports)
|
|
242
|
+
const pkgNameToComp = new Map();
|
|
243
|
+
for (const comp of components) {
|
|
244
|
+
pkgNameToComp.set(comp.id, comp.id);
|
|
245
|
+
pkgNameToComp.set(comp.name, comp.id);
|
|
246
|
+
// Handle scoped package names: @org/name → name
|
|
247
|
+
const unscoped = comp.name.replace(/^@[^/]+\//, "");
|
|
248
|
+
if (unscoped !== comp.name)
|
|
249
|
+
pkgNameToComp.set(unscoped, comp.id);
|
|
250
|
+
}
|
|
251
|
+
const findCompForFile = (filePath) => {
|
|
252
|
+
for (const entry of compPathEntries) {
|
|
253
|
+
if (filePath.startsWith(entry.path) || filePath === entry.path.slice(0, -1)) {
|
|
254
|
+
return entry.id;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
};
|
|
259
|
+
// Track seen connections to avoid duplicates within this strategy
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
for (const [filePath, imports] of Object.entries(importMap)) {
|
|
262
|
+
const sourceComp = findCompForFile(filePath);
|
|
263
|
+
if (!sourceComp)
|
|
264
|
+
continue;
|
|
265
|
+
for (const imp of imports) {
|
|
266
|
+
let targetComp = null;
|
|
267
|
+
if (imp.startsWith(".") || imp.startsWith("/")) {
|
|
268
|
+
// Relative import — resolve path
|
|
269
|
+
const fromDir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
270
|
+
const parts = fromDir.split("/");
|
|
271
|
+
for (const seg of imp.split("/")) {
|
|
272
|
+
if (seg === "..")
|
|
273
|
+
parts.pop();
|
|
274
|
+
else if (seg !== ".")
|
|
275
|
+
parts.push(seg);
|
|
276
|
+
}
|
|
277
|
+
targetComp = findCompForFile(parts.join("/") + "/");
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Package/bare import — check if it maps to a workspace component
|
|
281
|
+
const name = imp.split("/").slice(0, imp.startsWith("@") ? 2 : 1).join("/");
|
|
282
|
+
targetComp = pkgNameToComp.get(name) ?? pkgNameToComp.get(name.replace(/^@[^/]+\//, "")) ?? null;
|
|
283
|
+
}
|
|
284
|
+
if (targetComp && targetComp !== sourceComp) {
|
|
285
|
+
const key = `${sourceComp}::${targetComp}`;
|
|
286
|
+
if (seen.has(key))
|
|
287
|
+
continue;
|
|
288
|
+
seen.add(key);
|
|
289
|
+
connections.push({
|
|
290
|
+
from: sourceComp,
|
|
291
|
+
to: targetComp,
|
|
292
|
+
type: "import",
|
|
293
|
+
description: `Import: ${sourceComp} → ${targetComp}`,
|
|
294
|
+
confidence: 85,
|
|
295
|
+
async: false,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
228
301
|
/**
|
|
229
302
|
* Exact match only: id, slug, path, or name must match exactly.
|
|
230
303
|
* No fuzzy matching, synonyms, or partial matches — those are for the LLM.
|
|
@@ -37,9 +37,12 @@ export async function runStaticAnalysis(projectRoot, onProgress) {
|
|
|
37
37
|
onProgress?.("Detecting components...");
|
|
38
38
|
const components = await detectComponents(tk, structure);
|
|
39
39
|
onProgress?.(`Found ${components.components.length} component(s)`);
|
|
40
|
-
// Phase
|
|
40
|
+
// Phase 2.5: collect import map for enriched connection detection
|
|
41
|
+
onProgress?.("Building import map...");
|
|
42
|
+
const codeSamples = await collectCodeSamples(tk);
|
|
43
|
+
// Phase 3: connection mapping (needs components + infra + events + import map)
|
|
41
44
|
onProgress?.("Mapping connections...");
|
|
42
|
-
const connections = await mapConnections(tk, components.components, infra, events);
|
|
45
|
+
const connections = await mapConnections(tk, components.components, infra, events, codeSamples.importMap);
|
|
43
46
|
onProgress?.(`Found ${connections.connections.length} connection(s)`);
|
|
44
47
|
// Assemble result
|
|
45
48
|
const analysis = {
|
|
@@ -32,6 +32,20 @@ export interface StaticComponent {
|
|
|
32
32
|
path: string;
|
|
33
33
|
description: string;
|
|
34
34
|
technologies: string[];
|
|
35
|
+
/** Coupling: count of public exports (from enricher) */
|
|
36
|
+
interfaceSurface?: number;
|
|
37
|
+
/** Security: has auth/permission boundary (from enricher) */
|
|
38
|
+
hasBoundary?: boolean;
|
|
39
|
+
/** Security: type of boundary e.g. "auth-middleware", "rbac" */
|
|
40
|
+
boundaryType?: string;
|
|
41
|
+
/** Key public export names (from enricher) */
|
|
42
|
+
publicExports?: string[];
|
|
43
|
+
/** File metrics from static analysis */
|
|
44
|
+
fileMetrics?: {
|
|
45
|
+
fileCount: number;
|
|
46
|
+
totalLines: number;
|
|
47
|
+
codeFileCount: number;
|
|
48
|
+
};
|
|
35
49
|
}
|
|
36
50
|
export interface InfraResult {
|
|
37
51
|
docker: {
|
|
@@ -90,6 +104,8 @@ export interface StaticConnection {
|
|
|
90
104
|
description: string;
|
|
91
105
|
confidence: number;
|
|
92
106
|
async: boolean;
|
|
107
|
+
/** Coupling weight 1-10 (from enricher) */
|
|
108
|
+
weight?: number;
|
|
93
109
|
}
|
|
94
110
|
export interface ConnectionResult {
|
|
95
111
|
connections: StaticConnection[];
|
|
@@ -130,6 +146,16 @@ export interface StaticAnalysisResult {
|
|
|
130
146
|
validation: ValidationResult;
|
|
131
147
|
/** Gaps the static analysis couldn't resolve — passed to LLM for resolution */
|
|
132
148
|
gaps: AnalysisGap[];
|
|
149
|
+
/** Free-form architectural observations from enricher */
|
|
150
|
+
enrichmentInsights?: string[];
|
|
151
|
+
/** Flow verification results from enricher */
|
|
152
|
+
flowVerifications?: Array<{
|
|
153
|
+
flowName: string;
|
|
154
|
+
verified: boolean;
|
|
155
|
+
stepsVerified: number;
|
|
156
|
+
stepsTotal: number;
|
|
157
|
+
issues?: string[];
|
|
158
|
+
}>;
|
|
133
159
|
}
|
|
134
160
|
export interface TreeEntry {
|
|
135
161
|
path: string;
|