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 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('--rescan', 'Incremental re-scan when git commit changes')
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
- // Add databases from service-describer as components
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 (!componentIds.has(db.id)) {
48
- components.push({
49
- id: db.id,
50
- name: db.name,
51
- type: "database",
52
- layer: "data",
53
- path: "",
54
- description: sanitize(db.description),
55
- technologies: [db.type],
56
- });
57
- componentIds.add(db.id);
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 (!componentIds.has(svc.id)) {
65
- components.push({
66
- id: svc.id,
67
- name: sanitize(svc.name) ?? svc.name,
68
- type: "service",
69
- layer: "external",
70
- path: "",
71
- description: sanitize(svc.description),
72
- technologies: [svc.type],
73
- });
74
- componentIds.add(svc.id);
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
- buildPrompt(ctx: StaticContext, priorResults?: Record<string, unknown>): {
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;
@@ -1,3 +1,2 @@
1
1
  // Pipeline Agent Framework — Types
2
- // Simpler than ArchByteAgent: no tools, just prompt → JSON
3
2
  export {};
@@ -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
- allowedTools: [],
11
- maxTurns: 1,
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 === "result") {
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
- usage = {
25
- inputTokens: message.usage.input_tokens ?? 0,
26
- outputTokens: message.usage.output_tokens ?? 0,
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: resultText }],
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-haiku-4-5-20251001",
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: "haiku",
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
- return { components: [buildSingleAppComponent(structure)] };
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 3: connection mapping (needs components + infra + events)
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;