archbyte 0.1.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.
- package/README.md +282 -0
- package/bin/archbyte.js +213 -0
- package/dist/agents/core/component-detector.d.ts +2 -0
- package/dist/agents/core/component-detector.js +57 -0
- package/dist/agents/core/connection-mapper.d.ts +2 -0
- package/dist/agents/core/connection-mapper.js +77 -0
- package/dist/agents/core/doc-parser.d.ts +2 -0
- package/dist/agents/core/doc-parser.js +64 -0
- package/dist/agents/core/env-detector.d.ts +2 -0
- package/dist/agents/core/env-detector.js +51 -0
- package/dist/agents/core/event-detector.d.ts +2 -0
- package/dist/agents/core/event-detector.js +59 -0
- package/dist/agents/core/infra-analyzer.d.ts +2 -0
- package/dist/agents/core/infra-analyzer.js +72 -0
- package/dist/agents/core/structure-scanner.d.ts +2 -0
- package/dist/agents/core/structure-scanner.js +55 -0
- package/dist/agents/core/validator.d.ts +2 -0
- package/dist/agents/core/validator.js +74 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.js +73 -0
- package/dist/agents/llm/index.d.ts +8 -0
- package/dist/agents/llm/index.js +185 -0
- package/dist/agents/llm/prompt-builder.d.ts +3 -0
- package/dist/agents/llm/prompt-builder.js +251 -0
- package/dist/agents/llm/response-parser.d.ts +6 -0
- package/dist/agents/llm/response-parser.js +174 -0
- package/dist/agents/llm/types.d.ts +31 -0
- package/dist/agents/llm/types.js +2 -0
- package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
- package/dist/agents/pipeline/agents/component-identifier.js +102 -0
- package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
- package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
- package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
- package/dist/agents/pipeline/agents/flow-detector.js +101 -0
- package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
- package/dist/agents/pipeline/agents/service-describer.js +100 -0
- package/dist/agents/pipeline/agents/validator.d.ts +3 -0
- package/dist/agents/pipeline/agents/validator.js +102 -0
- package/dist/agents/pipeline/index.d.ts +13 -0
- package/dist/agents/pipeline/index.js +128 -0
- package/dist/agents/pipeline/merger.d.ts +7 -0
- package/dist/agents/pipeline/merger.js +212 -0
- package/dist/agents/pipeline/response-parser.d.ts +5 -0
- package/dist/agents/pipeline/response-parser.js +43 -0
- package/dist/agents/pipeline/types.d.ts +92 -0
- package/dist/agents/pipeline/types.js +3 -0
- package/dist/agents/prompt-data.d.ts +1 -0
- package/dist/agents/prompt-data.js +15 -0
- package/dist/agents/prompts-encode.d.ts +9 -0
- package/dist/agents/prompts-encode.js +26 -0
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.js +30 -0
- package/dist/agents/providers/anthropic.d.ts +10 -0
- package/dist/agents/providers/anthropic.js +117 -0
- package/dist/agents/providers/google.d.ts +10 -0
- package/dist/agents/providers/google.js +136 -0
- package/dist/agents/providers/ollama.d.ts +9 -0
- package/dist/agents/providers/ollama.js +162 -0
- package/dist/agents/providers/openai.d.ts +9 -0
- package/dist/agents/providers/openai.js +142 -0
- package/dist/agents/providers/router.d.ts +7 -0
- package/dist/agents/providers/router.js +55 -0
- package/dist/agents/runtime/orchestrator.d.ts +34 -0
- package/dist/agents/runtime/orchestrator.js +193 -0
- package/dist/agents/runtime/registry.d.ts +23 -0
- package/dist/agents/runtime/registry.js +56 -0
- package/dist/agents/runtime/types.d.ts +117 -0
- package/dist/agents/runtime/types.js +29 -0
- package/dist/agents/static/code-sampler.d.ts +3 -0
- package/dist/agents/static/code-sampler.js +153 -0
- package/dist/agents/static/component-detector.d.ts +3 -0
- package/dist/agents/static/component-detector.js +404 -0
- package/dist/agents/static/connection-mapper.d.ts +3 -0
- package/dist/agents/static/connection-mapper.js +280 -0
- package/dist/agents/static/doc-parser.d.ts +3 -0
- package/dist/agents/static/doc-parser.js +358 -0
- package/dist/agents/static/env-detector.d.ts +3 -0
- package/dist/agents/static/env-detector.js +73 -0
- package/dist/agents/static/event-detector.d.ts +3 -0
- package/dist/agents/static/event-detector.js +70 -0
- package/dist/agents/static/file-tree-collector.d.ts +3 -0
- package/dist/agents/static/file-tree-collector.js +51 -0
- package/dist/agents/static/index.d.ts +19 -0
- package/dist/agents/static/index.js +307 -0
- package/dist/agents/static/infra-analyzer.d.ts +3 -0
- package/dist/agents/static/infra-analyzer.js +208 -0
- package/dist/agents/static/structure-scanner.d.ts +3 -0
- package/dist/agents/static/structure-scanner.js +195 -0
- package/dist/agents/static/types.d.ts +165 -0
- package/dist/agents/static/types.js +2 -0
- package/dist/agents/static/utils.d.ts +21 -0
- package/dist/agents/static/utils.js +146 -0
- package/dist/agents/static/validator.d.ts +2 -0
- package/dist/agents/static/validator.js +75 -0
- package/dist/agents/tools/claude-code.d.ts +38 -0
- package/dist/agents/tools/claude-code.js +129 -0
- package/dist/agents/tools/local-fs.d.ts +12 -0
- package/dist/agents/tools/local-fs.js +112 -0
- package/dist/agents/tools/tool-definitions.d.ts +6 -0
- package/dist/agents/tools/tool-definitions.js +66 -0
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +586 -0
- package/dist/cli/auth.d.ts +46 -0
- package/dist/cli/auth.js +397 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +177 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +144 -0
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +321 -0
- package/dist/cli/gate.d.ts +13 -0
- package/dist/cli/gate.js +131 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +213 -0
- package/dist/cli/license-gate.d.ts +27 -0
- package/dist/cli/license-gate.js +121 -0
- package/dist/cli/patrol.d.ts +15 -0
- package/dist/cli/patrol.js +212 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +24 -0
- package/dist/cli/serve.d.ts +9 -0
- package/dist/cli/serve.js +65 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +233 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +275 -0
- package/dist/cli/stats.d.ts +9 -0
- package/dist/cli/stats.js +158 -0
- package/dist/cli/ui.d.ts +18 -0
- package/dist/cli/ui.js +144 -0
- package/dist/cli/validate.d.ts +54 -0
- package/dist/cli/validate.js +315 -0
- package/dist/cli/workflow.d.ts +10 -0
- package/dist/cli/workflow.js +594 -0
- package/dist/server/src/generator/index.d.ts +123 -0
- package/dist/server/src/generator/index.js +254 -0
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.js +1311 -0
- package/package.json +62 -0
- package/ui/dist/assets/index-B66Til39.js +70 -0
- package/ui/dist/assets/index-BE2OWbzu.css +1 -0
- package/ui/dist/index.html +14 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { resolveArchitecturePath, loadArchitectureFile, loadRulesConfig, getThreshold } from "./shared.js";
|
|
5
|
+
/**
|
|
6
|
+
* Print an architecture health dashboard to the terminal.
|
|
7
|
+
*/
|
|
8
|
+
export async function handleStats(options) {
|
|
9
|
+
const diagramPath = resolveArchitecturePath(options);
|
|
10
|
+
const arch = loadArchitectureFile(diagramPath);
|
|
11
|
+
const config = loadRulesConfig(options.config);
|
|
12
|
+
const realNodes = arch.nodes;
|
|
13
|
+
const components = realNodes.filter((n) => n.type === "component" || n.type === "service");
|
|
14
|
+
const databases = realNodes.filter((n) => n.type === "database");
|
|
15
|
+
const externals = realNodes.filter((n) => n.type === "external");
|
|
16
|
+
const totalConnections = arch.edges.length;
|
|
17
|
+
// Auto-detect project name from cwd
|
|
18
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.bold.cyan(`⚡ ArchByte Stats — ${projectName}`));
|
|
21
|
+
console.log();
|
|
22
|
+
// ── Scan Info (from analysis.json metadata) ──
|
|
23
|
+
const analysisPath = path.join(process.cwd(), ".archbyte", "analysis.json");
|
|
24
|
+
let scanMeta = {};
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(analysisPath)) {
|
|
27
|
+
const raw = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
|
|
28
|
+
scanMeta = (raw.metadata ?? {});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
if (scanMeta.analyzedAt || scanMeta.durationMs || scanMeta.filesScanned) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
if (scanMeta.durationMs != null) {
|
|
35
|
+
parts.push(`${(scanMeta.durationMs / 1000).toFixed(1)}s`);
|
|
36
|
+
}
|
|
37
|
+
if (scanMeta.filesScanned != null) {
|
|
38
|
+
parts.push(`${scanMeta.filesScanned} files scanned`);
|
|
39
|
+
}
|
|
40
|
+
if (scanMeta.tokenUsage) {
|
|
41
|
+
const total = scanMeta.tokenUsage.input + scanMeta.tokenUsage.output;
|
|
42
|
+
parts.push(`${total.toLocaleString()} tokens`);
|
|
43
|
+
}
|
|
44
|
+
if (scanMeta.mode) {
|
|
45
|
+
parts.push(scanMeta.mode === "pipeline" ? "static + model" : "static only");
|
|
46
|
+
}
|
|
47
|
+
if (scanMeta.analyzedAt) {
|
|
48
|
+
const d = new Date(scanMeta.analyzedAt);
|
|
49
|
+
parts.push(d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }));
|
|
50
|
+
}
|
|
51
|
+
console.log(chalk.gray(` Last scan: ${parts.join(" | ")}`));
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
// ── Summary ──
|
|
55
|
+
console.log(` Components: ${chalk.bold(String(components.length))} Connections: ${chalk.bold(String(totalConnections))}`);
|
|
56
|
+
console.log(` Databases: ${chalk.bold(String(databases.length))} Ext Services: ${chalk.bold(String(externals.length))}`);
|
|
57
|
+
console.log();
|
|
58
|
+
// ── By Layer ──
|
|
59
|
+
const layers = ["presentation", "application", "data", "external", "deployment"];
|
|
60
|
+
const layerCounts = new Map();
|
|
61
|
+
for (const node of realNodes) {
|
|
62
|
+
layerCounts.set(node.layer, (layerCounts.get(node.layer) || 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
const maxCount = Math.max(...layerCounts.values(), 1);
|
|
65
|
+
console.log(chalk.bold(" By Layer:"));
|
|
66
|
+
for (const layer of layers) {
|
|
67
|
+
const count = layerCounts.get(layer) || 0;
|
|
68
|
+
if (count === 0)
|
|
69
|
+
continue;
|
|
70
|
+
const barLen = Math.round((count / maxCount) * 10);
|
|
71
|
+
const bar = "█".repeat(barLen) + "░".repeat(10 - barLen);
|
|
72
|
+
const pct = realNodes.length > 0
|
|
73
|
+
? `(${Math.round((count / realNodes.length) * 100)}%)`
|
|
74
|
+
: "";
|
|
75
|
+
const label = (layer + " ").slice(0, 14);
|
|
76
|
+
console.log(chalk.gray(` ${label}${bar} ${count} ${pct}`));
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
// ── Health Indicators ──
|
|
80
|
+
console.log(chalk.bold(" Health Indicators:"));
|
|
81
|
+
// Connection density: edges / (n * (n-1) / 2) for undirected sense
|
|
82
|
+
const n = realNodes.length;
|
|
83
|
+
const possibleConnections = n > 1 ? (n * (n - 1)) / 2 : 1;
|
|
84
|
+
const density = totalConnections / possibleConnections;
|
|
85
|
+
const densityStr = density.toFixed(2);
|
|
86
|
+
if (density < 0.5) {
|
|
87
|
+
console.log(chalk.green(` ✓ Connection density: ${densityStr} (healthy < 0.5)`));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(chalk.yellow(` ⚠ Connection density: ${densityStr} (threshold: 0.5)`));
|
|
91
|
+
}
|
|
92
|
+
// Hub detection (threshold: 5 connections)
|
|
93
|
+
const connectionCounts = new Map();
|
|
94
|
+
for (const edge of arch.edges) {
|
|
95
|
+
connectionCounts.set(edge.source, (connectionCounts.get(edge.source) || 0) + 1);
|
|
96
|
+
connectionCounts.set(edge.target, (connectionCounts.get(edge.target) || 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
const hubThreshold = getThreshold(config, "max-connections", 6);
|
|
99
|
+
let hubFound = false;
|
|
100
|
+
for (const node of realNodes) {
|
|
101
|
+
const count = connectionCounts.get(node.id) || 0;
|
|
102
|
+
if (count >= hubThreshold) {
|
|
103
|
+
console.log(chalk.yellow(` ⚠ Hub detected: "${node.label}" has ${count} connections (threshold: ${hubThreshold})`));
|
|
104
|
+
hubFound = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!hubFound) {
|
|
108
|
+
console.log(chalk.green(` ✓ No hubs detected (threshold: ${hubThreshold})`));
|
|
109
|
+
}
|
|
110
|
+
// Orphan detection
|
|
111
|
+
const connectedIds = new Set();
|
|
112
|
+
for (const edge of arch.edges) {
|
|
113
|
+
connectedIds.add(edge.source);
|
|
114
|
+
connectedIds.add(edge.target);
|
|
115
|
+
}
|
|
116
|
+
let orphanFound = false;
|
|
117
|
+
for (const node of realNodes) {
|
|
118
|
+
if (!connectedIds.has(node.id)) {
|
|
119
|
+
console.log(chalk.yellow(` ⚠ Orphan found: "${node.label}" has 0 connections`));
|
|
120
|
+
orphanFound = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!orphanFound) {
|
|
124
|
+
console.log(chalk.green(" ✓ No orphans detected"));
|
|
125
|
+
}
|
|
126
|
+
// Layer violations (presentation → data, skipping application)
|
|
127
|
+
const layerOrder = {
|
|
128
|
+
presentation: 0,
|
|
129
|
+
application: 1,
|
|
130
|
+
data: 2,
|
|
131
|
+
external: 3,
|
|
132
|
+
deployment: 4,
|
|
133
|
+
};
|
|
134
|
+
const nodeMap = new Map();
|
|
135
|
+
for (const node of realNodes) {
|
|
136
|
+
nodeMap.set(node.id, node);
|
|
137
|
+
}
|
|
138
|
+
let violationFound = false;
|
|
139
|
+
for (const edge of arch.edges) {
|
|
140
|
+
const src = nodeMap.get(edge.source);
|
|
141
|
+
const tgt = nodeMap.get(edge.target);
|
|
142
|
+
if (!src || !tgt)
|
|
143
|
+
continue;
|
|
144
|
+
const srcOrder = layerOrder[src.layer];
|
|
145
|
+
const tgtOrder = layerOrder[tgt.layer];
|
|
146
|
+
if (srcOrder === undefined || tgtOrder === undefined)
|
|
147
|
+
continue;
|
|
148
|
+
// Only check the core layers (presentation/application/data)
|
|
149
|
+
if (srcOrder <= 1 && tgtOrder === 2 && tgtOrder - srcOrder > 1) {
|
|
150
|
+
console.log(chalk.yellow(` ⚠ Layer violation: "${src.label}" (${src.layer}) → "${tgt.label}" (${tgt.layer})`));
|
|
151
|
+
violationFound = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!violationFound) {
|
|
155
|
+
console.log(chalk.green(" ✓ No layer violations detected"));
|
|
156
|
+
}
|
|
157
|
+
console.log();
|
|
158
|
+
}
|
package/dist/cli/ui.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Spinner {
|
|
2
|
+
stop(result: string, color?: "green" | "red" | "yellow" | "cyan"): void;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Animated braille spinner. Falls back to static console.log when not a TTY.
|
|
6
|
+
*/
|
|
7
|
+
export declare function spinner(label: string): Spinner;
|
|
8
|
+
/**
|
|
9
|
+
* Arrow-key selection menu. Returns the selected index.
|
|
10
|
+
* Non-TTY fallback: returns 0 (first option).
|
|
11
|
+
*/
|
|
12
|
+
export declare function select(prompt: string, options: string[]): Promise<number>;
|
|
13
|
+
/**
|
|
14
|
+
* Y/n confirmation prompt. Returns true for y/Enter, false for n.
|
|
15
|
+
* Non-TTY fallback: returns true.
|
|
16
|
+
*/
|
|
17
|
+
export declare function confirm(prompt: string): Promise<boolean>;
|
|
18
|
+
export {};
|
package/dist/cli/ui.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
+
/**
|
|
4
|
+
* Animated braille spinner. Falls back to static console.log when not a TTY.
|
|
5
|
+
*/
|
|
6
|
+
export function spinner(label) {
|
|
7
|
+
if (!process.stdout.isTTY) {
|
|
8
|
+
console.log(` ${label}...`);
|
|
9
|
+
return {
|
|
10
|
+
stop(result) {
|
|
11
|
+
console.log(` ${label}... ${result}`);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
let frame = 0;
|
|
16
|
+
const interval = setInterval(() => {
|
|
17
|
+
const char = BRAILLE_FRAMES[frame % BRAILLE_FRAMES.length];
|
|
18
|
+
process.stdout.write(`\r\x1b[K ${chalk.cyan(char)} ${label}`);
|
|
19
|
+
frame++;
|
|
20
|
+
}, 80);
|
|
21
|
+
return {
|
|
22
|
+
stop(result, color) {
|
|
23
|
+
clearInterval(interval);
|
|
24
|
+
const icon = color === "red" ? "✗" : "✓";
|
|
25
|
+
const colorFn = color ? chalk[color] : chalk.green;
|
|
26
|
+
process.stdout.write(`\r\x1b[K ${colorFn(icon)} ${label}... ${colorFn(result)}\n`);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Arrow-key selection menu. Returns the selected index.
|
|
32
|
+
* Non-TTY fallback: returns 0 (first option).
|
|
33
|
+
*/
|
|
34
|
+
export function select(prompt, options) {
|
|
35
|
+
if (!process.stdout.isTTY || options.length === 0) {
|
|
36
|
+
console.log(` ${prompt}`);
|
|
37
|
+
if (options.length > 0)
|
|
38
|
+
console.log(` → ${options[0]}`);
|
|
39
|
+
return Promise.resolve(0);
|
|
40
|
+
}
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const stdin = process.stdin;
|
|
43
|
+
const wasRaw = stdin.isRaw;
|
|
44
|
+
stdin.setRawMode(true);
|
|
45
|
+
stdin.resume();
|
|
46
|
+
stdin.setEncoding("utf8");
|
|
47
|
+
let selected = 0;
|
|
48
|
+
// Hide cursor
|
|
49
|
+
process.stdout.write("\x1b[?25l");
|
|
50
|
+
function render() {
|
|
51
|
+
// Move up to clear previous render (except first time)
|
|
52
|
+
const lines = options.length + 1; // prompt + options
|
|
53
|
+
process.stdout.write(`\x1b[${lines}A`);
|
|
54
|
+
process.stdout.write(`\x1b[K ${chalk.bold(prompt)}\n`);
|
|
55
|
+
for (let i = 0; i < options.length; i++) {
|
|
56
|
+
process.stdout.write("\x1b[K");
|
|
57
|
+
if (i === selected) {
|
|
58
|
+
process.stdout.write(` ${chalk.cyan("❯")} ${chalk.cyan(options[i])}\n`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
process.stdout.write(` ${chalk.gray(options[i])}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Initial render — write blank lines first so cursor math works
|
|
66
|
+
process.stdout.write(`\x1b[K ${chalk.bold(prompt)}\n`);
|
|
67
|
+
for (let i = 0; i < options.length; i++) {
|
|
68
|
+
process.stdout.write("\x1b[K");
|
|
69
|
+
if (i === selected) {
|
|
70
|
+
process.stdout.write(` ${chalk.cyan("❯")} ${chalk.cyan(options[i])}\n`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
process.stdout.write(` ${chalk.gray(options[i])}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const onData = (data) => {
|
|
77
|
+
if (data === "\x1b[A") {
|
|
78
|
+
// Up arrow
|
|
79
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
80
|
+
render();
|
|
81
|
+
}
|
|
82
|
+
else if (data === "\x1b[B") {
|
|
83
|
+
// Down arrow
|
|
84
|
+
selected = (selected + 1) % options.length;
|
|
85
|
+
render();
|
|
86
|
+
}
|
|
87
|
+
else if (data === "\r" || data === "\n") {
|
|
88
|
+
// Enter — confirm
|
|
89
|
+
cleanup();
|
|
90
|
+
resolve(selected);
|
|
91
|
+
}
|
|
92
|
+
else if (data === "\x03") {
|
|
93
|
+
// Ctrl+C
|
|
94
|
+
cleanup();
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function cleanup() {
|
|
99
|
+
stdin.removeListener("data", onData);
|
|
100
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
101
|
+
stdin.pause();
|
|
102
|
+
// Show cursor
|
|
103
|
+
process.stdout.write("\x1b[?25h");
|
|
104
|
+
}
|
|
105
|
+
stdin.on("data", onData);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Y/n confirmation prompt. Returns true for y/Enter, false for n.
|
|
110
|
+
* Non-TTY fallback: returns true.
|
|
111
|
+
*/
|
|
112
|
+
export function confirm(prompt) {
|
|
113
|
+
if (!process.stdout.isTTY) {
|
|
114
|
+
console.log(` ${prompt} (Y/n): y`);
|
|
115
|
+
return Promise.resolve(true);
|
|
116
|
+
}
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
process.stdout.write(` ${prompt} ${chalk.gray("(Y/n)")}: `);
|
|
119
|
+
const stdin = process.stdin;
|
|
120
|
+
const wasRaw = stdin.isRaw;
|
|
121
|
+
stdin.setRawMode(true);
|
|
122
|
+
stdin.resume();
|
|
123
|
+
stdin.setEncoding("utf8");
|
|
124
|
+
const onData = (data) => {
|
|
125
|
+
stdin.removeListener("data", onData);
|
|
126
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
127
|
+
stdin.pause();
|
|
128
|
+
if (data === "n" || data === "N") {
|
|
129
|
+
process.stdout.write("n\n");
|
|
130
|
+
resolve(false);
|
|
131
|
+
}
|
|
132
|
+
else if (data === "\x03") {
|
|
133
|
+
process.stdout.write("\n");
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// y, Y, Enter — all true
|
|
138
|
+
process.stdout.write("y\n");
|
|
139
|
+
resolve(true);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
stdin.on("data", onData);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
watch?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface Violation {
|
|
10
|
+
rule: string;
|
|
11
|
+
level: RuleLevel;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check for layer bypass violations.
|
|
16
|
+
* Returns violations where a connection skips layers (e.g. presentation → data).
|
|
17
|
+
*/
|
|
18
|
+
export declare function checkNoLayerBypass(arch: Architecture, realNodes: ArchNode[], nodeMap: Map<string, ArchNode>, level: RuleLevel): Violation[];
|
|
19
|
+
/**
|
|
20
|
+
* Check for nodes exceeding the max connection threshold.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkMaxConnections(arch: Architecture, realNodes: ArchNode[], level: RuleLevel, threshold: number): Violation[];
|
|
23
|
+
/**
|
|
24
|
+
* Check for orphan nodes (no connections).
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkNoOrphans(arch: Architecture, realNodes: ArchNode[], level: RuleLevel): Violation[];
|
|
27
|
+
/**
|
|
28
|
+
* Check for circular dependencies.
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkCircularDeps(arch: Architecture, realNodes: ArchNode[], nodeMap: Map<string, ArchNode>, level: RuleLevel): Violation[];
|
|
31
|
+
/**
|
|
32
|
+
* Check if a node matches a custom rule matcher.
|
|
33
|
+
*/
|
|
34
|
+
export declare function matchesNode(node: ArchNode, matcher: CustomRuleMatcher): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Evaluate custom rules against the architecture.
|
|
37
|
+
*/
|
|
38
|
+
export declare function evaluateCustomRules(arch: Architecture, realNodes: ArchNode[], nodeMap: Map<string, ArchNode>, customRules: CustomRule[]): Violation[];
|
|
39
|
+
interface ValidationResult {
|
|
40
|
+
violations: Violation[];
|
|
41
|
+
errors: number;
|
|
42
|
+
warnings: number;
|
|
43
|
+
totalNodes: number;
|
|
44
|
+
totalEdges: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Core validation logic — runs all rules and returns results without side effects.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runValidation(options: ValidateOptions): ValidationResult;
|
|
50
|
+
/**
|
|
51
|
+
* Run architecture fitness function validation.
|
|
52
|
+
*/
|
|
53
|
+
export declare function handleValidate(options: ValidateOptions): Promise<void>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { resolveArchitecturePath, loadArchitectureFile, loadRulesConfig, getRuleLevel, getThreshold, parseCustomRulesFromYaml, } from "./shared.js";
|
|
5
|
+
/**
|
|
6
|
+
* Check for layer bypass violations.
|
|
7
|
+
* Returns violations where a connection skips layers (e.g. presentation → data).
|
|
8
|
+
*/
|
|
9
|
+
export function checkNoLayerBypass(arch, realNodes, nodeMap, level) {
|
|
10
|
+
const violations = [];
|
|
11
|
+
const layerOrder = {
|
|
12
|
+
presentation: 0,
|
|
13
|
+
application: 1,
|
|
14
|
+
data: 2,
|
|
15
|
+
};
|
|
16
|
+
for (const edge of arch.edges) {
|
|
17
|
+
const src = nodeMap.get(edge.source);
|
|
18
|
+
const tgt = nodeMap.get(edge.target);
|
|
19
|
+
if (!src || !tgt)
|
|
20
|
+
continue;
|
|
21
|
+
const srcOrder = layerOrder[src.layer];
|
|
22
|
+
const tgtOrder = layerOrder[tgt.layer];
|
|
23
|
+
if (srcOrder === undefined || tgtOrder === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
if (tgtOrder - srcOrder > 1) {
|
|
26
|
+
violations.push({
|
|
27
|
+
rule: "no-layer-bypass",
|
|
28
|
+
level,
|
|
29
|
+
message: `"${src.label}" (${src.layer}) → "${tgt.label}" (${tgt.layer})`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return violations;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check for nodes exceeding the max connection threshold.
|
|
37
|
+
*/
|
|
38
|
+
export function checkMaxConnections(arch, realNodes, level, threshold) {
|
|
39
|
+
const violations = [];
|
|
40
|
+
const connectionCounts = new Map();
|
|
41
|
+
for (const edge of arch.edges) {
|
|
42
|
+
connectionCounts.set(edge.source, (connectionCounts.get(edge.source) || 0) + 1);
|
|
43
|
+
connectionCounts.set(edge.target, (connectionCounts.get(edge.target) || 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
for (const node of realNodes) {
|
|
46
|
+
const count = connectionCounts.get(node.id) || 0;
|
|
47
|
+
if (count > threshold) {
|
|
48
|
+
violations.push({
|
|
49
|
+
rule: "max-connections",
|
|
50
|
+
level,
|
|
51
|
+
message: `"${node.label}" has ${count} connections (threshold: ${threshold})`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return violations;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Check for orphan nodes (no connections).
|
|
59
|
+
*/
|
|
60
|
+
export function checkNoOrphans(arch, realNodes, level) {
|
|
61
|
+
const violations = [];
|
|
62
|
+
const connectedIds = new Set();
|
|
63
|
+
for (const edge of arch.edges) {
|
|
64
|
+
connectedIds.add(edge.source);
|
|
65
|
+
connectedIds.add(edge.target);
|
|
66
|
+
}
|
|
67
|
+
for (const node of realNodes) {
|
|
68
|
+
if (!connectedIds.has(node.id)) {
|
|
69
|
+
violations.push({
|
|
70
|
+
rule: "no-orphans",
|
|
71
|
+
level,
|
|
72
|
+
message: `"${node.label}" has no connections`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return violations;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check for circular dependencies.
|
|
80
|
+
*/
|
|
81
|
+
export function checkCircularDeps(arch, realNodes, nodeMap, level) {
|
|
82
|
+
const violations = [];
|
|
83
|
+
const cycles = detectCycles(arch, realNodes);
|
|
84
|
+
for (const cycle of cycles) {
|
|
85
|
+
violations.push({
|
|
86
|
+
rule: "no-circular-deps",
|
|
87
|
+
level,
|
|
88
|
+
message: `Cycle: ${cycle.map((id) => nodeMap.get(id)?.label || id).join(" → ")}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return violations;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a node matches a custom rule matcher.
|
|
95
|
+
*/
|
|
96
|
+
export function matchesNode(node, matcher) {
|
|
97
|
+
let matches = true;
|
|
98
|
+
if (matcher.type && node.type !== matcher.type)
|
|
99
|
+
matches = false;
|
|
100
|
+
if (matcher.layer && node.layer !== matcher.layer)
|
|
101
|
+
matches = false;
|
|
102
|
+
if (matcher.id && node.id !== matcher.id)
|
|
103
|
+
matches = false;
|
|
104
|
+
return matcher.not ? !matches : matches;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Evaluate custom rules against the architecture.
|
|
108
|
+
*/
|
|
109
|
+
export function evaluateCustomRules(arch, realNodes, nodeMap, customRules) {
|
|
110
|
+
const violations = [];
|
|
111
|
+
for (const rule of customRules) {
|
|
112
|
+
for (const edge of arch.edges) {
|
|
113
|
+
const src = nodeMap.get(edge.source);
|
|
114
|
+
const tgt = nodeMap.get(edge.target);
|
|
115
|
+
if (!src || !tgt)
|
|
116
|
+
continue;
|
|
117
|
+
if (matchesNode(src, rule.from) && matchesNode(tgt, rule.to)) {
|
|
118
|
+
violations.push({
|
|
119
|
+
rule: rule.name,
|
|
120
|
+
level: rule.level,
|
|
121
|
+
message: `"${src.label}" (${src.layer}/${src.type}) → "${tgt.label}" (${tgt.layer}/${tgt.type})`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return violations;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Core validation logic — runs all rules and returns results without side effects.
|
|
130
|
+
*/
|
|
131
|
+
export function runValidation(options) {
|
|
132
|
+
const diagramPath = resolveArchitecturePath(options);
|
|
133
|
+
const arch = loadArchitectureFile(diagramPath);
|
|
134
|
+
const config = loadRulesConfig(options.config);
|
|
135
|
+
const violations = [];
|
|
136
|
+
const realNodes = arch.nodes;
|
|
137
|
+
const nodeMap = new Map();
|
|
138
|
+
for (const node of realNodes) {
|
|
139
|
+
nodeMap.set(node.id, node);
|
|
140
|
+
}
|
|
141
|
+
const layerBypassLevel = getRuleLevel(config, "no-layer-bypass", "error");
|
|
142
|
+
if (layerBypassLevel !== "off") {
|
|
143
|
+
violations.push(...checkNoLayerBypass(arch, realNodes, nodeMap, layerBypassLevel));
|
|
144
|
+
}
|
|
145
|
+
const maxConnLevel = getRuleLevel(config, "max-connections", "warn");
|
|
146
|
+
if (maxConnLevel !== "off") {
|
|
147
|
+
const threshold = getThreshold(config, "max-connections", 6);
|
|
148
|
+
violations.push(...checkMaxConnections(arch, realNodes, maxConnLevel, threshold));
|
|
149
|
+
}
|
|
150
|
+
const orphanLevel = getRuleLevel(config, "no-orphans", "warn");
|
|
151
|
+
if (orphanLevel !== "off") {
|
|
152
|
+
violations.push(...checkNoOrphans(arch, realNodes, orphanLevel));
|
|
153
|
+
}
|
|
154
|
+
const circularLevel = getRuleLevel(config, "no-circular-deps", "error");
|
|
155
|
+
if (circularLevel !== "off") {
|
|
156
|
+
violations.push(...checkCircularDeps(arch, realNodes, nodeMap, circularLevel));
|
|
157
|
+
}
|
|
158
|
+
// Custom rules
|
|
159
|
+
const rootDir = process.cwd();
|
|
160
|
+
const configPath = options.config
|
|
161
|
+
? path.resolve(rootDir, options.config)
|
|
162
|
+
: path.join(rootDir, ".archbyte", "archbyte.yaml");
|
|
163
|
+
if (fs.existsSync(configPath)) {
|
|
164
|
+
const yamlContent = fs.readFileSync(configPath, "utf-8");
|
|
165
|
+
const customRules = parseCustomRulesFromYaml(yamlContent);
|
|
166
|
+
if (customRules.length > 0) {
|
|
167
|
+
violations.push(...evaluateCustomRules(arch, realNodes, nodeMap, customRules));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
violations,
|
|
172
|
+
errors: violations.filter((v) => v.level === "error").length,
|
|
173
|
+
warnings: violations.filter((v) => v.level === "warn").length,
|
|
174
|
+
totalNodes: realNodes.length,
|
|
175
|
+
totalEdges: arch.edges.length,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Run architecture fitness function validation.
|
|
180
|
+
*/
|
|
181
|
+
export async function handleValidate(options) {
|
|
182
|
+
const result = runValidation(options);
|
|
183
|
+
// CI mode: output JSON and exit
|
|
184
|
+
if (options.ci) {
|
|
185
|
+
const output = {
|
|
186
|
+
passed: result.errors === 0,
|
|
187
|
+
errors: result.errors,
|
|
188
|
+
warnings: result.warnings,
|
|
189
|
+
violations: result.violations.map((v) => ({
|
|
190
|
+
rule: v.rule,
|
|
191
|
+
level: v.level,
|
|
192
|
+
message: v.message,
|
|
193
|
+
})),
|
|
194
|
+
summary: {
|
|
195
|
+
totalNodes: result.totalNodes,
|
|
196
|
+
totalEdges: result.totalEdges,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
console.log(JSON.stringify(output, null, 2));
|
|
200
|
+
process.exit(result.errors > 0 ? 1 : 0);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Human-readable output
|
|
204
|
+
printValidationReport(result);
|
|
205
|
+
// Watch mode: re-validate on file changes
|
|
206
|
+
if (options.watch) {
|
|
207
|
+
const chokidar = await import("chokidar");
|
|
208
|
+
const diagramPath = resolveArchitecturePath(options);
|
|
209
|
+
console.log(chalk.gray(` Watching ${diagramPath} for changes...`));
|
|
210
|
+
console.log(chalk.gray(" Press Ctrl+C to stop."));
|
|
211
|
+
const watcher = chokidar.watch(diagramPath, { ignoreInitial: true });
|
|
212
|
+
watcher.on("change", () => {
|
|
213
|
+
console.clear();
|
|
214
|
+
const watchResult = runValidation(options);
|
|
215
|
+
printValidationReport(watchResult);
|
|
216
|
+
console.log(chalk.gray(` Watching ${diagramPath} for changes...`));
|
|
217
|
+
console.log(chalk.gray(" Press Ctrl+C to stop."));
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (result.errors > 0) {
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function printValidationReport(result) {
|
|
226
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(chalk.bold.cyan(`⚡ ArchByte Validate — ${projectName}`));
|
|
229
|
+
console.log();
|
|
230
|
+
// Group violations by rule for display
|
|
231
|
+
const ruleViolations = new Map();
|
|
232
|
+
for (const v of result.violations) {
|
|
233
|
+
if (!ruleViolations.has(v.rule))
|
|
234
|
+
ruleViolations.set(v.rule, []);
|
|
235
|
+
ruleViolations.get(v.rule).push(v);
|
|
236
|
+
}
|
|
237
|
+
// Print built-in rules (even if they passed)
|
|
238
|
+
const builtinRules = ["no-layer-bypass", "max-connections", "no-orphans", "no-circular-deps"];
|
|
239
|
+
for (const rule of builtinRules) {
|
|
240
|
+
const violations = ruleViolations.get(rule) || [];
|
|
241
|
+
printRuleResult(rule, violations.length > 0 ? violations[0].level : "error", violations.length, result.violations);
|
|
242
|
+
}
|
|
243
|
+
// Print custom rule results
|
|
244
|
+
for (const [rule, violations] of ruleViolations) {
|
|
245
|
+
if (builtinRules.includes(rule))
|
|
246
|
+
continue;
|
|
247
|
+
printRuleResult(rule, violations[0].level, violations.length, result.violations);
|
|
248
|
+
}
|
|
249
|
+
console.log();
|
|
250
|
+
const resultStr = result.errors > 0 ? chalk.red("FAIL") : chalk.green("PASS");
|
|
251
|
+
console.log(` Result: ${result.warnings} warning${result.warnings !== 1 ? "s" : ""}, ${result.errors} error${result.errors !== 1 ? "s" : ""} — ${resultStr}`);
|
|
252
|
+
console.log();
|
|
253
|
+
}
|
|
254
|
+
function printRuleResult(rule, level, count, violations) {
|
|
255
|
+
if (count === 0) {
|
|
256
|
+
console.log(chalk.green(` ✓ ${rule}: passed (0 violations)`));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const icon = level === "error" ? chalk.red("✗") : chalk.yellow("⚠");
|
|
260
|
+
const label = level === "error"
|
|
261
|
+
? chalk.red(`${count} error${count !== 1 ? "s" : ""}`)
|
|
262
|
+
: chalk.yellow(`${count} warning${count !== 1 ? "s" : ""}`);
|
|
263
|
+
console.log(` ${icon} ${rule}: ${label}`);
|
|
264
|
+
const ruleViolations = violations.filter((v) => v.rule === rule);
|
|
265
|
+
for (const v of ruleViolations) {
|
|
266
|
+
console.log(chalk.gray(` → ${v.message}`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Detect cycles in the directed graph using DFS.
|
|
272
|
+
* Returns an array of cycles, each represented as a list of node IDs.
|
|
273
|
+
*/
|
|
274
|
+
function detectCycles(arch, realNodes) {
|
|
275
|
+
const adjacency = new Map();
|
|
276
|
+
for (const node of realNodes) {
|
|
277
|
+
adjacency.set(node.id, []);
|
|
278
|
+
}
|
|
279
|
+
for (const edge of arch.edges) {
|
|
280
|
+
const neighbors = adjacency.get(edge.source);
|
|
281
|
+
if (neighbors) {
|
|
282
|
+
neighbors.push(edge.target);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const visited = new Set();
|
|
286
|
+
const inStack = new Set();
|
|
287
|
+
const cycles = [];
|
|
288
|
+
const path = [];
|
|
289
|
+
function dfs(nodeId) {
|
|
290
|
+
visited.add(nodeId);
|
|
291
|
+
inStack.add(nodeId);
|
|
292
|
+
path.push(nodeId);
|
|
293
|
+
for (const neighbor of adjacency.get(nodeId) || []) {
|
|
294
|
+
if (!visited.has(neighbor)) {
|
|
295
|
+
dfs(neighbor);
|
|
296
|
+
}
|
|
297
|
+
else if (inStack.has(neighbor)) {
|
|
298
|
+
// Found a cycle — extract it from the path
|
|
299
|
+
const cycleStart = path.indexOf(neighbor);
|
|
300
|
+
if (cycleStart !== -1) {
|
|
301
|
+
const cycle = [...path.slice(cycleStart), neighbor];
|
|
302
|
+
cycles.push(cycle);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
path.pop();
|
|
307
|
+
inStack.delete(nodeId);
|
|
308
|
+
}
|
|
309
|
+
for (const node of realNodes) {
|
|
310
|
+
if (!visited.has(node.id)) {
|
|
311
|
+
dfs(node.id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return cycles;
|
|
315
|
+
}
|