archbyte 0.4.1 → 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.
- package/README.md +9 -25
- package/bin/archbyte.js +6 -41
- 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 +81 -106
- package/package.json +2 -2
- package/templates/archbyte.yaml +28 -7
- package/ui/dist/assets/index-BXsZyipz.js +72 -0
- package/ui/dist/assets/{index-DDCNauh7.css → index-ow1c3Nxp.css} +1 -1
- 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-DO4t5Xu1.js +0 -72
package/dist/cli/validate.js
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
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
|
-
if (result.errors > 0) {
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function printValidationReport(result) {
|
|
210
|
-
const projectName = process.cwd().split("/").pop() || "project";
|
|
211
|
-
console.log();
|
|
212
|
-
console.log(chalk.bold.cyan(`⚡ ArchByte Validate: ${projectName}`));
|
|
213
|
-
console.log();
|
|
214
|
-
// Group violations by rule for display
|
|
215
|
-
const ruleViolations = new Map();
|
|
216
|
-
for (const v of result.violations) {
|
|
217
|
-
if (!ruleViolations.has(v.rule))
|
|
218
|
-
ruleViolations.set(v.rule, []);
|
|
219
|
-
ruleViolations.get(v.rule).push(v);
|
|
220
|
-
}
|
|
221
|
-
// Print built-in rules (even if they passed)
|
|
222
|
-
const builtinRules = ["no-layer-bypass", "max-connections", "no-orphans", "no-circular-deps"];
|
|
223
|
-
for (const rule of builtinRules) {
|
|
224
|
-
const violations = ruleViolations.get(rule) || [];
|
|
225
|
-
printRuleResult(rule, violations.length > 0 ? violations[0].level : "error", violations.length, result.violations);
|
|
226
|
-
}
|
|
227
|
-
// Print custom rule results
|
|
228
|
-
for (const [rule, violations] of ruleViolations) {
|
|
229
|
-
if (builtinRules.includes(rule))
|
|
230
|
-
continue;
|
|
231
|
-
printRuleResult(rule, violations[0].level, violations.length, result.violations);
|
|
232
|
-
}
|
|
233
|
-
console.log();
|
|
234
|
-
const resultStr = result.errors > 0 ? chalk.red("FAIL") : chalk.green("PASS");
|
|
235
|
-
console.log(` Result: ${result.warnings} warning${result.warnings !== 1 ? "s" : ""}, ${result.errors} error${result.errors !== 1 ? "s" : ""} ${resultStr}`);
|
|
236
|
-
console.log();
|
|
237
|
-
}
|
|
238
|
-
function printRuleResult(rule, level, count, violations) {
|
|
239
|
-
if (count === 0) {
|
|
240
|
-
console.log(chalk.green(` ✓ ${rule}: passed (0 violations)`));
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
const icon = level === "error" ? chalk.red("✗") : chalk.yellow("⚠");
|
|
244
|
-
const label = level === "error"
|
|
245
|
-
? chalk.red(`${count} error${count !== 1 ? "s" : ""}`)
|
|
246
|
-
: chalk.yellow(`${count} warning${count !== 1 ? "s" : ""}`);
|
|
247
|
-
console.log(` ${icon} ${rule}: ${label}`);
|
|
248
|
-
const ruleViolations = violations.filter((v) => v.rule === rule);
|
|
249
|
-
for (const v of ruleViolations) {
|
|
250
|
-
console.log(chalk.gray(` → ${v.message}`));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Detect cycles in the directed graph using DFS.
|
|
256
|
-
* Returns an array of cycles, each represented as a list of node IDs.
|
|
257
|
-
*/
|
|
258
|
-
function detectCycles(arch, realNodes) {
|
|
259
|
-
const adjacency = new Map();
|
|
260
|
-
for (const node of realNodes) {
|
|
261
|
-
adjacency.set(node.id, []);
|
|
262
|
-
}
|
|
263
|
-
for (const edge of arch.edges) {
|
|
264
|
-
const neighbors = adjacency.get(edge.source);
|
|
265
|
-
if (neighbors) {
|
|
266
|
-
neighbors.push(edge.target);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const visited = new Set();
|
|
270
|
-
const inStack = new Set();
|
|
271
|
-
const cycles = [];
|
|
272
|
-
const path = [];
|
|
273
|
-
function dfs(nodeId) {
|
|
274
|
-
visited.add(nodeId);
|
|
275
|
-
inStack.add(nodeId);
|
|
276
|
-
path.push(nodeId);
|
|
277
|
-
for (const neighbor of adjacency.get(nodeId) || []) {
|
|
278
|
-
if (!visited.has(neighbor)) {
|
|
279
|
-
dfs(neighbor);
|
|
280
|
-
}
|
|
281
|
-
else if (inStack.has(neighbor)) {
|
|
282
|
-
// Found a cycle — extract it from the path
|
|
283
|
-
const cycleStart = path.indexOf(neighbor);
|
|
284
|
-
if (cycleStart !== -1) {
|
|
285
|
-
const cycle = [...path.slice(cycleStart), neighbor];
|
|
286
|
-
cycles.push(cycle);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
path.pop();
|
|
291
|
-
inStack.delete(nodeId);
|
|
292
|
-
}
|
|
293
|
-
for (const node of realNodes) {
|
|
294
|
-
if (!visited.has(node.id)) {
|
|
295
|
-
dfs(node.id);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return cycles;
|
|
299
|
-
}
|