archbyte 0.4.2 → 0.5.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.
- package/README.md +9 -25
- package/bin/archbyte.js +6 -41
- package/dist/agents/static/component-detector.js +71 -107
- package/dist/agents/static/connection-mapper.js +24 -25
- package/dist/agents/static/deep-drill.d.ts +72 -0
- package/dist/agents/static/deep-drill.js +388 -0
- package/dist/agents/static/doc-parser.js +73 -48
- package/dist/agents/static/env-detector.js +3 -6
- package/dist/agents/static/event-detector.js +20 -26
- package/dist/agents/static/infra-analyzer.js +15 -1
- package/dist/agents/static/structure-scanner.js +56 -57
- package/dist/agents/static/taxonomy.d.ts +19 -0
- package/dist/agents/static/taxonomy.js +147 -0
- package/dist/agents/tools/local-fs.js +5 -2
- package/dist/cli/analyze.js +49 -27
- package/dist/cli/license-gate.js +47 -19
- package/dist/cli/run.js +117 -1
- package/dist/cli/setup.d.ts +6 -1
- package/dist/cli/setup.js +35 -16
- package/dist/cli/shared.d.ts +0 -11
- package/dist/cli/shared.js +0 -61
- package/dist/cli/workflow.js +8 -15
- package/dist/server/src/index.js +276 -168
- package/package.json +2 -2
- package/templates/archbyte.yaml +28 -7
- package/ui/dist/assets/index-BQouokNH.css +1 -0
- package/ui/dist/assets/index-QllGSFhe.js +72 -0
- package/ui/dist/index.html +2 -2
- package/dist/cli/arch-diff.d.ts +0 -38
- package/dist/cli/arch-diff.js +0 -61
- package/dist/cli/diff.d.ts +0 -10
- package/dist/cli/diff.js +0 -144
- package/dist/cli/patrol.d.ts +0 -18
- package/dist/cli/patrol.js +0 -596
- package/dist/cli/validate.d.ts +0 -53
- package/dist/cli/validate.js +0 -299
- package/ui/dist/assets/index-DDCNauh7.css +0 -1
- package/ui/dist/assets/index-DO4t5Xu1.js +0 -72
package/dist/cli/patrol.js
DELETED
|
@@ -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
|
-
}
|
package/dist/cli/validate.d.ts
DELETED
|
@@ -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 {};
|