archbyte 0.4.2 → 0.5.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.
@@ -1,596 +0,0 @@
1
- import * as path from "path";
2
- import * as fs from "fs";
3
- import { execSync } from "child_process";
4
- import chalk from "chalk";
5
- import { runValidation } from "./validate.js";
6
- import { loadPatrolIgnore } from "./shared.js";
7
- import { loadArchitectureJSON, diffArchitectures, hasStructuralChanges } from "./arch-diff.js";
8
- const PATROL_DIR = ".archbyte/patrols";
9
- /**
10
- * Convert a gitignore-style pattern to chokidar glob(s).
11
- */
12
- function toChokidarGlobs(line) {
13
- // Directory pattern: "dist/" → "**/dist/**"
14
- if (line.endsWith("/")) {
15
- return [`**/${line.slice(0, -1)}/**`];
16
- }
17
- // Glob with extension: "*.pyc" → "**/*.pyc"
18
- if (line.startsWith("*.")) {
19
- return [`**/${line}`];
20
- }
21
- // Already a glob — pass through
22
- if (line.includes("*") || line.includes("?")) {
23
- return [line];
24
- }
25
- // Plain name: treat as both file and directory
26
- return [`**/${line}`, `**/${line}/**`];
27
- }
28
- /**
29
- * Build chokidar ignored list from three layers:
30
- * 1. Baseline: hidden dirs + node_modules (always)
31
- * 2. .gitignore patterns (if present)
32
- * 3. archbyte.yaml patrol.ignore patterns (if configured)
33
- */
34
- function buildWatchIgnored(configPath) {
35
- const ignored = [
36
- /(^|[/\\])\./, // hidden files/dirs (.git, .archbyte, .venv, .next, etc.)
37
- "**/node_modules/**",
38
- ];
39
- // Layer 2: .gitignore
40
- const gitignorePath = path.join(process.cwd(), ".gitignore");
41
- if (fs.existsSync(gitignorePath)) {
42
- const lines = fs.readFileSync(gitignorePath, "utf-8").split("\n");
43
- for (const raw of lines) {
44
- const line = raw.trim();
45
- if (!line || line.startsWith("#") || line.startsWith("!"))
46
- continue;
47
- ignored.push(...toChokidarGlobs(line));
48
- }
49
- }
50
- // Layer 3: archbyte.yaml patrol.ignore
51
- const userPatterns = loadPatrolIgnore(configPath);
52
- for (const pattern of userPatterns) {
53
- ignored.push(...toChokidarGlobs(pattern));
54
- }
55
- return ignored;
56
- }
57
- const HISTORY_FILE = "history.jsonl";
58
- const LATEST_FILE = "latest.json";
59
- function parseInterval(str) {
60
- const match = str.match(/^(\d+)(s|m|h)$/);
61
- if (!match) {
62
- console.error(chalk.red(`Invalid interval: "${str}". Use format like 30s, 5m, 1h`));
63
- process.exit(1);
64
- }
65
- const [, num, unit] = match;
66
- const multipliers = { s: 1000, m: 60_000, h: 3_600_000 };
67
- return parseInt(num) * multipliers[unit];
68
- }
69
- function ensurePatrolDir() {
70
- const dir = path.join(process.cwd(), PATROL_DIR);
71
- if (!fs.existsSync(dir)) {
72
- fs.mkdirSync(dir, { recursive: true });
73
- }
74
- return dir;
75
- }
76
- function loadLatestRecord(patrolDir) {
77
- const latestPath = path.join(patrolDir, LATEST_FILE);
78
- if (!fs.existsSync(latestPath))
79
- return null;
80
- try {
81
- return JSON.parse(fs.readFileSync(latestPath, "utf-8"));
82
- }
83
- catch {
84
- return null;
85
- }
86
- }
87
- function saveRecord(patrolDir, record) {
88
- // Write latest
89
- fs.writeFileSync(path.join(patrolDir, LATEST_FILE), JSON.stringify(record, null, 2), "utf-8");
90
- // Append to history
91
- fs.appendFileSync(path.join(patrolDir, HISTORY_FILE), JSON.stringify(record) + "\n", "utf-8");
92
- }
93
- function diffIssues(previous, current) {
94
- const key = (i) => `${i.type}:${i.rule || i.metric}:${i.message}`;
95
- const prevKeys = new Set(previous.map(key));
96
- const currKeys = new Set(current.map(key));
97
- return {
98
- newIssues: current.filter((i) => !prevKeys.has(key(i))),
99
- resolvedIssues: previous.filter((i) => !currKeys.has(key(i))),
100
- };
101
- }
102
- /**
103
- * Load token usage from analysis.json metadata
104
- */
105
- function loadTokenUsage() {
106
- try {
107
- const analysisPath = path.join(process.cwd(), ".archbyte", "analysis.json");
108
- if (!fs.existsSync(analysisPath))
109
- return null;
110
- const analysis = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
111
- const tokenUsage = analysis.metadata?.tokenUsage;
112
- if (tokenUsage?.input && tokenUsage?.output) {
113
- return { input: tokenUsage.input, output: tokenUsage.output };
114
- }
115
- return null;
116
- }
117
- catch {
118
- return null;
119
- }
120
- }
121
- // ─── Git change tracking ───
122
- function gitExec(cmd, rootDir) {
123
- try {
124
- return execSync(cmd, {
125
- cwd: rootDir,
126
- encoding: "utf-8",
127
- stdio: ["pipe", "pipe", "pipe"],
128
- }).trim();
129
- }
130
- catch {
131
- return null;
132
- }
133
- }
134
- function captureGitState(rootDir, patrolDir) {
135
- const commitHash = gitExec("git rev-parse HEAD", rootDir);
136
- if (!commitHash)
137
- return undefined;
138
- const previous = loadLatestRecord(patrolDir);
139
- const previousCommitHash = previous?.git?.commitHash;
140
- let changedFiles = [];
141
- let commitCount = 0;
142
- if (previousCommitHash && previousCommitHash !== commitHash) {
143
- const countStr = gitExec(`git rev-list --count ${previousCommitHash}..HEAD`, rootDir);
144
- if (countStr)
145
- commitCount = parseInt(countStr, 10) || 0;
146
- const diff = gitExec(`git diff --name-only ${previousCommitHash}..HEAD`, rootDir);
147
- if (diff)
148
- changedFiles = diff.split("\n").filter(Boolean);
149
- }
150
- // Include uncommitted changes (staged + unstaged + untracked)
151
- const unstaged = gitExec("git diff --name-only", rootDir);
152
- if (unstaged) {
153
- for (const f of unstaged.split("\n").filter(Boolean)) {
154
- if (!changedFiles.includes(f))
155
- changedFiles.push(f);
156
- }
157
- }
158
- const staged = gitExec("git diff --name-only --cached", rootDir);
159
- if (staged) {
160
- for (const f of staged.split("\n").filter(Boolean)) {
161
- if (!changedFiles.includes(f))
162
- changedFiles.push(f);
163
- }
164
- }
165
- const untracked = gitExec("git ls-files --others --exclude-standard", rootDir);
166
- if (untracked) {
167
- for (const f of untracked.split("\n").filter(Boolean)) {
168
- if (!changedFiles.includes(f))
169
- changedFiles.push(f);
170
- }
171
- }
172
- return {
173
- commitHash,
174
- previousCommitHash,
175
- commitCount,
176
- changedFiles: changedFiles.slice(0, 100),
177
- changedFileCount: changedFiles.length,
178
- };
179
- }
180
- // ─── Structural diff → issues ───
181
- function structuralDiffToIssues(diff) {
182
- const issues = [];
183
- for (const node of diff.addedNodes) {
184
- issues.push({
185
- type: 'structural',
186
- metric: 'component-added',
187
- level: 'warn',
188
- message: `New component: "${node.label}" (${node.type}, ${node.layer})`,
189
- nodeIds: [node.id],
190
- });
191
- }
192
- for (const node of diff.removedNodes) {
193
- issues.push({
194
- type: 'structural',
195
- metric: 'component-removed',
196
- level: 'warn',
197
- message: `Removed component: "${node.label}" (${node.type}, ${node.layer})`,
198
- nodeIds: [node.id],
199
- });
200
- }
201
- for (const mod of diff.modifiedNodes) {
202
- issues.push({
203
- type: 'structural',
204
- metric: 'component-modified',
205
- level: 'warn',
206
- message: `"${mod.id}" ${mod.field}: "${mod.from}" -> "${mod.to}"`,
207
- nodeIds: [mod.id],
208
- });
209
- }
210
- if (diff.addedEdges.length > 0 || diff.removedEdges.length > 0) {
211
- issues.push({
212
- type: 'structural',
213
- metric: 'connections-changed',
214
- level: 'warn',
215
- message: `Connections: +${diff.addedEdges.length} added, -${diff.removedEdges.length} removed`,
216
- });
217
- }
218
- return issues;
219
- }
220
- // ─── Metrics from validation ───
221
- function computeMetrics(result) {
222
- return {
223
- circularDependencies: result.violations.filter((v) => v.rule === "no-circular-deps").length,
224
- highDegreeHubCount: result.violations.filter((v) => v.rule === "max-connections").length,
225
- disconnectedComponentCount: result.violations.filter((v) => v.rule === "no-orphans").length,
226
- };
227
- }
228
- /**
229
- * Run a full patrol cycle: snapshot → analyze → generate → validate → diff.
230
- */
231
- async function runPatrolCycle(options, patrolDir) {
232
- const cycleStart = Date.now();
233
- const rootDir = process.cwd();
234
- // 0. Snapshot architecture before cycle for structural diff
235
- const prevArch = loadArchitectureJSON(rootDir);
236
- // 0b. Capture git state
237
- const git = captureGitState(rootDir, patrolDir);
238
- // 1. Run full analyze (static context → LLM pipeline, incremental)
239
- try {
240
- const { handleAnalyze } = await import("./analyze.js");
241
- await handleAnalyze({ verbose: false, skipServeHint: true });
242
- }
243
- catch (err) {
244
- console.error(chalk.yellow(` Analyze failed: ${err instanceof Error ? err.message : String(err)}`));
245
- }
246
- // 2. Generate diagram from updated analysis
247
- try {
248
- const { handleGenerate } = await import("./generate.js");
249
- await handleGenerate({ verbose: false });
250
- }
251
- catch (err) {
252
- console.error(chalk.yellow(` Generate failed: ${err instanceof Error ? err.message : String(err)}`));
253
- }
254
- // 3. Compute structural diff
255
- const currArch = loadArchitectureJSON(rootDir);
256
- let structuralDiff;
257
- if (prevArch && currArch) {
258
- const diff = diffArchitectures(prevArch, currArch);
259
- if (hasStructuralChanges(diff)) {
260
- structuralDiff = diff;
261
- }
262
- }
263
- // 4. Validate (get rule violations)
264
- const previous = loadLatestRecord(patrolDir);
265
- const result = runValidation({
266
- diagram: options.diagram,
267
- config: options.config,
268
- });
269
- // Convert rule violations to audit issues
270
- const ruleIssues = result.violations.map((v) => ({
271
- type: 'rule-violation',
272
- rule: v.rule,
273
- level: v.level,
274
- message: v.message,
275
- ruleType: 'builtin',
276
- }));
277
- // Convert structural diff to audit issues
278
- const structuralIssues = structuralDiff ? structuralDiffToIssues(structuralDiff) : [];
279
- // Merge all issues
280
- const allIssues = [...ruleIssues, ...structuralIssues];
281
- // Diff against previous cycle
282
- const prevIssues = previous?.issues ?? [];
283
- const { newIssues, resolvedIssues } = diffIssues(prevIssues, allIssues);
284
- // 5. Extract token usage
285
- const tokenData = loadTokenUsage();
286
- // 6. Build record with all fields (structured + flattened for UI)
287
- const record = {
288
- timestamp: new Date().toISOString(),
289
- passed: result.errors === 0 && structuralIssues.length === 0,
290
- issues: allIssues,
291
- summary: {
292
- ruleViolations: ruleIssues.length,
293
- structuralIssues: structuralIssues.length,
294
- totalIssues: allIssues.length,
295
- },
296
- newIssues,
297
- resolvedIssues,
298
- metrics: computeMetrics(result),
299
- durationMs: Date.now() - cycleStart,
300
- tokenUsage: tokenData
301
- ? {
302
- inputTokens: tokenData.input,
303
- outputTokens: tokenData.output,
304
- totalTokens: tokenData.input + tokenData.output,
305
- }
306
- : undefined,
307
- // Structural diff
308
- structuralDiff,
309
- // Git context
310
- git,
311
- // Architecture counts
312
- totalNodes: currArch?.nodes.length ?? 0,
313
- totalEdges: currArch?.edges.length ?? 0,
314
- // Flattened for UI (backward compat)
315
- errors: result.errors,
316
- warnings: result.warnings,
317
- newViolations: newIssues.map((i) => ({
318
- rule: i.rule || i.metric || "",
319
- level: i.level,
320
- message: i.message,
321
- })),
322
- resolvedViolations: resolvedIssues.map((i) => ({
323
- rule: i.rule || i.metric || "",
324
- level: i.level,
325
- message: i.message,
326
- })),
327
- };
328
- saveRecord(patrolDir, record);
329
- return record;
330
- }
331
- function printPatrolResult(record, cycleNum) {
332
- const time = new Date(record.timestamp).toLocaleTimeString();
333
- const status = record.passed ? chalk.green("HEALTHY") : chalk.red("VIOLATION");
334
- const duration = record.durationMs ? `${(record.durationMs / 1000).toFixed(1)}s` : "?";
335
- console.log();
336
- console.log(chalk.bold.cyan(` Patrol #${cycleNum} | ${time} | ${status} | ${duration}`));
337
- console.log(chalk.gray(` Architecture: ${record.totalNodes} nodes, ${record.totalEdges} edges`));
338
- // Token usage
339
- if (record.tokenUsage) {
340
- const tokens = record.tokenUsage;
341
- console.log(chalk.gray(` Tokens: ${tokens.inputTokens.toLocaleString()} in + ${tokens.outputTokens.toLocaleString()} out = ${tokens.totalTokens.toLocaleString()} total`));
342
- }
343
- // Git changes
344
- if (record.git && (record.git.commitCount > 0 || record.git.changedFileCount > 0)) {
345
- const parts = [];
346
- if (record.git.commitCount > 0)
347
- parts.push(`${record.git.commitCount} commit(s)`);
348
- parts.push(`${record.git.changedFileCount} file(s) changed`);
349
- console.log(chalk.gray(` Git: ${parts.join(", ")}`));
350
- }
351
- // Structural changes
352
- if (record.structuralDiff && hasStructuralChanges(record.structuralDiff)) {
353
- const d = record.structuralDiff;
354
- console.log(chalk.bold.yellow(` Architecture drift detected:`));
355
- for (const n of d.addedNodes) {
356
- console.log(chalk.green(` + ${n.label} (${n.type})`));
357
- }
358
- for (const n of d.removedNodes) {
359
- console.log(chalk.red(` - ${n.label} (${n.type})`));
360
- }
361
- for (const m of d.modifiedNodes) {
362
- console.log(chalk.yellow(` ~ ${m.id}: ${m.field} "${m.from}" -> "${m.to}"`));
363
- }
364
- if (d.addedEdges.length > 0) {
365
- console.log(chalk.green(` + ${d.addedEdges.length} new connection(s)`));
366
- }
367
- if (d.removedEdges.length > 0) {
368
- console.log(chalk.red(` - ${d.removedEdges.length} removed connection(s)`));
369
- }
370
- }
371
- // New rule violations
372
- const newRuleIssues = record.newIssues.filter((i) => i.type === "rule-violation");
373
- if (newRuleIssues.length > 0) {
374
- console.log(chalk.red(` ${newRuleIssues.length} new rule violation(s):`));
375
- for (const issue of newRuleIssues) {
376
- const icon = issue.level === "error" ? chalk.red("!!") : chalk.yellow("!!");
377
- const label = issue.rule || issue.metric || "unknown";
378
- console.log(chalk.gray(` ${icon} [${label}] ${issue.message}`));
379
- }
380
- }
381
- // Resolved issues
382
- if (record.resolvedIssues.length > 0) {
383
- console.log(chalk.green(` ${record.resolvedIssues.length} resolved:`));
384
- for (const issue of record.resolvedIssues) {
385
- const label = issue.rule || issue.metric || "unknown";
386
- console.log(chalk.green(` -- [${label}] ${issue.message}`));
387
- }
388
- }
389
- // No changes at all
390
- if (record.newIssues.length === 0 &&
391
- record.resolvedIssues.length === 0 &&
392
- (!record.structuralDiff || !hasStructuralChanges(record.structuralDiff))) {
393
- console.log(chalk.gray(` No changes. ${record.summary.ruleViolations} rules, ${record.summary.structuralIssues} structural`));
394
- }
395
- }
396
- function printHistory(patrolDir) {
397
- const historyPath = path.join(patrolDir, HISTORY_FILE);
398
- if (!fs.existsSync(historyPath)) {
399
- console.log(chalk.yellow(" No patrol history found. Run archbyte patrol to start."));
400
- return;
401
- }
402
- const lines = fs.readFileSync(historyPath, "utf-8").trim().split("\n").filter(Boolean);
403
- if (lines.length === 0) {
404
- console.log(chalk.yellow(" No patrol history found."));
405
- return;
406
- }
407
- const projectName = process.cwd().split("/").pop() || "project";
408
- console.log();
409
- console.log(chalk.bold.cyan(` ArchByte Patrol History: ${projectName}`));
410
- console.log();
411
- // Show last 20 records (skip malformed lines)
412
- const records = lines.slice(-20).flatMap((l) => {
413
- try {
414
- return [JSON.parse(l)];
415
- }
416
- catch {
417
- return [];
418
- }
419
- });
420
- // Health sparkline
421
- const sparkline = records
422
- .map((r) => (r.passed ? chalk.green("*") : chalk.red("*")))
423
- .join("");
424
- console.log(` Health: ${sparkline} (last ${records.length} patrols)`);
425
- console.log();
426
- // Table
427
- console.log(chalk.gray(" Time Status Nodes Edges Rules Struct New Resolved Git"));
428
- console.log(chalk.gray(" " + "-".repeat(100)));
429
- for (const r of records) {
430
- const time = new Date(r.timestamp).toLocaleString().padEnd(20);
431
- const status = r.passed ? chalk.green("PASS ") : chalk.red("FAIL ");
432
- const nodes = String(r.totalNodes ?? "?").padEnd(6);
433
- const edges = String(r.totalEdges ?? "?").padEnd(6);
434
- const rules = String(r.summary?.ruleViolations ?? 0).padEnd(6);
435
- const structural = String(r.summary?.structuralIssues ?? 0).padEnd(8);
436
- const newV = String(r.newIssues?.length ?? 0).padEnd(5);
437
- const resolved = String(r.resolvedIssues?.length ?? 0).padEnd(10);
438
- const gitInfo = r.git
439
- ? `${r.git.commitCount}c/${r.git.changedFileCount}f`
440
- : "-";
441
- console.log(` ${time} ${status} ${nodes}${edges}${rules}${structural}${newV}${resolved}${gitInfo}`);
442
- }
443
- console.log();
444
- // Summary
445
- const totalPatrols = lines.length;
446
- const failedPatrols = records.filter((r) => !r.passed).length;
447
- const healthPct = totalPatrols > 0
448
- ? Math.round(((totalPatrols - failedPatrols) / totalPatrols) * 100)
449
- : 100;
450
- console.log(` Total patrols: ${totalPatrols} | Health rate: ${healthPct}% | Failed: ${failedPatrols}`);
451
- console.log();
452
- }
453
- function handleViolationAction(record, action) {
454
- if (record.newIssues.length === 0 && (!record.structuralDiff || !hasStructuralChanges(record.structuralDiff)))
455
- return;
456
- switch (action) {
457
- case "json":
458
- console.log(JSON.stringify({
459
- event: "patrol-issue",
460
- timestamp: record.timestamp,
461
- newIssues: record.newIssues,
462
- structuralDiff: record.structuralDiff,
463
- git: record.git,
464
- }));
465
- break;
466
- case "log":
467
- default:
468
- // Already printed in printPatrolResult
469
- break;
470
- }
471
- }
472
- /**
473
- * Run the architecture patrol daemon.
474
- * Each cycle: snapshot → analyze (incremental) → generate → validate → diff
475
- *
476
- * --once: run a single cycle then exit (used by UI "Run Now")
477
- */
478
- export async function handlePatrol(options) {
479
- const projectName = process.cwd().split("/").pop() || "project";
480
- const patrolDir = ensurePatrolDir();
481
- // History mode
482
- if (options.history) {
483
- printHistory(patrolDir);
484
- return;
485
- }
486
- const action = options.onViolation || "log";
487
- // Run initial cycle immediately
488
- let cycleNum = 1;
489
- // Watch mode: trigger patrol cycle on source file changes
490
- if (options.watch) {
491
- const chokidar = await import("chokidar");
492
- const userPatterns = loadPatrolIgnore(options.config);
493
- console.log();
494
- console.log(chalk.bold.cyan(` ArchByte Patrol: ${projectName}`));
495
- console.log(chalk.gray(` Mode: watch | On violation: ${action}`));
496
- console.log(chalk.gray(` Detects: rule violations, architecture drift, git changes`));
497
- console.log(chalk.gray(` History: archbyte patrol --history`));
498
- if (userPatterns.length > 0) {
499
- console.log(chalk.gray(` Ignoring: ${userPatterns.join(", ")} (from archbyte.yaml)`));
500
- }
501
- else {
502
- console.log(chalk.gray(` Tip: Add patrol.ignore in archbyte.yaml to exclude paths from watching`));
503
- }
504
- console.log(chalk.gray(" Press Ctrl+C to stop."));
505
- // Set up watcher FIRST — its file handles keep the event loop alive
506
- // so the process can't exit during the initial cycle.
507
- let debounceTimer = null;
508
- let running = false;
509
- const triggerCycle = () => {
510
- if (debounceTimer)
511
- clearTimeout(debounceTimer);
512
- debounceTimer = setTimeout(async () => {
513
- if (running)
514
- return;
515
- running = true;
516
- cycleNum++;
517
- try {
518
- const record = await runPatrolCycle(options, patrolDir);
519
- printPatrolResult(record, cycleNum);
520
- handleViolationAction(record, action);
521
- }
522
- catch (err) {
523
- console.error(chalk.red(` Patrol cycle #${cycleNum} failed: ${err}`));
524
- }
525
- finally {
526
- running = false;
527
- }
528
- }, 500);
529
- };
530
- const watcher = chokidar.watch(".", {
531
- cwd: process.cwd(),
532
- ignoreInitial: true,
533
- ignored: buildWatchIgnored(options.config),
534
- });
535
- watcher.on("change", triggerCycle);
536
- watcher.on("add", triggerCycle);
537
- watcher.on("unlink", triggerCycle);
538
- const shutdown = () => {
539
- watcher.close();
540
- if (debounceTimer)
541
- clearTimeout(debounceTimer);
542
- console.log();
543
- console.log(chalk.gray(` Patrol stopped after ${cycleNum} cycles.`));
544
- process.exit(0);
545
- };
546
- process.on("SIGINT", shutdown);
547
- process.on("SIGTERM", shutdown);
548
- // Now run initial cycle — watcher is already anchoring the event loop
549
- try {
550
- const record = await runPatrolCycle(options, patrolDir);
551
- printPatrolResult(record, cycleNum);
552
- handleViolationAction(record, action);
553
- }
554
- catch (err) {
555
- console.error(chalk.red(` Initial patrol cycle failed: ${err}`));
556
- }
557
- console.log(chalk.gray(" Watching for changes..."));
558
- await new Promise(() => { });
559
- return;
560
- }
561
- // Timer mode (default): poll on interval
562
- const intervalMs = parseInterval(options.interval || "5m");
563
- const intervalStr = options.interval || "5m";
564
- console.log();
565
- console.log(chalk.bold.cyan(` ArchByte Patrol: ${projectName}`));
566
- console.log(chalk.gray(` Interval: ${intervalStr} | On violation: ${action}`));
567
- console.log(chalk.gray(` Detects: rule violations, architecture drift, git changes`));
568
- console.log(chalk.gray(` History: archbyte patrol --history`));
569
- console.log(chalk.gray(" Press Ctrl+C to stop."));
570
- const record = await runPatrolCycle(options, patrolDir);
571
- printPatrolResult(record, cycleNum);
572
- handleViolationAction(record, action);
573
- // Patrol loop
574
- const timer = setInterval(async () => {
575
- cycleNum++;
576
- try {
577
- const record = await runPatrolCycle(options, patrolDir);
578
- printPatrolResult(record, cycleNum);
579
- handleViolationAction(record, action);
580
- }
581
- catch (err) {
582
- console.error(chalk.red(` Patrol cycle #${cycleNum} failed: ${err}`));
583
- }
584
- }, intervalMs);
585
- // Graceful shutdown
586
- const shutdown = () => {
587
- clearInterval(timer);
588
- console.log();
589
- console.log(chalk.gray(` Patrol stopped after ${cycleNum} cycles.`));
590
- process.exit(0);
591
- };
592
- process.on("SIGINT", shutdown);
593
- process.on("SIGTERM", shutdown);
594
- // Keep alive
595
- await new Promise(() => { });
596
- }
@@ -1,53 +0,0 @@
1
- import type { Architecture, ArchNode } from "../server/src/generator/index.js";
2
- import { type RuleLevel, type CustomRule, type CustomRuleMatcher } from "./shared.js";
3
- interface ValidateOptions {
4
- diagram?: string;
5
- config?: string;
6
- ci?: boolean;
7
- }
8
- export interface Violation {
9
- rule: string;
10
- level: RuleLevel;
11
- message: string;
12
- }
13
- /**
14
- * Check for layer bypass violations.
15
- * Returns violations where a connection skips layers (e.g. presentation → data).
16
- */
17
- export declare function checkNoLayerBypass(arch: Architecture, realNodes: ArchNode[], nodeMap: Map<string, ArchNode>, level: RuleLevel): Violation[];
18
- /**
19
- * Check for nodes exceeding the max connection threshold.
20
- */
21
- export declare function checkMaxConnections(arch: Architecture, realNodes: ArchNode[], level: RuleLevel, threshold: number): Violation[];
22
- /**
23
- * Check for orphan nodes (no connections).
24
- */
25
- export declare function checkNoOrphans(arch: Architecture, realNodes: ArchNode[], level: RuleLevel): Violation[];
26
- /**
27
- * Check for circular dependencies.
28
- */
29
- export declare function checkCircularDeps(arch: Architecture, realNodes: ArchNode[], nodeMap: Map<string, ArchNode>, level: RuleLevel): Violation[];
30
- /**
31
- * Check if a node matches a custom rule matcher.
32
- */
33
- export declare function matchesNode(node: ArchNode, matcher: CustomRuleMatcher): boolean;
34
- /**
35
- * Evaluate custom rules against the architecture.
36
- */
37
- export declare function evaluateCustomRules(arch: Architecture, realNodes: ArchNode[], nodeMap: Map<string, ArchNode>, customRules: CustomRule[]): Violation[];
38
- interface ValidationResult {
39
- violations: Violation[];
40
- errors: number;
41
- warnings: number;
42
- totalNodes: number;
43
- totalEdges: number;
44
- }
45
- /**
46
- * Core validation logic — runs all rules and returns results without side effects.
47
- */
48
- export declare function runValidation(options: ValidateOptions): ValidationResult;
49
- /**
50
- * Run architecture fitness function validation.
51
- */
52
- export declare function handleValidate(options: ValidateOptions): Promise<void>;
53
- export {};