archbyte 0.4.0 → 0.4.1

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.
@@ -79,25 +79,28 @@ export async function handleAnalyze(options) {
79
79
  config.apiKey = options.apiKey;
80
80
  }
81
81
  if (!config) {
82
- console.error(chalk.red("No model provider configured."));
83
- console.error();
84
- console.error(chalk.bold("Zero-config (Claude Code users):"));
85
- console.error(chalk.gray(" Install Claude Code → archbyte analyze just works"));
86
- console.error();
87
- console.error(chalk.bold("Or set up with:"));
88
- console.error(chalk.gray(" archbyte config set provider anthropic"));
89
- console.error(chalk.gray(" archbyte config set api-key sk-ant-..."));
90
- console.error();
91
- console.error(chalk.bold("Or use environment variables:"));
92
- console.error(chalk.gray(" export ARCHBYTE_PROVIDER=anthropic"));
93
- console.error(chalk.gray(" export ARCHBYTE_API_KEY=sk-ant-..."));
94
- console.error();
95
- console.error(chalk.bold("Or run without a model:"));
96
- console.error(chalk.gray(" archbyte analyze --static"));
97
- console.error();
98
- console.error(chalk.bold("Supported providers:"));
99
- console.error(chalk.gray(" anthropic, openai, google, claude-sdk"));
100
- process.exit(1);
82
+ const msg = [
83
+ chalk.red("No model provider configured."),
84
+ "",
85
+ chalk.bold("Zero-config (Claude Code users):"),
86
+ chalk.gray(" Install Claude Code → archbyte analyze just works"),
87
+ "",
88
+ chalk.bold("Or set up with:"),
89
+ chalk.gray(" archbyte config set provider anthropic"),
90
+ chalk.gray(" archbyte config set api-key sk-ant-..."),
91
+ "",
92
+ chalk.bold("Or use environment variables:"),
93
+ chalk.gray(" export ARCHBYTE_PROVIDER=anthropic"),
94
+ chalk.gray(" export ARCHBYTE_API_KEY=sk-ant-..."),
95
+ "",
96
+ chalk.bold("Or run without a model:"),
97
+ chalk.gray(" archbyte analyze --static"),
98
+ "",
99
+ chalk.bold("Supported providers:"),
100
+ chalk.gray(" anthropic, openai, google, claude-sdk"),
101
+ ].join("\n");
102
+ console.error(msg);
103
+ throw new Error("No model provider configured");
101
104
  }
102
105
  const providerLabel = config.provider === "claude-sdk" ? "Claude Code (SDK)" : config.provider;
103
106
  console.log(chalk.gray(`Provider: ${chalk.white(providerLabel)}`));
@@ -177,6 +180,19 @@ export async function handleAnalyze(options) {
177
180
  console.log();
178
181
  }
179
182
  }
183
+ // 3b. File-tree drift detection (catches zero-commit and major project growth)
184
+ if (priorMeta && !options.force && !incrementalContext) {
185
+ const currentFileCount = countProjectFiles(rootDir);
186
+ const priorCount = priorMeta.filesScanned ?? 0;
187
+ if (currentFileCount > 0 && priorCount > 0 && currentFileCount <= priorCount * 1.5) {
188
+ // File count hasn't grown significantly — skip re-scan
189
+ console.log(chalk.green("No significant changes detected since last scan. Use --force to re-scan."));
190
+ return;
191
+ }
192
+ if (currentFileCount > priorCount) {
193
+ console.log(chalk.yellow(`File tree grew from ${priorCount} to ${currentFileCount} files — running full scan.`));
194
+ }
195
+ }
180
196
  // 4. Run static context collection → LLM pipeline
181
197
  const progress = progressBar(7);
182
198
  progress.update(0, "Collecting static context...");
@@ -328,6 +344,33 @@ function getGitCommit() {
328
344
  return undefined;
329
345
  }
330
346
  }
