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.
Files changed (142) hide show
  1. package/README.md +282 -0
  2. package/bin/archbyte.js +213 -0
  3. package/dist/agents/core/component-detector.d.ts +2 -0
  4. package/dist/agents/core/component-detector.js +57 -0
  5. package/dist/agents/core/connection-mapper.d.ts +2 -0
  6. package/dist/agents/core/connection-mapper.js +77 -0
  7. package/dist/agents/core/doc-parser.d.ts +2 -0
  8. package/dist/agents/core/doc-parser.js +64 -0
  9. package/dist/agents/core/env-detector.d.ts +2 -0
  10. package/dist/agents/core/env-detector.js +51 -0
  11. package/dist/agents/core/event-detector.d.ts +2 -0
  12. package/dist/agents/core/event-detector.js +59 -0
  13. package/dist/agents/core/infra-analyzer.d.ts +2 -0
  14. package/dist/agents/core/infra-analyzer.js +72 -0
  15. package/dist/agents/core/structure-scanner.d.ts +2 -0
  16. package/dist/agents/core/structure-scanner.js +55 -0
  17. package/dist/agents/core/validator.d.ts +2 -0
  18. package/dist/agents/core/validator.js +74 -0
  19. package/dist/agents/index.d.ts +24 -0
  20. package/dist/agents/index.js +73 -0
  21. package/dist/agents/llm/index.d.ts +8 -0
  22. package/dist/agents/llm/index.js +185 -0
  23. package/dist/agents/llm/prompt-builder.d.ts +3 -0
  24. package/dist/agents/llm/prompt-builder.js +251 -0
  25. package/dist/agents/llm/response-parser.d.ts +6 -0
  26. package/dist/agents/llm/response-parser.js +174 -0
  27. package/dist/agents/llm/types.d.ts +31 -0
  28. package/dist/agents/llm/types.js +2 -0
  29. package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
  30. package/dist/agents/pipeline/agents/component-identifier.js +102 -0
  31. package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
  32. package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
  33. package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
  34. package/dist/agents/pipeline/agents/flow-detector.js +101 -0
  35. package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
  36. package/dist/agents/pipeline/agents/service-describer.js +100 -0
  37. package/dist/agents/pipeline/agents/validator.d.ts +3 -0
  38. package/dist/agents/pipeline/agents/validator.js +102 -0
  39. package/dist/agents/pipeline/index.d.ts +13 -0
  40. package/dist/agents/pipeline/index.js +128 -0
  41. package/dist/agents/pipeline/merger.d.ts +7 -0
  42. package/dist/agents/pipeline/merger.js +212 -0
  43. package/dist/agents/pipeline/response-parser.d.ts +5 -0
  44. package/dist/agents/pipeline/response-parser.js +43 -0
  45. package/dist/agents/pipeline/types.d.ts +92 -0
  46. package/dist/agents/pipeline/types.js +3 -0
  47. package/dist/agents/prompt-data.d.ts +1 -0
  48. package/dist/agents/prompt-data.js +15 -0
  49. package/dist/agents/prompts-encode.d.ts +9 -0
  50. package/dist/agents/prompts-encode.js +26 -0
  51. package/dist/agents/prompts.d.ts +12 -0
  52. package/dist/agents/prompts.js +30 -0
  53. package/dist/agents/providers/anthropic.d.ts +10 -0
  54. package/dist/agents/providers/anthropic.js +117 -0
  55. package/dist/agents/providers/google.d.ts +10 -0
  56. package/dist/agents/providers/google.js +136 -0
  57. package/dist/agents/providers/ollama.d.ts +9 -0
  58. package/dist/agents/providers/ollama.js +162 -0
  59. package/dist/agents/providers/openai.d.ts +9 -0
  60. package/dist/agents/providers/openai.js +142 -0
  61. package/dist/agents/providers/router.d.ts +7 -0
  62. package/dist/agents/providers/router.js +55 -0
  63. package/dist/agents/runtime/orchestrator.d.ts +34 -0
  64. package/dist/agents/runtime/orchestrator.js +193 -0
  65. package/dist/agents/runtime/registry.d.ts +23 -0
  66. package/dist/agents/runtime/registry.js +56 -0
  67. package/dist/agents/runtime/types.d.ts +117 -0
  68. package/dist/agents/runtime/types.js +29 -0
  69. package/dist/agents/static/code-sampler.d.ts +3 -0
  70. package/dist/agents/static/code-sampler.js +153 -0
  71. package/dist/agents/static/component-detector.d.ts +3 -0
  72. package/dist/agents/static/component-detector.js +404 -0
  73. package/dist/agents/static/connection-mapper.d.ts +3 -0
  74. package/dist/agents/static/connection-mapper.js +280 -0
  75. package/dist/agents/static/doc-parser.d.ts +3 -0
  76. package/dist/agents/static/doc-parser.js +358 -0
  77. package/dist/agents/static/env-detector.d.ts +3 -0
  78. package/dist/agents/static/env-detector.js +73 -0
  79. package/dist/agents/static/event-detector.d.ts +3 -0
  80. package/dist/agents/static/event-detector.js +70 -0
  81. package/dist/agents/static/file-tree-collector.d.ts +3 -0
  82. package/dist/agents/static/file-tree-collector.js +51 -0
  83. package/dist/agents/static/index.d.ts +19 -0
  84. package/dist/agents/static/index.js +307 -0
  85. package/dist/agents/static/infra-analyzer.d.ts +3 -0
  86. package/dist/agents/static/infra-analyzer.js +208 -0
  87. package/dist/agents/static/structure-scanner.d.ts +3 -0
  88. package/dist/agents/static/structure-scanner.js +195 -0
  89. package/dist/agents/static/types.d.ts +165 -0
  90. package/dist/agents/static/types.js +2 -0
  91. package/dist/agents/static/utils.d.ts +21 -0
  92. package/dist/agents/static/utils.js +146 -0
  93. package/dist/agents/static/validator.d.ts +2 -0
  94. package/dist/agents/static/validator.js +75 -0
  95. package/dist/agents/tools/claude-code.d.ts +38 -0
  96. package/dist/agents/tools/claude-code.js +129 -0
  97. package/dist/agents/tools/local-fs.d.ts +12 -0
  98. package/dist/agents/tools/local-fs.js +112 -0
  99. package/dist/agents/tools/tool-definitions.d.ts +6 -0
  100. package/dist/agents/tools/tool-definitions.js +66 -0
  101. package/dist/cli/analyze.d.ts +27 -0
  102. package/dist/cli/analyze.js +586 -0
  103. package/dist/cli/auth.d.ts +46 -0
  104. package/dist/cli/auth.js +397 -0
  105. package/dist/cli/config.d.ts +11 -0
  106. package/dist/cli/config.js +177 -0
  107. package/dist/cli/diff.d.ts +10 -0
  108. package/dist/cli/diff.js +144 -0
  109. package/dist/cli/export.d.ts +10 -0
  110. package/dist/cli/export.js +321 -0
  111. package/dist/cli/gate.d.ts +13 -0
  112. package/dist/cli/gate.js +131 -0
  113. package/dist/cli/generate.d.ts +10 -0
  114. package/dist/cli/generate.js +213 -0
  115. package/dist/cli/license-gate.d.ts +27 -0
  116. package/dist/cli/license-gate.js +121 -0
  117. package/dist/cli/patrol.d.ts +15 -0
  118. package/dist/cli/patrol.js +212 -0
  119. package/dist/cli/run.d.ts +11 -0
  120. package/dist/cli/run.js +24 -0
  121. package/dist/cli/serve.d.ts +9 -0
  122. package/dist/cli/serve.js +65 -0
  123. package/dist/cli/setup.d.ts +1 -0
  124. package/dist/cli/setup.js +233 -0
  125. package/dist/cli/shared.d.ts +68 -0
  126. package/dist/cli/shared.js +275 -0
  127. package/dist/cli/stats.d.ts +9 -0
  128. package/dist/cli/stats.js +158 -0
  129. package/dist/cli/ui.d.ts +18 -0
  130. package/dist/cli/ui.js +144 -0
  131. package/dist/cli/validate.d.ts +54 -0
  132. package/dist/cli/validate.js +315 -0
  133. package/dist/cli/workflow.d.ts +10 -0
  134. package/dist/cli/workflow.js +594 -0
  135. package/dist/server/src/generator/index.d.ts +123 -0
  136. package/dist/server/src/generator/index.js +254 -0
  137. package/dist/server/src/index.d.ts +8 -0
  138. package/dist/server/src/index.js +1311 -0
  139. package/package.json +62 -0
  140. package/ui/dist/assets/index-B66Til39.js +70 -0
  141. package/ui/dist/assets/index-BE2OWbzu.css +1 -0
  142. 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
+ }
@@ -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
+ }