autonomous-flow-daemon 1.6.0 → 1.9.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/CHANGELOG.md +85 -85
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -266
- package/mcp-config.json +10 -10
- package/package.json +4 -2
- package/src/adapters/index.ts +370 -370
- package/src/cli.ts +162 -127
- package/src/commands/benchmark.ts +187 -187
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/evolution.ts +84 -1
- package/src/commands/fix.ts +158 -158
- package/src/commands/lang.ts +41 -41
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -14
- package/src/commands/score.ts +276 -276
- package/src/commands/start.ts +155 -155
- package/src/commands/status.ts +157 -157
- package/src/commands/stop.ts +68 -68
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +329 -16
- package/src/constants.ts +32 -32
- package/src/core/boast.ts +280 -280
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -117
- package/src/core/discovery.ts +65 -65
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -71
- package/src/core/hologram/fallback.ts +11 -11
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -227
- package/src/core/hologram/py-extractor.ts +132 -132
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -320
- package/src/core/hologram/types.ts +27 -25
- package/src/core/hologram.ts +73 -71
- package/src/core/i18n/messages.ts +309 -309
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -33
- package/src/core/log-utils.ts +38 -38
- package/src/core/lru-map.ts +61 -61
- package/src/core/notify.ts +74 -74
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -28
- package/src/daemon/client.ts +78 -65
- package/src/daemon/event-batcher.ts +108 -108
- package/src/daemon/guards.ts +13 -13
- package/src/daemon/http-routes.ts +376 -293
- package/src/daemon/mcp-handler.ts +575 -270
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -590
- package/src/daemon/types.ts +121 -100
- package/src/daemon/workspace-map.ts +104 -92
- package/src/platform.ts +60 -60
- package/src/version.ts +15 -15
- package/README.ko.md +0 -266
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
3
|
-
import { generateHologram } from "../core/hologram";
|
|
4
|
-
import { getSystemLanguage } from "../core/locale";
|
|
5
|
-
|
|
6
|
-
// ── ANSI helpers ──
|
|
7
|
-
const C = {
|
|
8
|
-
reset: "\x1b[0m",
|
|
9
|
-
bold: "\x1b[1m",
|
|
10
|
-
dim: "\x1b[2m",
|
|
11
|
-
red: "\x1b[31m",
|
|
12
|
-
green: "\x1b[32m",
|
|
13
|
-
yellow: "\x1b[33m",
|
|
14
|
-
cyan: "\x1b[36m",
|
|
15
|
-
white: "\x1b[37m",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
interface FileResult {
|
|
19
|
-
path: string;
|
|
20
|
-
originalLines: number;
|
|
21
|
-
hologramLines: number;
|
|
22
|
-
originalChars: number;
|
|
23
|
-
hologramChars: number;
|
|
24
|
-
savings: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function collectTsFiles(dir: string, base: string): string[] {
|
|
28
|
-
const results: string[] = [];
|
|
29
|
-
const skipDirs = new Set(["node_modules", ".git", ".afd", "dist", "coverage", ".omc"]);
|
|
30
|
-
|
|
31
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
-
if (entry.isDirectory()) {
|
|
33
|
-
if (!skipDirs.has(entry.name)) {
|
|
34
|
-
results.push(...collectTsFiles(join(dir, entry.name), base));
|
|
35
|
-
}
|
|
36
|
-
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
37
|
-
results.push(join(dir, entry.name));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return results;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function formatChars(n: number): string {
|
|
44
|
-
if (n < 1000) return `${n}`;
|
|
45
|
-
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}K`;
|
|
46
|
-
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function savingsColor(pct: number): string {
|
|
50
|
-
if (pct >= 80) return C.green;
|
|
51
|
-
if (pct >= 50) return C.yellow;
|
|
52
|
-
return C.red;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function padRight(s: string, w: number): string {
|
|
56
|
-
const stripped = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
57
|
-
return s + " ".repeat(Math.max(0, w - stripped.length));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function padLeft(s: string, w: number): string {
|
|
61
|
-
const stripped = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
62
|
-
return " ".repeat(Math.max(0, w - stripped.length)) + s;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function benchmarkCommand(options: { sort?: string; top?: string; json?: boolean }) {
|
|
66
|
-
const lang = getSystemLanguage();
|
|
67
|
-
const ko = lang === "ko";
|
|
68
|
-
const cwd = process.cwd();
|
|
69
|
-
const files = collectTsFiles(join(cwd, "src"), cwd);
|
|
70
|
-
|
|
71
|
-
if (files.length === 0) {
|
|
72
|
-
console.error(`${C.red}[afd] ${ko ? "src/ 디렉토리에 TS/JS 파일이 없습니다." : "No TS/JS files found in src/."}${C.reset}`);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const results: FileResult[] = [];
|
|
77
|
-
const startTime = performance.now();
|
|
78
|
-
|
|
79
|
-
for (const filePath of files) {
|
|
80
|
-
try {
|
|
81
|
-
const source = readFileSync(filePath, "utf-8");
|
|
82
|
-
const { hologram, originalLength, hologramLength, savings } = await generateHologram(filePath, source);
|
|
83
|
-
results.push({
|
|
84
|
-
path: relative(cwd, filePath),
|
|
85
|
-
originalLines: source.split("\n").length,
|
|
86
|
-
hologramLines: hologram.split("\n").length,
|
|
87
|
-
originalChars: originalLength,
|
|
88
|
-
hologramChars: hologramLength,
|
|
89
|
-
savings: Math.round(savings * 10) / 10,
|
|
90
|
-
});
|
|
91
|
-
} catch {
|
|
92
|
-
// Skip files that fail to parse
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const elapsed = Math.round(performance.now() - startTime);
|
|
97
|
-
|
|
98
|
-
// Sort
|
|
99
|
-
const sortKey = options.sort ?? "savings";
|
|
100
|
-
results.sort((a, b) => {
|
|
101
|
-
if (sortKey === "size") return b.originalChars - a.originalChars;
|
|
102
|
-
if (sortKey === "name") return a.path.localeCompare(b.path);
|
|
103
|
-
return b.savings - a.savings; // default: savings desc
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const limit = options.top ? parseInt(options.top, 10) : results.length;
|
|
107
|
-
const display = results.slice(0, limit);
|
|
108
|
-
|
|
109
|
-
// JSON output
|
|
110
|
-
if (options.json) {
|
|
111
|
-
const totalOriginal = results.reduce((s, r) => s + r.originalChars, 0);
|
|
112
|
-
const totalHologram = results.reduce((s, r) => s + r.hologramChars, 0);
|
|
113
|
-
console.log(JSON.stringify({
|
|
114
|
-
files: results.length,
|
|
115
|
-
totalOriginalChars: totalOriginal,
|
|
116
|
-
totalHologramChars: totalHologram,
|
|
117
|
-
totalSavedChars: totalOriginal - totalHologram,
|
|
118
|
-
overallCompression: totalOriginal > 0 ? Math.round((1 - totalHologram / totalOriginal) * 1000) / 10 : 0,
|
|
119
|
-
estimatedTokensSaved: Math.round((totalOriginal - totalHologram) / 4),
|
|
120
|
-
elapsedMs: elapsed,
|
|
121
|
-
results: results.map(r => ({
|
|
122
|
-
path: r.path,
|
|
123
|
-
originalLines: r.originalLines,
|
|
124
|
-
hologramLines: r.hologramLines,
|
|
125
|
-
originalChars: r.originalChars,
|
|
126
|
-
hologramChars: r.hologramChars,
|
|
127
|
-
savings: r.savings,
|
|
128
|
-
})),
|
|
129
|
-
}, null, 2));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ── Header ──
|
|
134
|
-
console.log();
|
|
135
|
-
console.log(`${C.bold}${C.cyan} ╔══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
136
|
-
console.log(`${C.bold}${C.cyan} ║ ${ko ? "홀로그램 AST 압축 벤치마크" : "Hologram AST Compression Benchmark"} ║${C.reset}`);
|
|
137
|
-
console.log(`${C.bold}${C.cyan} ╚══════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
138
|
-
console.log();
|
|
139
|
-
|
|
140
|
-
// ── Table Header ──
|
|
141
|
-
const colFile = ko ? "파일" : "File";
|
|
142
|
-
const colLines = ko ? "원본줄" : "Lines";
|
|
143
|
-
const colHolo = ko ? "홀로줄" : "Holo";
|
|
144
|
-
const colOrig = ko ? "원본" : "Original";
|
|
145
|
-
const colComp = ko ? "압축" : "Compressed";
|
|
146
|
-
const colSave = ko ? "절감률" : "Savings";
|
|
147
|
-
|
|
148
|
-
console.log(
|
|
149
|
-
` ${C.dim}${padRight(colFile, 40)} ${padLeft(colLines, 6)} ${padLeft(colHolo, 6)} ${padLeft(colOrig, 8)} ${padLeft(colComp, 8)} ${padLeft(colSave, 8)}${C.reset}`
|
|
150
|
-
);
|
|
151
|
-
console.log(` ${C.dim}${"─".repeat(80)}${C.reset}`);
|
|
152
|
-
|
|
153
|
-
// ── Rows ──
|
|
154
|
-
for (const r of display) {
|
|
155
|
-
const sc = savingsColor(r.savings);
|
|
156
|
-
const savingsStr = `${sc}${r.savings.toFixed(1)}%${C.reset}`;
|
|
157
|
-
console.log(
|
|
158
|
-
` ${padRight(r.path, 40)} ${padLeft(String(r.originalLines), 6)} ${padLeft(String(r.hologramLines), 6)} ${padLeft(formatChars(r.originalChars), 8)} ${padLeft(formatChars(r.hologramChars), 8)} ${padLeft(savingsStr, 8)}`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (limit < results.length) {
|
|
163
|
-
console.log(` ${C.dim}... ${ko ? `외 ${results.length - limit}개 파일` : `and ${results.length - limit} more files`}${C.reset}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Summary ──
|
|
167
|
-
const totalOriginal = results.reduce((s, r) => s + r.originalChars, 0);
|
|
168
|
-
const totalHologram = results.reduce((s, r) => s + r.hologramChars, 0);
|
|
169
|
-
const totalSaved = totalOriginal - totalHologram;
|
|
170
|
-
const overallPct = totalOriginal > 0 ? Math.round((1 - totalHologram / totalOriginal) * 1000) / 10 : 0;
|
|
171
|
-
const estimatedTokens = Math.round(totalSaved / 4);
|
|
172
|
-
const high = results.filter(r => r.savings >= 70).length;
|
|
173
|
-
|
|
174
|
-
console.log();
|
|
175
|
-
console.log(` ${C.dim}${"─".repeat(80)}${C.reset}`);
|
|
176
|
-
console.log(` ${C.bold}${ko ? "요약" : "Summary"}${C.reset}`);
|
|
177
|
-
console.log();
|
|
178
|
-
console.log(` ${ko ? "분석 파일" : "Files analyzed"} ${C.bold}${results.length}${C.reset}`);
|
|
179
|
-
console.log(` ${ko ? "전체 압축률" : "Overall compression"} ${C.bold}${savingsColor(overallPct)}${overallPct}%${C.reset}`);
|
|
180
|
-
console.log(` ${ko ? "원본 크기" : "Original size"} ${C.bold}${formatChars(totalOriginal)}${C.reset} ${C.dim}(${(totalOriginal / 1024).toFixed(0)} KB)${C.reset}`);
|
|
181
|
-
console.log(` ${ko ? "압축 크기" : "Compressed size"} ${C.bold}${formatChars(totalHologram)}${C.reset} ${C.dim}(${(totalHologram / 1024).toFixed(0)} KB)${C.reset}`);
|
|
182
|
-
console.log(` ${ko ? "절약 크기" : "Saved"} ${C.bold}${C.green}${formatChars(totalSaved)}${C.reset} ${C.dim}(${(totalSaved / 1024).toFixed(0)} KB)${C.reset}`);
|
|
183
|
-
console.log(` ${ko ? "추정 토큰 절약" : "Est. tokens saved"} ${C.bold}${C.green}~${estimatedTokens.toLocaleString()}${C.reset}`);
|
|
184
|
-
console.log(` ${ko ? "70%+ 압축 파일" : "70%+ compression"} ${C.bold}${high}${C.reset}/${results.length} ${C.dim}(${Math.round(high / results.length * 100)}%)${C.reset}`);
|
|
185
|
-
console.log(` ${ko ? "처리 시간" : "Elapsed"} ${C.dim}${elapsed}ms${C.reset}`);
|
|
186
|
-
console.log();
|
|
187
|
-
}
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { generateHologram } from "../core/hologram";
|
|
4
|
+
import { getSystemLanguage } from "../core/locale";
|
|
5
|
+
|
|
6
|
+
// ── ANSI helpers ──
|
|
7
|
+
const C = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
bold: "\x1b[1m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
white: "\x1b[37m",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface FileResult {
|
|
19
|
+
path: string;
|
|
20
|
+
originalLines: number;
|
|
21
|
+
hologramLines: number;
|
|
22
|
+
originalChars: number;
|
|
23
|
+
hologramChars: number;
|
|
24
|
+
savings: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function collectTsFiles(dir: string, base: string): string[] {
|
|
28
|
+
const results: string[] = [];
|
|
29
|
+
const skipDirs = new Set(["node_modules", ".git", ".afd", "dist", "coverage", ".omc"]);
|
|
30
|
+
|
|
31
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
if (!skipDirs.has(entry.name)) {
|
|
34
|
+
results.push(...collectTsFiles(join(dir, entry.name), base));
|
|
35
|
+
}
|
|
36
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
37
|
+
results.push(join(dir, entry.name));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatChars(n: number): string {
|
|
44
|
+
if (n < 1000) return `${n}`;
|
|
45
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}K`;
|
|
46
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function savingsColor(pct: number): string {
|
|
50
|
+
if (pct >= 80) return C.green;
|
|
51
|
+
if (pct >= 50) return C.yellow;
|
|
52
|
+
return C.red;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function padRight(s: string, w: number): string {
|
|
56
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
57
|
+
return s + " ".repeat(Math.max(0, w - stripped.length));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function padLeft(s: string, w: number): string {
|
|
61
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
62
|
+
return " ".repeat(Math.max(0, w - stripped.length)) + s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function benchmarkCommand(options: { sort?: string; top?: string; json?: boolean }) {
|
|
66
|
+
const lang = getSystemLanguage();
|
|
67
|
+
const ko = lang === "ko";
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
const files = collectTsFiles(join(cwd, "src"), cwd);
|
|
70
|
+
|
|
71
|
+
if (files.length === 0) {
|
|
72
|
+
console.error(`${C.red}[afd] ${ko ? "src/ 디렉토리에 TS/JS 파일이 없습니다." : "No TS/JS files found in src/."}${C.reset}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const results: FileResult[] = [];
|
|
77
|
+
const startTime = performance.now();
|
|
78
|
+
|
|
79
|
+
for (const filePath of files) {
|
|
80
|
+
try {
|
|
81
|
+
const source = readFileSync(filePath, "utf-8");
|
|
82
|
+
const { hologram, originalLength, hologramLength, savings } = await generateHologram(filePath, source);
|
|
83
|
+
results.push({
|
|
84
|
+
path: relative(cwd, filePath),
|
|
85
|
+
originalLines: source.split("\n").length,
|
|
86
|
+
hologramLines: hologram.split("\n").length,
|
|
87
|
+
originalChars: originalLength,
|
|
88
|
+
hologramChars: hologramLength,
|
|
89
|
+
savings: Math.round(savings * 10) / 10,
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
// Skip files that fail to parse
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
97
|
+
|
|
98
|
+
// Sort
|
|
99
|
+
const sortKey = options.sort ?? "savings";
|
|
100
|
+
results.sort((a, b) => {
|
|
101
|
+
if (sortKey === "size") return b.originalChars - a.originalChars;
|
|
102
|
+
if (sortKey === "name") return a.path.localeCompare(b.path);
|
|
103
|
+
return b.savings - a.savings; // default: savings desc
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const limit = options.top ? parseInt(options.top, 10) : results.length;
|
|
107
|
+
const display = results.slice(0, limit);
|
|
108
|
+
|
|
109
|
+
// JSON output
|
|
110
|
+
if (options.json) {
|
|
111
|
+
const totalOriginal = results.reduce((s, r) => s + r.originalChars, 0);
|
|
112
|
+
const totalHologram = results.reduce((s, r) => s + r.hologramChars, 0);
|
|
113
|
+
console.log(JSON.stringify({
|
|
114
|
+
files: results.length,
|
|
115
|
+
totalOriginalChars: totalOriginal,
|
|
116
|
+
totalHologramChars: totalHologram,
|
|
117
|
+
totalSavedChars: totalOriginal - totalHologram,
|
|
118
|
+
overallCompression: totalOriginal > 0 ? Math.round((1 - totalHologram / totalOriginal) * 1000) / 10 : 0,
|
|
119
|
+
estimatedTokensSaved: Math.round((totalOriginal - totalHologram) / 4),
|
|
120
|
+
elapsedMs: elapsed,
|
|
121
|
+
results: results.map(r => ({
|
|
122
|
+
path: r.path,
|
|
123
|
+
originalLines: r.originalLines,
|
|
124
|
+
hologramLines: r.hologramLines,
|
|
125
|
+
originalChars: r.originalChars,
|
|
126
|
+
hologramChars: r.hologramChars,
|
|
127
|
+
savings: r.savings,
|
|
128
|
+
})),
|
|
129
|
+
}, null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Header ──
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(`${C.bold}${C.cyan} ╔══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
136
|
+
console.log(`${C.bold}${C.cyan} ║ ${ko ? "홀로그램 AST 압축 벤치마크" : "Hologram AST Compression Benchmark"} ║${C.reset}`);
|
|
137
|
+
console.log(`${C.bold}${C.cyan} ╚══════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
138
|
+
console.log();
|
|
139
|
+
|
|
140
|
+
// ── Table Header ──
|
|
141
|
+
const colFile = ko ? "파일" : "File";
|
|
142
|
+
const colLines = ko ? "원본줄" : "Lines";
|
|
143
|
+
const colHolo = ko ? "홀로줄" : "Holo";
|
|
144
|
+
const colOrig = ko ? "원본" : "Original";
|
|
145
|
+
const colComp = ko ? "압축" : "Compressed";
|
|
146
|
+
const colSave = ko ? "절감률" : "Savings";
|
|
147
|
+
|
|
148
|
+
console.log(
|
|
149
|
+
` ${C.dim}${padRight(colFile, 40)} ${padLeft(colLines, 6)} ${padLeft(colHolo, 6)} ${padLeft(colOrig, 8)} ${padLeft(colComp, 8)} ${padLeft(colSave, 8)}${C.reset}`
|
|
150
|
+
);
|
|
151
|
+
console.log(` ${C.dim}${"─".repeat(80)}${C.reset}`);
|
|
152
|
+
|
|
153
|
+
// ── Rows ──
|
|
154
|
+
for (const r of display) {
|
|
155
|
+
const sc = savingsColor(r.savings);
|
|
156
|
+
const savingsStr = `${sc}${r.savings.toFixed(1)}%${C.reset}`;
|
|
157
|
+
console.log(
|
|
158
|
+
` ${padRight(r.path, 40)} ${padLeft(String(r.originalLines), 6)} ${padLeft(String(r.hologramLines), 6)} ${padLeft(formatChars(r.originalChars), 8)} ${padLeft(formatChars(r.hologramChars), 8)} ${padLeft(savingsStr, 8)}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (limit < results.length) {
|
|
163
|
+
console.log(` ${C.dim}... ${ko ? `외 ${results.length - limit}개 파일` : `and ${results.length - limit} more files`}${C.reset}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Summary ──
|
|
167
|
+
const totalOriginal = results.reduce((s, r) => s + r.originalChars, 0);
|
|
168
|
+
const totalHologram = results.reduce((s, r) => s + r.hologramChars, 0);
|
|
169
|
+
const totalSaved = totalOriginal - totalHologram;
|
|
170
|
+
const overallPct = totalOriginal > 0 ? Math.round((1 - totalHologram / totalOriginal) * 1000) / 10 : 0;
|
|
171
|
+
const estimatedTokens = Math.round(totalSaved / 4);
|
|
172
|
+
const high = results.filter(r => r.savings >= 70).length;
|
|
173
|
+
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(` ${C.dim}${"─".repeat(80)}${C.reset}`);
|
|
176
|
+
console.log(` ${C.bold}${ko ? "요약" : "Summary"}${C.reset}`);
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(` ${ko ? "분석 파일" : "Files analyzed"} ${C.bold}${results.length}${C.reset}`);
|
|
179
|
+
console.log(` ${ko ? "전체 압축률" : "Overall compression"} ${C.bold}${savingsColor(overallPct)}${overallPct}%${C.reset}`);
|
|
180
|
+
console.log(` ${ko ? "원본 크기" : "Original size"} ${C.bold}${formatChars(totalOriginal)}${C.reset} ${C.dim}(${(totalOriginal / 1024).toFixed(0)} KB)${C.reset}`);
|
|
181
|
+
console.log(` ${ko ? "압축 크기" : "Compressed size"} ${C.bold}${formatChars(totalHologram)}${C.reset} ${C.dim}(${(totalHologram / 1024).toFixed(0)} KB)${C.reset}`);
|
|
182
|
+
console.log(` ${ko ? "절약 크기" : "Saved"} ${C.bold}${C.green}${formatChars(totalSaved)}${C.reset} ${C.dim}(${(totalSaved / 1024).toFixed(0)} KB)${C.reset}`);
|
|
183
|
+
console.log(` ${ko ? "추정 토큰 절약" : "Est. tokens saved"} ${C.bold}${C.green}~${estimatedTokens.toLocaleString()}${C.reset}`);
|
|
184
|
+
console.log(` ${ko ? "70%+ 압축 파일" : "70%+ compression"} ${C.bold}${high}${C.reset}/${results.length} ${C.dim}(${Math.round(high / results.length * 100)}%)${C.reset}`);
|
|
185
|
+
console.log(` ${ko ? "처리 시간" : "Elapsed"} ${C.dim}${elapsed}ms${C.reset}`);
|
|
186
|
+
console.log();
|
|
187
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* afd correlate — Cross-Project Pattern Correlation CLI
|
|
3
|
+
*
|
|
4
|
+
* Analyzes federated antibodies (scope != 'local') to surface Global Hotspot
|
|
5
|
+
* patterns that recur across 2+ distinct project scopes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { correlatePatterns } from "../core/correlation-engine";
|
|
9
|
+
import { generateValidator } from "../core/validator-generator";
|
|
10
|
+
import type { ValidatorGenInput } from "../core/validator-generator";
|
|
11
|
+
import { initDb } from "../core/db";
|
|
12
|
+
import { getSystemLanguage } from "../core/locale";
|
|
13
|
+
|
|
14
|
+
interface CorrelateOptions {
|
|
15
|
+
minScopes?: string;
|
|
16
|
+
apply?: boolean;
|
|
17
|
+
includeLocal?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const msgs = {
|
|
21
|
+
en: {
|
|
22
|
+
title: "afd correlate — Cross-Project Pattern Correlation",
|
|
23
|
+
noData: "No cross-project patterns found. Pull antibodies from a remote store first:\n afd sync --pull --remote <url>",
|
|
24
|
+
noDataLocal: "No cross-project patterns found even with local scope included.",
|
|
25
|
+
header: "Global Hotspot patterns detected across multiple projects:",
|
|
26
|
+
scopes: "scopes",
|
|
27
|
+
occurrences: "occurrences",
|
|
28
|
+
covered: "validator exists",
|
|
29
|
+
uncovered: "no validator",
|
|
30
|
+
variants: "variants",
|
|
31
|
+
scopeList: "projects",
|
|
32
|
+
applyTitle: "Auto-generating global validators for uncovered hotspots...",
|
|
33
|
+
applyDone: (n: number) => `${n} global validator(s) generated. Daemon will hot-reload automatically.`,
|
|
34
|
+
applySkipped: (n: number) => `${n} hotspot(s) already covered — skipped.`,
|
|
35
|
+
hint: "Run `afd correlate --apply` to generate global validators for uncovered hotspots.",
|
|
36
|
+
confidence: "confidence",
|
|
37
|
+
communityVerified: "Community Verified",
|
|
38
|
+
minScopesLabel: "Min scopes",
|
|
39
|
+
totalScopes: "Scopes in dataset",
|
|
40
|
+
},
|
|
41
|
+
ko: {
|
|
42
|
+
title: "afd correlate — 크로스 프로젝트 패턴 상관관계",
|
|
43
|
+
noData: "크로스 프로젝트 패턴을 찾지 못했습니다. 원격 스토어에서 항체를 먼저 Pull 하세요:\n afd sync --pull --remote <url>",
|
|
44
|
+
noDataLocal: "로컬 스코프 포함 시에도 크로스 프로젝트 패턴을 찾지 못했습니다.",
|
|
45
|
+
header: "여러 프로젝트에서 감지된 글로벌 핫스팟 패턴:",
|
|
46
|
+
scopes: "스코프",
|
|
47
|
+
occurrences: "발생",
|
|
48
|
+
covered: "검증기 있음",
|
|
49
|
+
uncovered: "검증기 없음",
|
|
50
|
+
variants: "변형",
|
|
51
|
+
scopeList: "프로젝트",
|
|
52
|
+
applyTitle: "미보호 핫스팟에 대한 글로벌 검증기 자동 생성 중...",
|
|
53
|
+
applyDone: (n: number) => `${n}개 글로벌 검증기 생성 완료. 데몬이 자동으로 핫 리로드합니다.`,
|
|
54
|
+
applySkipped: (n: number) => `${n}개 핫스팟은 이미 보호됨 — 건너뜀.`,
|
|
55
|
+
hint: "`afd correlate --apply`를 실행하여 미보호 핫스팟의 글로벌 검증기를 자동 생성하세요.",
|
|
56
|
+
confidence: "신뢰도",
|
|
57
|
+
communityVerified: "커뮤니티 검증됨",
|
|
58
|
+
minScopesLabel: "최소 스코프",
|
|
59
|
+
totalScopes: "데이터셋 내 스코프",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
|
|
64
|
+
const W = 62;
|
|
65
|
+
|
|
66
|
+
function hline(l: string, r: string) { return `${l}${BOX.h.repeat(W)}${r}`; }
|
|
67
|
+
function row(s: string) {
|
|
68
|
+
const pad = Math.max(0, W - 2 - s.length);
|
|
69
|
+
return `${BOX.v} ${s}${" ".repeat(pad)} ${BOX.v}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function correlateCommand(opts: CorrelateOptions = {}) {
|
|
73
|
+
const lang = getSystemLanguage();
|
|
74
|
+
const m = msgs[lang];
|
|
75
|
+
|
|
76
|
+
const minScopes = parseInt(opts.minScopes ?? "2", 10) || 2;
|
|
77
|
+
const includeLocal = opts.includeLocal ?? false;
|
|
78
|
+
|
|
79
|
+
const db = initDb();
|
|
80
|
+
try {
|
|
81
|
+
const result = correlatePatterns(db, { minScopes, includeLocal, limit: 10 });
|
|
82
|
+
|
|
83
|
+
if (result.hotspots.length === 0) {
|
|
84
|
+
console.log(`[afd correlate] ${includeLocal ? m.noDataLocal : m.noData}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Apply mode ──────────────────────────────────────────────────────────
|
|
89
|
+
if (opts.apply) {
|
|
90
|
+
const uncovered = result.hotspots.filter(h => !h.alreadyCovered);
|
|
91
|
+
const covered = result.hotspots.filter(h => h.alreadyCovered);
|
|
92
|
+
|
|
93
|
+
if (uncovered.length === 0) {
|
|
94
|
+
console.log(`[afd correlate] ${m.applySkipped(covered.length)}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`[afd correlate] ${m.applyTitle}`);
|
|
99
|
+
let generated = 0;
|
|
100
|
+
|
|
101
|
+
for (const h of uncovered) {
|
|
102
|
+
const input = buildGlobalValidatorInput(h.canonicalType);
|
|
103
|
+
if (!input) continue;
|
|
104
|
+
const res = generateValidator(input);
|
|
105
|
+
if (res.written) {
|
|
106
|
+
console.log(` ✅ ${res.filename} (${h.scopeCount} ${m.scopes})`);
|
|
107
|
+
generated++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`[afd correlate] ${m.applyDone(generated)}`);
|
|
112
|
+
if (covered.length > 0) {
|
|
113
|
+
console.log(`[afd correlate] ${m.applySkipped(covered.length)}`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Display mode ────────────────────────────────────────────────────────
|
|
119
|
+
console.log("");
|
|
120
|
+
console.log(hline(BOX.tl, BOX.tr));
|
|
121
|
+
console.log(row(`🌐 ${m.title}`));
|
|
122
|
+
console.log(hline(BOX.ml, BOX.mr));
|
|
123
|
+
console.log(row(`${m.totalScopes}: ${result.totalScopes} | ${m.minScopesLabel}: ${minScopes}`));
|
|
124
|
+
console.log(hline(BOX.ml, BOX.mr));
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < result.hotspots.length; i++) {
|
|
127
|
+
const h = result.hotspots[i];
|
|
128
|
+
const statusIcon = h.alreadyCovered ? "🛡️" : "⚠️";
|
|
129
|
+
const statusText = h.alreadyCovered ? m.covered : m.uncovered;
|
|
130
|
+
const rank = `#${i + 1}`;
|
|
131
|
+
const confStr = `${Math.round(h.confidence * 100)}%`;
|
|
132
|
+
|
|
133
|
+
console.log(row(`${rank} ${statusIcon} ${h.canonicalType}`));
|
|
134
|
+
console.log(row(` ${h.scopeCount} ${m.scopes} | ${h.totalOccurrences} ${m.occurrences} | ${m.confidence}: ${confStr}`));
|
|
135
|
+
console.log(row(` ${m.scopeList}: ${h.scopes.join(", ")}`));
|
|
136
|
+
console.log(row(` ${statusText}`));
|
|
137
|
+
|
|
138
|
+
if (h.variants.length > 1) {
|
|
139
|
+
const maxVarW = W - 16;
|
|
140
|
+
const varStr = h.variants.slice(1).join(", ");
|
|
141
|
+
const truncated = varStr.length > maxVarW ? varStr.slice(0, maxVarW - 3) + "..." : varStr;
|
|
142
|
+
console.log(row(` ↳ ${m.variants}: ${truncated}`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (i < result.hotspots.length - 1) {
|
|
146
|
+
console.log(row(""));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const uncoveredCount = result.hotspots.filter(h => !h.alreadyCovered).length;
|
|
151
|
+
if (uncoveredCount > 0) {
|
|
152
|
+
console.log(hline(BOX.ml, BOX.mr));
|
|
153
|
+
console.log(row(`💡 ${m.hint}`));
|
|
154
|
+
}
|
|
155
|
+
console.log(hline(BOX.bl, BOX.br));
|
|
156
|
+
} finally {
|
|
157
|
+
db.close();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a ValidatorGenInput for a global hotspot based on its pattern type.
|
|
163
|
+
*/
|
|
164
|
+
function buildGlobalValidatorInput(patternType: string): ValidatorGenInput | null {
|
|
165
|
+
const pt = patternType.toLowerCase();
|
|
166
|
+
|
|
167
|
+
if (pt.includes("delet") || pt.includes("removal")) {
|
|
168
|
+
return { failureType: "deletion", originalPath: "global", corruptedContent: "DELETED", restoredContent: null };
|
|
169
|
+
}
|
|
170
|
+
if (pt.includes("empty") || pt.includes("blank")) {
|
|
171
|
+
return { failureType: "corruption", originalPath: "global", corruptedContent: "", restoredContent: null };
|
|
172
|
+
}
|
|
173
|
+
if (pt.includes("truncat")) {
|
|
174
|
+
return { failureType: "corruption", originalPath: "global", corruptedContent: "x", restoredContent: null };
|
|
175
|
+
}
|
|
176
|
+
if (pt.includes("json") || pt.includes("syntax") || pt.includes("parse")) {
|
|
177
|
+
return { failureType: "corruption", originalPath: "global.json", corruptedContent: "{invalid", restoredContent: null };
|
|
178
|
+
}
|
|
179
|
+
return { failureType: "corruption", originalPath: "global", corruptedContent: "corrupted", restoredContent: null };
|
|
180
|
+
}
|