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.
Files changed (61) hide show
  1. package/CHANGELOG.md +85 -85
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -266
  5. package/mcp-config.json +10 -10
  6. package/package.json +4 -2
  7. package/src/adapters/index.ts +370 -370
  8. package/src/cli.ts +162 -127
  9. package/src/commands/benchmark.ts +187 -187
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/evolution.ts +84 -1
  13. package/src/commands/fix.ts +158 -158
  14. package/src/commands/lang.ts +41 -41
  15. package/src/commands/plugin.ts +110 -0
  16. package/src/commands/restart.ts +14 -14
  17. package/src/commands/score.ts +276 -276
  18. package/src/commands/start.ts +155 -155
  19. package/src/commands/status.ts +157 -157
  20. package/src/commands/stop.ts +68 -68
  21. package/src/commands/suggest.ts +211 -0
  22. package/src/commands/sync.ts +329 -16
  23. package/src/constants.ts +32 -32
  24. package/src/core/boast.ts +280 -280
  25. package/src/core/config.ts +49 -49
  26. package/src/core/correlation-engine.ts +265 -0
  27. package/src/core/db.ts +145 -117
  28. package/src/core/discovery.ts +65 -65
  29. package/src/core/federation.ts +129 -0
  30. package/src/core/hologram/engine.ts +71 -71
  31. package/src/core/hologram/fallback.ts +11 -11
  32. package/src/core/hologram/go-extractor.ts +203 -0
  33. package/src/core/hologram/incremental.ts +227 -227
  34. package/src/core/hologram/py-extractor.ts +132 -132
  35. package/src/core/hologram/rust-extractor.ts +244 -0
  36. package/src/core/hologram/ts-extractor.ts +406 -320
  37. package/src/core/hologram/types.ts +27 -25
  38. package/src/core/hologram.ts +73 -71
  39. package/src/core/i18n/messages.ts +309 -309
  40. package/src/core/locale.ts +88 -88
  41. package/src/core/log-rotate.ts +33 -33
  42. package/src/core/log-utils.ts +38 -38
  43. package/src/core/lru-map.ts +61 -61
  44. package/src/core/notify.ts +74 -74
  45. package/src/core/plugin-manager.ts +225 -0
  46. package/src/core/rule-suggestion.ts +127 -0
  47. package/src/core/validator-generator.ts +224 -0
  48. package/src/core/workspace.ts +28 -28
  49. package/src/daemon/client.ts +78 -65
  50. package/src/daemon/event-batcher.ts +108 -108
  51. package/src/daemon/guards.ts +13 -13
  52. package/src/daemon/http-routes.ts +376 -293
  53. package/src/daemon/mcp-handler.ts +575 -270
  54. package/src/daemon/mcp-subscriptions.ts +81 -0
  55. package/src/daemon/mesh.ts +51 -0
  56. package/src/daemon/server.ts +655 -590
  57. package/src/daemon/types.ts +121 -100
  58. package/src/daemon/workspace-map.ts +104 -92
  59. package/src/platform.ts +60 -60
  60. package/src/version.ts +15 -15
  61. 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
+ }
@@ -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
+ }