archbyte 0.5.1 → 0.5.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
@@ -22,6 +22,11 @@ import { handleVersion, handleUpdate } from '../dist/cli/version.js';
22
22
  import { requireLicense } from '../dist/cli/license-gate.js';
23
23
  import { DEFAULT_PORT } from '../dist/cli/constants.js';
24
24
 
25
+ // When spawned by `archbyte serve` (internal), skip interactive license checks.
26
+ // The user already authenticated when they started the server.
27
+ const isInternal = process.env.ARCHBYTE_INTERNAL === '1';
28
+ const gate = isInternal ? async () => {} : requireLicense;
29
+
25
30
  const require = createRequire(import.meta.url);
26
31
  const { version: PKG_VERSION } = require('../package.json');
27
32
 
@@ -111,7 +116,7 @@ program
111
116
  .option('--force', 'Force full re-scan (skip incremental detection)')
112
117
  .option('--dry-run', 'Preview without running')
113
118
  .action(async (options) => {
114
- await requireLicense('analyze');
119
+ await gate('analyze');
115
120
  await handleAnalyze(options);
116
121
  });
117
122
 
@@ -122,7 +127,7 @@ program
122
127
  .option('-o, --output <path>', 'Output diagram (default: .archbyte/architecture.json)')
123
128
  .option('-v, --verbose', 'Show detailed output')
124
129
  .action(async (options) => {
125
- await requireLicense('generate');
130
+ await gate('generate');
126
131
  await handleGenerate(options);
127
132
  });
128
133
 
