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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Manager — third-party validator adapter system.
|
|
3
|
+
*
|
|
4
|
+
* Plugin manifest: .afd/plugins/<name>.json
|
|
5
|
+
* Installed validator wrapper: .afd/validators/plugin-<name>.js
|
|
6
|
+
*
|
|
7
|
+
* Installation strategy:
|
|
8
|
+
* 1. `bun add <package>` → installs to workspace node_modules
|
|
9
|
+
* 2. Wrap the package's main export in .afd/validators/plugin-<name>.js
|
|
10
|
+
* 3. Write manifest to .afd/plugins/<name>.json
|
|
11
|
+
* Hot-reload is automatic (existing fs.watch on validators dir)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from "fs";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
import { spawnSync } from "child_process";
|
|
17
|
+
import { findWorkspaceRoot } from "./workspace";
|
|
18
|
+
|
|
19
|
+
// ── Public Types ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Contract for third-party validator plugins.
|
|
23
|
+
* The npm package's main export must satisfy this interface.
|
|
24
|
+
* A plugin can export a function directly or an object with a `validate` method.
|
|
25
|
+
*/
|
|
26
|
+
export interface ValidatorPlugin {
|
|
27
|
+
/** Return true if the content is CORRUPTED (should be blocked). */
|
|
28
|
+
validate(newContent: string, filePath: string): boolean;
|
|
29
|
+
/** Optional metadata shown by `afd plugin list`. */
|
|
30
|
+
meta?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PluginManifest {
|
|
38
|
+
name: string;
|
|
39
|
+
package: string;
|
|
40
|
+
version: string;
|
|
41
|
+
description: string;
|
|
42
|
+
source: "npm";
|
|
43
|
+
validatorFile: string;
|
|
44
|
+
installDate: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function pluginsDir(): string {
|
|
50
|
+
const root = findWorkspaceRoot();
|
|
51
|
+
return join(root, ".afd", "plugins");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validatorsDir(): string {
|
|
55
|
+
const root = findWorkspaceRoot();
|
|
56
|
+
return join(root, ".afd", "validators");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function manifestPath(name: string): string {
|
|
60
|
+
return join(pluginsDir(), `${name}.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function wrapperPath(name: string): string {
|
|
64
|
+
return join(validatorsDir(), `plugin-${name}.js`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Install ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export interface InstallResult {
|
|
70
|
+
success: boolean;
|
|
71
|
+
message: string;
|
|
72
|
+
manifest?: PluginManifest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function installPlugin(packageName: string): InstallResult {
|
|
76
|
+
const root = findWorkspaceRoot();
|
|
77
|
+
|
|
78
|
+
// 1. bun add <package>
|
|
79
|
+
const addResult = spawnSync("bun", ["add", packageName], {
|
|
80
|
+
cwd: root,
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
stdio: "pipe",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (addResult.status !== 0) {
|
|
86
|
+
return { success: false, message: addResult.stderr?.trim() || `Failed to install ${packageName}` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Resolve installed package entry point
|
|
90
|
+
let resolvedMain: string;
|
|
91
|
+
try {
|
|
92
|
+
resolvedMain = require.resolve(packageName, { paths: [root] });
|
|
93
|
+
} catch {
|
|
94
|
+
// Fallback: try node_modules/<package>/index.js
|
|
95
|
+
const fallback = join(root, "node_modules", packageName, "index.js");
|
|
96
|
+
if (!existsSync(fallback)) {
|
|
97
|
+
return { success: false, message: `Cannot resolve entry point for ${packageName}` };
|
|
98
|
+
}
|
|
99
|
+
resolvedMain = fallback;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Validate the plugin exports a usable validator
|
|
103
|
+
let pluginExport: unknown;
|
|
104
|
+
try {
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
106
|
+
pluginExport = require(resolvedMain);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { success: false, message: `Failed to load plugin: ${(e as Error).message}` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isDirectFn = typeof pluginExport === "function";
|
|
112
|
+
const hasValidate = typeof (pluginExport as ValidatorPlugin)?.validate === "function";
|
|
113
|
+
if (!isDirectFn && !hasValidate) {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
message: `Plugin must export a function or an object with a validate(newContent, filePath) method`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. Read package.json for version/description
|
|
121
|
+
let pkgVersion = "unknown";
|
|
122
|
+
let pkgDescription = "";
|
|
123
|
+
try {
|
|
124
|
+
const pkgJson = JSON.parse(
|
|
125
|
+
readFileSync(join(root, "node_modules", packageName, "package.json"), "utf-8")
|
|
126
|
+
);
|
|
127
|
+
pkgVersion = pkgJson.version ?? "unknown";
|
|
128
|
+
pkgDescription = pkgJson.description ?? "";
|
|
129
|
+
} catch { /* best-effort */ }
|
|
130
|
+
|
|
131
|
+
// 5. Write wrapper into .afd/validators/
|
|
132
|
+
mkdirSync(validatorsDir(), { recursive: true });
|
|
133
|
+
const safeName = packageName.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
134
|
+
const wrapper = buildWrapper(packageName, resolvedMain, isDirectFn);
|
|
135
|
+
writeFileSync(wrapperPath(safeName), wrapper, "utf-8");
|
|
136
|
+
|
|
137
|
+
// 6. Write manifest into .afd/plugins/
|
|
138
|
+
mkdirSync(pluginsDir(), { recursive: true });
|
|
139
|
+
const manifest: PluginManifest = {
|
|
140
|
+
name: safeName,
|
|
141
|
+
package: packageName,
|
|
142
|
+
version: pkgVersion,
|
|
143
|
+
description: pkgDescription,
|
|
144
|
+
source: "npm",
|
|
145
|
+
validatorFile: `plugin-${safeName}.js`,
|
|
146
|
+
installDate: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
writeFileSync(manifestPath(safeName), JSON.stringify(manifest, null, 2), "utf-8");
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
message: `Installed ${packageName}@${pkgVersion} → .afd/validators/plugin-${safeName}.js`,
|
|
153
|
+
manifest,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── List ─────────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export function listPlugins(): PluginManifest[] {
|
|
160
|
+
const dir = pluginsDir();
|
|
161
|
+
if (!existsSync(dir)) return [];
|
|
162
|
+
return readdirSync(dir)
|
|
163
|
+
.filter((f) => f.endsWith(".json"))
|
|
164
|
+
.map((f) => {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(readFileSync(join(dir, f), "utf-8")) as PluginManifest;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
.filter(Boolean) as PluginManifest[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Remove ───────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
export interface RemoveResult {
|
|
177
|
+
success: boolean;
|
|
178
|
+
message: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function removePlugin(name: string): RemoveResult {
|
|
182
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
183
|
+
const mPath = manifestPath(safeName);
|
|
184
|
+
const wPath = wrapperPath(safeName);
|
|
185
|
+
|
|
186
|
+
if (!existsSync(mPath)) {
|
|
187
|
+
return { success: false, message: `Plugin not found: ${name}` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let manifest: PluginManifest;
|
|
191
|
+
try {
|
|
192
|
+
manifest = JSON.parse(readFileSync(mPath, "utf-8"));
|
|
193
|
+
} catch {
|
|
194
|
+
return { success: false, message: `Corrupt manifest for ${name}` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Remove wrapper
|
|
198
|
+
if (existsSync(wPath)) unlinkSync(wPath);
|
|
199
|
+
|
|
200
|
+
// Remove manifest
|
|
201
|
+
unlinkSync(mPath);
|
|
202
|
+
|
|
203
|
+
// bun remove <package>
|
|
204
|
+
const root = findWorkspaceRoot();
|
|
205
|
+
spawnSync("bun", ["remove", manifest.package], { cwd: root, stdio: "pipe" });
|
|
206
|
+
|
|
207
|
+
return { success: true, message: `Removed plugin ${name} (${manifest.package})` };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Wrapper codegen ──────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function buildWrapper(packageName: string, resolvedMain: string, isDirectFn: boolean): string {
|
|
213
|
+
const rel = resolve(resolvedMain).replace(/\\/g, "/");
|
|
214
|
+
return [
|
|
215
|
+
`// [afd plugin wrapper] package: ${packageName}`,
|
|
216
|
+
`// Auto-generated — managed by \`afd plugin\`. Do not edit manually.`,
|
|
217
|
+
`const _plugin = require(${JSON.stringify(rel)});`,
|
|
218
|
+
`module.exports = function(newContent, filePath) {`,
|
|
219
|
+
isDirectFn
|
|
220
|
+
? ` return _plugin(newContent, filePath);`
|
|
221
|
+
: ` return _plugin.validate(newContent, filePath);`,
|
|
222
|
+
`};`,
|
|
223
|
+
``,
|
|
224
|
+
].join("\n");
|
|
225
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Suggestion Engine — analyzes mistake_history to recommend
|
|
3
|
+
* auto-validator generation for frequently recurring failure patterns.
|
|
4
|
+
*
|
|
5
|
+
* Query strategy: aggregate by (file_path, mistake_type), rank by frequency,
|
|
6
|
+
* filter out patterns already covered by existing validators.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { Database } from "bun:sqlite";
|
|
12
|
+
import { VALIDATORS_DIR } from "../daemon/types";
|
|
13
|
+
|
|
14
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface RuleSuggestion {
|
|
17
|
+
/** Target file path (workspace-relative) */
|
|
18
|
+
filePath: string;
|
|
19
|
+
/** Mistake category (English enum) */
|
|
20
|
+
mistakeType: string;
|
|
21
|
+
/** Number of occurrences in the analysis window */
|
|
22
|
+
frequency: number;
|
|
23
|
+
/** Most recent occurrence timestamp (epoch ms) */
|
|
24
|
+
lastSeen: number;
|
|
25
|
+
/** Representative description from the most recent event */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Whether an existing validator already covers this file */
|
|
28
|
+
alreadyCovered: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SuggestionOptions {
|
|
32
|
+
/** Analysis window in days (default: 30) */
|
|
33
|
+
days?: number;
|
|
34
|
+
/** Minimum frequency to trigger a suggestion (default: 3) */
|
|
35
|
+
minFrequency?: number;
|
|
36
|
+
/** Maximum number of suggestions to return (default: 10) */
|
|
37
|
+
limit?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Core query ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Aggregate mistake_history and produce ranked suggestions.
|
|
44
|
+
*/
|
|
45
|
+
export function suggestRules(db: Database, opts: SuggestionOptions = {}): RuleSuggestion[] {
|
|
46
|
+
const days = opts.days ?? 30;
|
|
47
|
+
const minFreq = opts.minFrequency ?? 3;
|
|
48
|
+
const limit = opts.limit ?? 10;
|
|
49
|
+
|
|
50
|
+
const cutoffMs = Date.now() - days * 86_400_000;
|
|
51
|
+
|
|
52
|
+
// Single query: group by (file_path, mistake_type), count, get latest
|
|
53
|
+
const rows = db.prepare(`
|
|
54
|
+
SELECT
|
|
55
|
+
file_path,
|
|
56
|
+
mistake_type,
|
|
57
|
+
COUNT(*) AS frequency,
|
|
58
|
+
MAX(timestamp) AS last_seen,
|
|
59
|
+
-- Get the description from the most recent entry
|
|
60
|
+
(SELECT description FROM mistake_history m2
|
|
61
|
+
WHERE m2.file_path = m1.file_path AND m2.mistake_type = m1.mistake_type
|
|
62
|
+
ORDER BY m2.timestamp DESC LIMIT 1) AS description
|
|
63
|
+
FROM mistake_history m1
|
|
64
|
+
WHERE timestamp >= ?
|
|
65
|
+
GROUP BY file_path, mistake_type
|
|
66
|
+
HAVING COUNT(*) >= ?
|
|
67
|
+
ORDER BY frequency DESC, last_seen DESC
|
|
68
|
+
LIMIT ?
|
|
69
|
+
`).all(cutoffMs, minFreq, limit) as {
|
|
70
|
+
file_path: string;
|
|
71
|
+
mistake_type: string;
|
|
72
|
+
frequency: number;
|
|
73
|
+
last_seen: number;
|
|
74
|
+
description: string;
|
|
75
|
+
}[];
|
|
76
|
+
|
|
77
|
+
const coveredFiles = getExistingValidatorTargets();
|
|
78
|
+
|
|
79
|
+
return rows.map(row => ({
|
|
80
|
+
filePath: row.file_path,
|
|
81
|
+
mistakeType: row.mistake_type,
|
|
82
|
+
frequency: row.frequency,
|
|
83
|
+
lastSeen: row.last_seen,
|
|
84
|
+
description: row.description,
|
|
85
|
+
alreadyCovered: coveredFiles.has(row.file_path),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Validator coverage detection ────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Scan existing `.afd/validators/*.js` files and extract the file targets
|
|
93
|
+
* they protect (by parsing the `endsWith("...")` pattern in the source).
|
|
94
|
+
*/
|
|
95
|
+
function getExistingValidatorTargets(): Set<string> {
|
|
96
|
+
const targets = new Set<string>();
|
|
97
|
+
const dir = VALIDATORS_DIR;
|
|
98
|
+
if (!existsSync(dir)) return targets;
|
|
99
|
+
|
|
100
|
+
let files: string[];
|
|
101
|
+
try { files = readdirSync(dir).filter(f => f.endsWith(".js")); } catch { return targets; }
|
|
102
|
+
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
try {
|
|
105
|
+
const code = readFileSync(join(dir, file), "utf-8");
|
|
106
|
+
// Extract endsWith("...") targets from generated validators
|
|
107
|
+
const matches = code.matchAll(/endsWith\(["']([^"']+)["']\)/g);
|
|
108
|
+
for (const m of matches) {
|
|
109
|
+
targets.add(m[1]);
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// skip unreadable files
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return targets;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if a specific file path is already covered by a validator.
|
|
120
|
+
*/
|
|
121
|
+
export function isFileCovered(filePath: string, coveredTargets?: Set<string>): boolean {
|
|
122
|
+
const targets = coveredTargets ?? getExistingValidatorTargets();
|
|
123
|
+
for (const target of targets) {
|
|
124
|
+
if (filePath.endsWith(target)) return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Validator Generator — converts quarantine failure patterns into
|
|
3
|
+
* executable `.js` validator scripts for `.afd/validators/`.
|
|
4
|
+
*
|
|
5
|
+
* Strategy: template-based code generation with heuristic pattern detection.
|
|
6
|
+
* Each quarantine lesson is classified into a failure category, then a
|
|
7
|
+
* category-specific template emits a self-contained validator function.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
11
|
+
import { join, extname } from "path";
|
|
12
|
+
import { VALIDATORS_DIR } from "../daemon/types";
|
|
13
|
+
|
|
14
|
+
// ── Header stamped on every generated validator ─────────────────────────────
|
|
15
|
+
|
|
16
|
+
const AUTO_HEADER = "// [afd auto-generated validator] DO NOT EDIT — regenerate with `afd evolution --generate`\n";
|
|
17
|
+
|
|
18
|
+
// ── Pattern classification ──────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
type ValidatorCategory =
|
|
21
|
+
| "prevent-deletion"
|
|
22
|
+
| "prevent-empty"
|
|
23
|
+
| "prevent-truncation"
|
|
24
|
+
| "require-valid-json"
|
|
25
|
+
| "prevent-corruption";
|
|
26
|
+
|
|
27
|
+
interface ClassifiedPattern {
|
|
28
|
+
category: ValidatorCategory;
|
|
29
|
+
/** The file path or glob that this validator targets */
|
|
30
|
+
target: string;
|
|
31
|
+
/** Extra context for the template (e.g. minimum line count) */
|
|
32
|
+
meta: Record<string, string | number>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface GeneratorResult {
|
|
36
|
+
filename: string;
|
|
37
|
+
written: boolean;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Classify a quarantine lesson into a generator category.
|
|
43
|
+
*/
|
|
44
|
+
function classify(
|
|
45
|
+
failureType: "corruption" | "deletion",
|
|
46
|
+
originalPath: string,
|
|
47
|
+
corruptedContent: string,
|
|
48
|
+
restoredContent: string | null,
|
|
49
|
+
): ClassifiedPattern {
|
|
50
|
+
const target = originalPath;
|
|
51
|
+
|
|
52
|
+
if (failureType === "deletion") {
|
|
53
|
+
return { category: "prevent-deletion", target, meta: {} };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Empty file
|
|
57
|
+
if (corruptedContent.trim().length === 0) {
|
|
58
|
+
return { category: "prevent-empty", target, meta: {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Severe truncation (>90% content loss)
|
|
62
|
+
if (restoredContent && corruptedContent.length < restoredContent.length * 0.1) {
|
|
63
|
+
const minLines = restoredContent.split("\n").length;
|
|
64
|
+
return { category: "prevent-truncation", target, meta: { minLines: Math.max(1, Math.floor(minLines * 0.3)) } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// JSON syntax error
|
|
68
|
+
if (originalPath.endsWith(".json")) {
|
|
69
|
+
try {
|
|
70
|
+
JSON.parse(corruptedContent);
|
|
71
|
+
} catch {
|
|
72
|
+
return { category: "require-valid-json", target, meta: {} };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Generic corruption fallback
|
|
77
|
+
return { category: "prevent-corruption", target, meta: {} };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Template emitters ───────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function emitPreventDeletion(target: string): string {
|
|
83
|
+
return `${AUTO_HEADER}
|
|
84
|
+
// Prevents deletion or complete emptying of: ${target}
|
|
85
|
+
module.exports = function(newContent, filePath) {
|
|
86
|
+
if (!filePath.endsWith(${JSON.stringify(normalizeTarget(target))})) return false;
|
|
87
|
+
// Content marked as DELETED or completely empty → corruption
|
|
88
|
+
if (!newContent || newContent.trim().length === 0) return true;
|
|
89
|
+
if (newContent.trim() === "DELETED") return true;
|
|
90
|
+
return false;
|
|
91
|
+
};
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function emitPreventEmpty(target: string): string {
|
|
96
|
+
return `${AUTO_HEADER}
|
|
97
|
+
// Prevents emptying of: ${target}
|
|
98
|
+
module.exports = function(newContent, filePath) {
|
|
99
|
+
if (!filePath.endsWith(${JSON.stringify(normalizeTarget(target))})) return false;
|
|
100
|
+
if (!newContent || newContent.trim().length === 0) return true;
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function emitPreventTruncation(target: string, minLines: number): string {
|
|
107
|
+
return `${AUTO_HEADER}
|
|
108
|
+
// Prevents severe truncation of: ${target} (minimum ${minLines} lines)
|
|
109
|
+
module.exports = function(newContent, filePath) {
|
|
110
|
+
if (!filePath.endsWith(${JSON.stringify(normalizeTarget(target))})) return false;
|
|
111
|
+
if (!newContent) return true;
|
|
112
|
+
var lineCount = newContent.split("\\n").length;
|
|
113
|
+
if (lineCount < ${minLines}) return true;
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function emitRequireValidJson(target: string): string {
|
|
120
|
+
return `${AUTO_HEADER}
|
|
121
|
+
// Ensures valid JSON syntax for: ${target}
|
|
122
|
+
module.exports = function(newContent, filePath) {
|
|
123
|
+
if (!filePath.endsWith(${JSON.stringify(normalizeTarget(target))})) return false;
|
|
124
|
+
if (!newContent || newContent.trim().length === 0) return true;
|
|
125
|
+
try {
|
|
126
|
+
JSON.parse(newContent);
|
|
127
|
+
return false;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function emitPreventCorruption(target: string): string {
|
|
136
|
+
return `${AUTO_HEADER}
|
|
137
|
+
// Generic corruption guard for: ${target}
|
|
138
|
+
module.exports = function(newContent, filePath) {
|
|
139
|
+
if (!filePath.endsWith(${JSON.stringify(normalizeTarget(target))})) return false;
|
|
140
|
+
// Block empty or near-empty overwrites
|
|
141
|
+
if (!newContent || newContent.trim().length < 5) return true;
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Normalize path to a suffix for endsWith matching (forward slashes) */
|
|
148
|
+
function normalizeTarget(p: string): string {
|
|
149
|
+
return p.replace(/\\/g, "/");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export interface ValidatorGenInput {
|
|
155
|
+
failureType: "corruption" | "deletion";
|
|
156
|
+
originalPath: string;
|
|
157
|
+
corruptedContent: string;
|
|
158
|
+
restoredContent: string | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate a single validator from a quarantine pattern.
|
|
163
|
+
* Returns the filename and whether it was written (skips if user-modified).
|
|
164
|
+
*/
|
|
165
|
+
export function generateValidator(input: ValidatorGenInput, baseDir?: string): GeneratorResult {
|
|
166
|
+
const dir = baseDir ?? VALIDATORS_DIR;
|
|
167
|
+
mkdirSync(dir, { recursive: true });
|
|
168
|
+
|
|
169
|
+
const pattern = classify(input.failureType, input.originalPath, input.corruptedContent, input.restoredContent);
|
|
170
|
+
const filename = buildFilename(pattern);
|
|
171
|
+
const filepath = join(dir, filename);
|
|
172
|
+
|
|
173
|
+
// Safety: do not overwrite user-modified validators
|
|
174
|
+
if (existsSync(filepath)) {
|
|
175
|
+
const existing = readFileSync(filepath, "utf-8");
|
|
176
|
+
if (!existing.startsWith(AUTO_HEADER)) {
|
|
177
|
+
return { filename, written: false, reason: "user-modified" };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let code: string;
|
|
182
|
+
switch (pattern.category) {
|
|
183
|
+
case "prevent-deletion":
|
|
184
|
+
code = emitPreventDeletion(pattern.target);
|
|
185
|
+
break;
|
|
186
|
+
case "prevent-empty":
|
|
187
|
+
code = emitPreventEmpty(pattern.target);
|
|
188
|
+
break;
|
|
189
|
+
case "prevent-truncation":
|
|
190
|
+
code = emitPreventTruncation(pattern.target, (pattern.meta.minLines as number) || 5);
|
|
191
|
+
break;
|
|
192
|
+
case "require-valid-json":
|
|
193
|
+
code = emitRequireValidJson(pattern.target);
|
|
194
|
+
break;
|
|
195
|
+
case "prevent-corruption":
|
|
196
|
+
code = emitPreventCorruption(pattern.target);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
writeFileSync(filepath, code, "utf-8");
|
|
201
|
+
return { filename, written: true, reason: "generated" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generate validators for all provided quarantine lessons.
|
|
206
|
+
* Returns summary of generated files.
|
|
207
|
+
*/
|
|
208
|
+
export function generateValidators(inputs: ValidatorGenInput[], baseDir?: string): GeneratorResult[] {
|
|
209
|
+
return inputs.map(input => generateValidator(input, baseDir));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildFilename(pattern: ClassifiedPattern): string {
|
|
213
|
+
// Produce a descriptive filename from the target path
|
|
214
|
+
const ext = extname(pattern.target);
|
|
215
|
+
const base = pattern.target
|
|
216
|
+
.replace(/^\./, "") // strip leading dot
|
|
217
|
+
.replace(/[/\\]/g, "-") // path separators to dashes
|
|
218
|
+
.replace(ext, "") // strip extension
|
|
219
|
+
.replace(/[^a-zA-Z0-9-]/g, "-")
|
|
220
|
+
.replace(/-+/g, "-")
|
|
221
|
+
.replace(/^-|-$/g, "");
|
|
222
|
+
|
|
223
|
+
return `auto-${pattern.category}-${base}${ext ? "-" + ext.slice(1) : ""}.js`;
|
|
224
|
+
}
|
package/src/core/workspace.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import { join, dirname, resolve } from "path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Walk upward from `from` to find the nearest directory containing `.afd/` or `.git/`.
|
|
6
|
-
* Returns the absolute workspace root path, or falls back to `from` itself.
|
|
7
|
-
*/
|
|
8
|
-
export function findWorkspaceRoot(from: string = process.cwd()): string {
|
|
9
|
-
let dir = resolve(from);
|
|
10
|
-
const root = dirname(dir) === dir ? dir : undefined; // filesystem root guard
|
|
11
|
-
|
|
12
|
-
while (true) {
|
|
13
|
-
if (existsSync(join(dir, ".afd")) || existsSync(join(dir, ".git"))) {
|
|
14
|
-
return dir;
|
|
15
|
-
}
|
|
16
|
-
const parent = dirname(dir);
|
|
17
|
-
if (parent === dir) break; // reached filesystem root
|
|
18
|
-
dir = parent;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Fallback: return original directory
|
|
22
|
-
return resolve(from);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Resolve an `.afd/`-relative path against the workspace root */
|
|
26
|
-
export function resolveAfdPath(relativePath: string, from?: string): string {
|
|
27
|
-
return join(findWorkspaceRoot(from), relativePath);
|
|
28
|
-
}
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join, dirname, resolve } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk upward from `from` to find the nearest directory containing `.afd/` or `.git/`.
|
|
6
|
+
* Returns the absolute workspace root path, or falls back to `from` itself.
|
|
7
|
+
*/
|
|
8
|
+
export function findWorkspaceRoot(from: string = process.cwd()): string {
|
|
9
|
+
let dir = resolve(from);
|
|
10
|
+
const root = dirname(dir) === dir ? dir : undefined; // filesystem root guard
|
|
11
|
+
|
|
12
|
+
while (true) {
|
|
13
|
+
if (existsSync(join(dir, ".afd")) || existsSync(join(dir, ".git"))) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
const parent = dirname(dir);
|
|
17
|
+
if (parent === dir) break; // reached filesystem root
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback: return original directory
|
|
22
|
+
return resolve(from);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve an `.afd/`-relative path against the workspace root */
|
|
26
|
+
export function resolveAfdPath(relativePath: string, from?: string): string {
|
|
27
|
+
return join(findWorkspaceRoot(from), relativePath);
|
|
28
|
+
}
|