347
+ function countProjectFiles(rootDir) {
348
+ let count = 0;
349
+ const skip = new Set(["node_modules", ".git", ".archbyte", ".next", "dist", "build", ".cache"]);
350
+ function walk(dir, depth) {
351
+ if (depth > 4)
352
+ return;
353
+ try {
354
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
355
+ for (const entry of entries) {
356
+ if (entry.name.startsWith(".") && entry.name !== ".github")
357
+ continue;
358
+ if (skip.has(entry.name))
359
+ continue;
360
+ const full = path.join(dir, entry.name);
361
+ if (entry.isDirectory()) {
362
+ walk(full, depth + 1);
363
+ }
364
+ else if (/\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i.test(entry.name)) {
365
+ count++;
366
+ }
367
+ }
368
+ }
369
+ catch { /* permission errors, etc. */ }
370
+ }
371
+ walk(rootDir, 0);
372
+ return count;
373
+ }
331
374
  function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage, incrementalMode, skippedAgents) {
332
375
  const meta = {
333
376
  analyzedAt: new Date().toISOString(),
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Architecture diff — computes structural changes between two architecture snapshots.
3
+ * Used by patrol for change detection and by diff command for comparison.
4
+ */
5
+ import type { Architecture } from "../server/src/generator/index.js";
6
+ export interface StructuralDiff {
7
+ addedNodes: Array<{
8
+ id: string;
9
+ label: string;
10
+ type: string;
11
+ layer: string;
12
+ }>;
13
+ removedNodes: Array<{
14
+ id: string;
15
+ label: string;
16
+ type: string;
17
+ layer: string;
18
+ }>;
19
+ modifiedNodes: Array<{
20
+ id: string;
21
+ field: string;
22
+ from: string;
23
+ to: string;
24
+ }>;
25
+ addedEdges: Array<{
26
+ source: string;
27
+ target: string;
28
+ label?: string;
29
+ }>;
30
+ removedEdges: Array<{
31
+ source: string;
32
+ target: string;
33
+ label?: string;
34
+ }>;
35
+ }
36
+ export declare function diffArchitectures(prev: Architecture, curr: Architecture): StructuralDiff;
37
+ export declare function loadArchitectureJSON(rootDir: string): Architecture | null;
38
+ export declare function hasStructuralChanges(diff: StructuralDiff): boolean;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Architecture diff — computes structural changes between two architecture snapshots.
3
+ * Used by patrol for change detection and by diff command for comparison.
4
+ */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ export function diffArchitectures(prev, curr) {
8
+ const prevNodeMap = new Map();
9
+ for (const n of prev.nodes)
10
+ prevNodeMap.set(n.id, n);
11
+ const currNodeMap = new Map();
12
+ for (const n of curr.nodes)
13
+ currNodeMap.set(n.id, n);
14
+ const prevNodeIds = new Set(prev.nodes.map((n) => n.id));
15
+ const currNodeIds = new Set(curr.nodes.map((n) => n.id));
16
+ const addedNodes = curr.nodes
17
+ .filter((n) => !prevNodeIds.has(n.id))
18
+ .map((n) => ({ id: n.id, label: n.label, type: n.type, layer: n.layer }));
19
+ const removedNodes = prev.nodes
20
+ .filter((n) => !currNodeIds.has(n.id))
21
+ .map((n) => ({ id: n.id, label: n.label, type: n.type, layer: n.layer }));
22
+ const modifiedNodes = [];
23
+ for (const n of curr.nodes) {
24
+ const old = prevNodeMap.get(n.id);
25
+ if (!old)
26
+ continue;
27
+ for (const field of ["label", "type", "layer"]) {
28
+ if (old[field] !== n[field]) {
29
+ modifiedNodes.push({ id: n.id, field, from: old[field], to: n[field] });
30
+ }
31
+ }
32
+ }
33
+ const edgeKey = (e) => `${e.source}->${e.target}`;
34
+ const prevEdgeKeys = new Set(prev.edges.map(edgeKey));
35
+ const currEdgeKeys = new Set(curr.edges.map(edgeKey));
36
+ const addedEdges = curr.edges
37
+ .filter((e) => !prevEdgeKeys.has(edgeKey(e)))
38
+ .map((e) => ({ source: e.source, target: e.target, label: e.label }));
39
+ const removedEdges = prev.edges
40
+ .filter((e) => !currEdgeKeys.has(edgeKey(e)))
41
+ .map((e) => ({ source: e.source, target: e.target, label: e.label }));
42
+ return { addedNodes, removedNodes, modifiedNodes, addedEdges, removedEdges };
43
+ }
44
+ export function loadArchitectureJSON(rootDir) {
45
+ const archPath = path.join(rootDir, ".archbyte", "architecture.json");
46
+ if (!fs.existsSync(archPath))
47
+ return null;
48
+ try {
49
+ return JSON.parse(fs.readFileSync(archPath, "utf-8"));
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ export function hasStructuralChanges(diff) {
56
+ return (diff.addedNodes.length > 0 ||
57
+ diff.removedNodes.length > 0 ||
58
+ diff.modifiedNodes.length > 0 ||
59
+ diff.addedEdges.length > 0 ||
60
+ diff.removedEdges.length > 0);
61
+ }
@@ -4,13 +4,15 @@ interface PatrolOptions {
4
4
  interval?: string;
5
5
  onViolation?: string;
6
6
  daemon?: boolean;
7
+ once?: boolean;
8
+ watch?: boolean;
7
9
  history?: boolean;
8
- rescan?: boolean;
9
10
  }
10
11
  /**
11
12
  * Run the architecture patrol daemon.
12
- * Inspired by Gastown's patrol loop pattern cyclic monitoring
13
- * that detects drift and reports violations.
13
+ * Each cycle: snapshot analyze (incremental) generate → validate → diff
14
+ *
15
+ * --once: run a single cycle then exit (used by UI "Run Now")
14
16
  */
15
17
  export declare function handlePatrol(options: PatrolOptions): Promise<void>;
16
18
  export {};