@@ -33,13 +33,13 @@ export const serviceDescriber = {
33
33
  parts.push(`Detected language: ${ctx.structure.language}`);
34
34
  parts.push(`Languages: ${ctx.structure.languages.join(", ") || "none"}`);
35
35
  parts.push(`Framework: ${ctx.structure.framework ?? "none"}`);
36
- // Docs
36
+ // Docs — only project description, NOT externalDependencies.
37
+ // Doc-extracted dependency mentions prime the LLM to hallucinate phantom services
38
+ // (e.g., docs mention "MCP" → LLM creates "MCP Server" component).
39
+ // The LLM should discover services from actual code evidence only.
37
40
  if (ctx.docs.projectDescription) {
38
41
  parts.push(`\nFrom docs: ${ctx.docs.projectDescription}`);
39
42
  }
40
- if (ctx.docs.externalDependencies.length > 0) {
41
- parts.push(`\nExternal dependencies mentioned: ${ctx.docs.externalDependencies.join(", ")}`);
42
- }
43
43
  // Docker services — only include if infra/config files changed (or full scan)
44
44
  if (ctx.infra.docker.composeFile && (hasInfraChanges || hasConfigChanges)) {
45
45
  const svcInfo = ctx.infra.docker.services.map((s) => {
@@ -1,5 +1,6 @@
1
1
  // Pipeline — Merger
2
2
  // Assembles all agent outputs into a StaticAnalysisResult
3
+ import { categorizeDep } from "../static/taxonomy.js";
3
4
  function sanitize(s) {
4
5
  if (!s)
5
6
  return s;
@@ -9,21 +10,24 @@ function sanitize(s) {
9
10
  * Build a set of "evidence tokens" from the static context — things that concretely
10
11
  * exist in the codebase (dependencies, env vars, docker images/services).
11
12
  * Used to gate LLM-generated databases/external services against hallucination.
13
+ *
14
+ * Uses the package taxonomy to resolve package names to their display names
15
+ * (e.g., "pg" → also adds "postgresql", "stripe" → also adds "stripe").
16
+ * This lets the LLM use human-readable names while still requiring code evidence.
12
17
  */
13
18
  function buildEvidenceTokens(ctx) {
14
19
  const tokens = new Set();
20
+ /** Add a dependency name + its taxonomy display name as tokens. */
21
+ function addDep(dep) {
22
+ tokens.add(dep.toLowerCase());
23
+ const cat = categorizeDep(dep);
24
+ if (cat)
25
+ tokens.add(cat.displayName.toLowerCase());
26
+ }
15
27
  // Package dependencies from import map (codeSamples.importMap: file → imported modules)
16
28
  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
- }
29
+ for (const imp of imports)
30
+ addDep(imp);
27
31
  }
28
32
  // Config files may contain dependency info (package.json deps etc.)
29
33
  for (const cfg of ctx.codeSamples.configFiles) {
@@ -31,13 +35,7 @@ function buildEvidenceTokens(ctx) {
31
35
  try {
32
36
  const pkg = JSON.parse(cfg.content);
33
37
  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
- }
38
+ addDep(dep);
41
39
  }
42
40
  }
43
41
  catch { /* ignore parse errors */ }
@@ -59,35 +57,29 @@ function buildEvidenceTokens(ctx) {
59
57
  for (const s of ctx.infra.cloud.services) {
60
58
  tokens.add(s.toLowerCase());
61
59
  }
62
- // External dependencies mentioned in docs
63
- for (const dep of ctx.docs.externalDependencies) {
64
- tokens.add(dep.toLowerCase());
65
- }
60
+ // NOTE: ctx.docs.externalDependencies intentionally excluded.
61
+ // Doc mentions (from markdown/README) are not concrete code evidence and cause
62
+ // hallucination — the LLM sees "MCP" in docs and creates phantom components.
63
+ // Only code-level signals (imports, deps, env vars, Docker, cloud) count.
66
64
  return tokens;
67
65
  }
68
66
  /**
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.
67
+ * Check if a service/database has concrete evidence in the static context.
68
+ * Strict exact-match only no substring/regex fuzzy matching.
69
+ * The taxonomy enriches evidence tokens with display names (pg → PostgreSQL)
70
+ * so the LLM can use human-readable names and still match.
71
71
  */
72
72
  function hasEvidence(id, name, type, evidenceTokens) {
73
- // Build candidate keywords from the service
74
73
  const candidates = [
75
74
  id.toLowerCase(),
76
75
  name.toLowerCase(),
77
76
  type.toLowerCase(),
78
- // Split hyphenated IDs: "aws-sqs" → ["aws", "sqs"]
77
+ // Split hyphenated IDs: "aws-sqs" → also check "aws", "sqs"
79
78
  ...id.toLowerCase().split("-"),
80
79
  ].filter(Boolean);
81
80
  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
- }
81
+ if (evidenceTokens.has(candidate))
82
+ return true;
91
83
  }
92
84
  return false;
93
85
  }
@@ -50,10 +50,11 @@ export async function handleAnalyze(options) {
50
50
  progress.update(1, "Building analysis...");
51
51
  const freshAnalysis = buildAnalysisFromStatic(result, rootDir);
52
52
  const duration = Date.now() - startTime;
53
- // Merge into existing analysis if it was produced by an agentic run,
53
+ // Merge into existing data if it was produced by an agentic run,
54
54
  // preserving LLM-generated components/connections while refreshing
55
55
  // static data (environments, metadata, project info).
56
56
  const existingAnalysis = loadExistingAnalysis(rootDir);
57
+ const existingSpec = loadSpec(rootDir);
57
58
  const wasAgentic = existingAnalysis && existingAnalysis.metadata?.mode !== "static";
58
59
  const analysis = wasAgentic ? mergeStaticIntoExisting(existingAnalysis, freshAnalysis) : freshAnalysis;
59
60
  // Stamp scan metadata on analysis.json (backward compat)
@@ -62,10 +63,19 @@ export async function handleAnalyze(options) {
62
63
  ameta.mode = wasAgentic ? "static-refresh" : "static";
63
64
  writeAnalysis(rootDir, analysis);
64
65
  // Dual-write: archbyte.yaml + metadata.json
65
- const existingSpec = loadSpec(rootDir);
66
- const spec = staticResultToSpec(result, rootDir, existingSpec?.rules);
67
- writeSpec(rootDir, spec);
68
- writeScanMetadata(rootDir, duration, "static");
66
+ // When prior data came from an agentic run, only refresh static fields
67
+ // (project info, environments) — never overwrite LLM components/connections.
68
+ if (wasAgentic && existingSpec) {
69
+ const freshSpec = staticResultToSpec(result, rootDir, existingSpec.rules);
70
+ existingSpec.project = freshSpec.project;
71
+ existingSpec.environments = freshSpec.environments;
72
+ writeSpec(rootDir, existingSpec);
73
+ }
74
+ else {
75
+ const spec = staticResultToSpec(result, rootDir, existingSpec?.rules);
76
+ writeSpec(rootDir, spec);
77
+ }
78
+ writeScanMetadata(rootDir, duration, wasAgentic ? "static-refresh" : "static");
69
79
  progress.update(2, "Generating diagram...");
70
80
  await autoGenerate(rootDir, options);
71
81
  progress.done("Analysis complete");
@@ -138,6 +138,11 @@ function createHttpServer() {
138
138
  return;
139
139
  }
140
140
  const url = req.url || "/";
141
+ // Log API actions to terminal (skip static files, SSE, health checks)
142
+ if (url.startsWith("/api/") && url !== "/api/health") {
143
+ const label = url.replace("/api/", "").split("?")[0];
144
+ console.error(`[archbyte] ${req.method} ${label}`);
145
+ }
141
146
  // SSE endpoint
142
147
  if (url === "/events") {
143
148
  res.writeHead(200, {
@@ -1129,7 +1134,7 @@ function createHttpServer() {
1129
1134
  const child = spawn(process.execPath, [bin, "generate"], {
1130
1135
  cwd: config.workspaceRoot,
1131
1136
  stdio: ["ignore", "pipe", "pipe"],
1132
- env: { ...process.env, FORCE_COLOR: "0" },
1137
+ env: { ...process.env, FORCE_COLOR: "0", ARCHBYTE_INTERNAL: "1" },
1133
1138
  });
1134
1139
  runningWorkflows.set("__generate__", child);
1135
1140
  broadcastOpsEvent({ type: "generate:started" });
@@ -1221,7 +1226,7 @@ function createHttpServer() {
1221
1226
  const child = spawn(process.execPath, [bin, "workflow", "--run", id], {
1222
1227
  cwd: config.workspaceRoot,
1223
1228
  stdio: ["ignore", "pipe", "pipe"],
1224
- env: { ...process.env, FORCE_COLOR: "0" },
1229
+ env: { ...process.env, FORCE_COLOR: "0", ARCHBYTE_INTERNAL: "1" },
1225
1230
  });
1226
1231
  runningWorkflows.set(id, child);
1227
1232
  broadcastOpsEvent({ type: "workflow:started", id });
@@ -1452,6 +1457,22 @@ function createHttpServer() {
1452
1457
  }
1453
1458
  return;
1454
1459
  }
1460
+ // API: UI event logging — fire-and-forget from UI to server console
1461
+ if (url === "/api/ui-event" && req.method === "POST") {
1462
+ let body = "";
1463
+ req.on("data", (chunk) => { body += chunk.toString(); });
1464
+ req.on("end", () => {
1465
+ try {
1466
+ const { event, detail } = JSON.parse(body);
1467
+ const msg = detail ? `${event} — ${detail}` : event;
1468
+ console.error(`[archbyte] ui: ${msg}`);
1469
+ }
1470
+ catch { }
1471
+ res.writeHead(204);
1472
+ res.end();
1473
+ });
1474
+ return;
1475
+ }
1455
1476
  // API: Telemetry — record agent timing data
1456
1477
  if (url === "/api/telemetry" && req.method === "POST") {
1457
1478
  let body = "";
@@ -1717,7 +1738,7 @@ function runAnalyzePipeline(mode = "static", fileChanges) {
1717
1738
  const analyzeChild = spawn(process.execPath, analyzeArgs, {
1718
1739
  cwd: config.workspaceRoot,
1719
1740
  stdio: ["ignore", "pipe", "pipe"],
1720
- env: { ...process.env, FORCE_COLOR: "0" },
1741
+ env: { ...process.env, FORCE_COLOR: "0", ARCHBYTE_INTERNAL: "1" },
1721
1742
  });
1722
1743
  let analyzeStderr = "";
1723
1744
  analyzeChild.stdout?.on("data", (d) => process.stderr.write(`[analyze] ${d}`));
@@ -1733,7 +1754,7 @@ function runAnalyzePipeline(mode = "static", fileChanges) {
1733
1754
  const genChild = spawn(process.execPath, [bin, "generate"], {
1734
1755
  cwd: config.workspaceRoot,
1735
1756
  stdio: ["ignore", "pipe", "pipe"],
1736
- env: { ...process.env, FORCE_COLOR: "0" },
1757
+ env: { ...process.env, FORCE_COLOR: "0", ARCHBYTE_INTERNAL: "1" },
1737
1758
  });
1738
1759
  genChild.stdout?.on("data", (d) => process.stderr.write(`[generate] ${d}`));
1739
1760
  genChild.stderr?.on("data", (d) => process.stderr.write(`[generate] ${d}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "ArchByte - See what agents build. As they build it.",
5
5
  "type": "module",
6
6
  "bin": {