autonomous-flow-daemon 1.1.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 (75) hide show
  1. package/CHANGELOG.md +85 -46
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -337
  5. package/mcp-config.json +10 -10
  6. package/package.json +14 -6
  7. package/src/adapters/index.ts +370 -159
  8. package/src/cli.ts +162 -57
  9. package/src/commands/benchmark.ts +187 -0
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/diagnose.ts +56 -14
  13. package/src/commands/doctor.ts +243 -0
  14. package/src/commands/evolution.ts +190 -0
  15. package/src/commands/fix.ts +158 -138
  16. package/src/commands/hooks.ts +136 -0
  17. package/src/commands/lang.ts +41 -41
  18. package/src/commands/mcp.ts +129 -0
  19. package/src/commands/plugin.ts +110 -0
  20. package/src/commands/restart.ts +14 -0
  21. package/src/commands/score.ts +276 -208
  22. package/src/commands/start.ts +155 -96
  23. package/src/commands/stats.ts +103 -0
  24. package/src/commands/status.ts +157 -0
  25. package/src/commands/stop.ts +68 -49
  26. package/src/commands/suggest.ts +211 -0
  27. package/src/commands/sync.ts +567 -21
  28. package/src/commands/vaccine.ts +177 -0
  29. package/src/constants.ts +32 -8
  30. package/src/core/boast.ts +280 -265
  31. package/src/core/config.ts +49 -49
  32. package/src/core/correlation-engine.ts +265 -0
  33. package/src/core/db.ts +145 -46
  34. package/src/core/discovery.ts +65 -65
  35. package/src/core/evolution.ts +215 -0
  36. package/src/core/federation.ts +129 -0
  37. package/src/core/hologram/engine.ts +71 -0
  38. package/src/core/hologram/fallback.ts +11 -0
  39. package/src/core/hologram/go-extractor.ts +203 -0
  40. package/src/core/hologram/incremental.ts +227 -0
  41. package/src/core/hologram/py-extractor.ts +132 -0
  42. package/src/core/hologram/rust-extractor.ts +244 -0
  43. package/src/core/hologram/ts-extractor.ts +406 -0
  44. package/src/core/hologram/types.ts +27 -0
  45. package/src/core/hologram.ts +73 -243
  46. package/src/core/hook-manager.ts +259 -0
  47. package/src/core/i18n/messages.ts +309 -266
  48. package/src/core/immune.ts +8 -123
  49. package/src/core/locale.ts +88 -88
  50. package/src/core/log-rotate.ts +33 -0
  51. package/src/core/log-utils.ts +38 -0
  52. package/src/core/lru-map.ts +61 -0
  53. package/src/core/notify.ts +74 -66
  54. package/src/core/plugin-manager.ts +225 -0
  55. package/src/core/rule-engine.ts +287 -0
  56. package/src/core/rule-suggestion.ts +127 -0
  57. package/src/core/semantic-diff.ts +432 -0
  58. package/src/core/telemetry.ts +94 -0
  59. package/src/core/vaccine-registry.ts +212 -0
  60. package/src/core/validator-generator.ts +224 -0
  61. package/src/core/workspace.ts +28 -0
  62. package/src/core/yaml-minimal.ts +176 -0
  63. package/src/daemon/client.ts +78 -37
  64. package/src/daemon/event-batcher.ts +108 -0
  65. package/src/daemon/guards.ts +13 -0
  66. package/src/daemon/http-routes.ts +376 -0
  67. package/src/daemon/mcp-handler.ts +575 -0
  68. package/src/daemon/mcp-subscriptions.ts +81 -0
  69. package/src/daemon/mesh.ts +51 -0
  70. package/src/daemon/server.ts +655 -504
  71. package/src/daemon/types.ts +121 -0
  72. package/src/daemon/workspace-map.ts +104 -0
  73. package/src/platform.ts +60 -39
  74. package/src/version.ts +15 -0
  75. package/README.ko.md +0 -306
@@ -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,287 @@
1
+ /**
2
+ * Custom Diagnostic Rule Engine
3
+ *
4
+ * Loads diagnostic rules from:
5
+ * 1. Built-in rules (hardcoded IMM-001~003 equivalents)
6
+ * 2. Project rules: .afd/rules/*.yml
7
+ *
8
+ * Each rule defines a condition + severity + auto-heal patches.
9
+ */
10
+
11
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
12
+ import { join, extname } from "path";
13
+ import { parse as parseYaml } from "./yaml-minimal";
14
+
15
+ // Re-declare types locally to avoid circular import with immune.ts
16
+ interface Symptom {
17
+ id: string;
18
+ patternType: string;
19
+ fileTarget: string;
20
+ title: string;
21
+ description: string;
22
+ severity: "critical" | "warning" | "info";
23
+ patches: PatchOp[];
24
+ }
25
+
26
+ interface PatchOp {
27
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
28
+ path: string;
29
+ value?: string;
30
+ }
31
+
32
+ // ── Rule Definition ──
33
+
34
+ export interface DiagnosticRule {
35
+ id: string;
36
+ title: string;
37
+ description: string;
38
+ severity: "critical" | "warning" | "info";
39
+ condition: RuleCondition;
40
+ patches: PatchOp[];
41
+ }
42
+
43
+ export type RuleCondition =
44
+ | { type: "file-missing"; path: string }
45
+ | { type: "file-empty"; path: string; minLength?: number }
46
+ | { type: "file-invalid-json"; path: string }
47
+ | { type: "file-missing-line"; path: string; pattern: string }
48
+ | { type: "file-contains"; path: string; pattern: string }; // inverse: triggers when pattern IS found
49
+
50
+ // ── Built-in Rules (IMM-001~003) ──
51
+
52
+ const CLAUDEIGNORE_DEFAULT = `# Autonomous Flow Daemon defaults
53
+ node_modules/
54
+ dist/
55
+ .afd/
56
+ *.log
57
+ .env
58
+ `;
59
+
60
+ const HOOKS_DEFAULT = `{
61
+ "hooks": []
62
+ }
63
+ `;
64
+
65
+ const BUILTIN_RULES: DiagnosticRule[] = [
66
+ {
67
+ id: "IMM-001",
68
+ title: "Missing .claudeignore",
69
+ description:
70
+ "No .claudeignore found. Without it, AI agents ingest node_modules, build artifacts, and other noise — wasting tokens and degrading context quality.",
71
+ severity: "critical",
72
+ condition: { type: "file-missing", path: ".claudeignore" },
73
+ patches: [{ op: "add", path: "/.claudeignore", value: CLAUDEIGNORE_DEFAULT }],
74
+ },
75
+ {
76
+ id: "IMM-002",
77
+ title: "Missing or invalid hooks.json",
78
+ description:
79
+ "No valid .claude/hooks.json found. Without a hooks file, pre/post-command automation cannot be configured.",
80
+ severity: "warning",
81
+ condition: { type: "file-invalid-json", path: ".claude/hooks.json" },
82
+ patches: [{ op: "add", path: "/.claude/hooks.json", value: HOOKS_DEFAULT }],
83
+ },
84
+ {
85
+ id: "IMM-003",
86
+ title: "Missing or empty CLAUDE.md",
87
+ description:
88
+ "No CLAUDE.md found or content is too short. AI agents have no project constitution to follow.",
89
+ severity: "critical",
90
+ condition: { type: "file-empty", path: "CLAUDE.md", minLength: 20 },
91
+ patches: [{ op: "add", path: "/CLAUDE.md", value: "# Project Constitution\n\n<!-- Add project rules here -->\n" }],
92
+ },
93
+ ];
94
+
95
+ // ── Condition Evaluator ──
96
+
97
+ function evaluateCondition(cond: RuleCondition): { triggered: boolean; detail?: string } {
98
+ switch (cond.type) {
99
+ case "file-missing":
100
+ return { triggered: !existsSync(cond.path) };
101
+
102
+ case "file-empty": {
103
+ if (!existsSync(cond.path)) return { triggered: true, detail: "file not found" };
104
+ try {
105
+ const content = readFileSync(cond.path, "utf-8").trim();
106
+ const min = cond.minLength ?? 1;
107
+ return {
108
+ triggered: content.length < min,
109
+ detail: `${content.length} chars (min: ${min})`,
110
+ };
111
+ } catch {
112
+ return { triggered: true, detail: "unreadable" };
113
+ }
114
+ }
115
+
116
+ case "file-invalid-json": {
117
+ if (!existsSync(cond.path)) return { triggered: true, detail: "file not found" };
118
+ try {
119
+ JSON.parse(readFileSync(cond.path, "utf-8"));
120
+ return { triggered: false };
121
+ } catch {
122
+ return { triggered: true, detail: "invalid JSON" };
123
+ }
124
+ }
125
+
126
+ case "file-missing-line": {
127
+ if (!existsSync(cond.path)) return { triggered: true, detail: "file not found" };
128
+ try {
129
+ const content = readFileSync(cond.path, "utf-8");
130
+ const regex = new RegExp(cond.pattern);
131
+ return { triggered: !regex.test(content), detail: `pattern /${cond.pattern}/ not found` };
132
+ } catch {
133
+ return { triggered: true, detail: "unreadable" };
134
+ }
135
+ }
136
+
137
+ case "file-contains": {
138
+ if (!existsSync(cond.path)) return { triggered: false }; // file doesn't exist → pattern can't be found
139
+ try {
140
+ const content = readFileSync(cond.path, "utf-8");
141
+ const regex = new RegExp(cond.pattern);
142
+ return { triggered: regex.test(content), detail: `pattern /${cond.pattern}/ found` };
143
+ } catch {
144
+ return { triggered: false };
145
+ }
146
+ }
147
+
148
+ default:
149
+ return { triggered: false };
150
+ }
151
+ }
152
+
153
+ // ── Rule Loader ──
154
+
155
+ function loadYamlRules(rulesDir: string): DiagnosticRule[] {
156
+ if (!existsSync(rulesDir)) return [];
157
+
158
+ const rules: DiagnosticRule[] = [];
159
+
160
+ let files: string[];
161
+ try {
162
+ files = readdirSync(rulesDir).filter(f => extname(f) === ".yml" || extname(f) === ".yaml");
163
+ } catch {
164
+ return [];
165
+ }
166
+
167
+ for (const file of files) {
168
+ const filePath = join(rulesDir, file);
169
+ try {
170
+ if (!statSync(filePath).isFile()) continue;
171
+ const content = readFileSync(filePath, "utf-8");
172
+ const parsed = parseYaml(content);
173
+ if (!parsed || !parsed.id || !parsed.condition) continue;
174
+
175
+ const rule: DiagnosticRule = {
176
+ id: String(parsed.id),
177
+ title: String(parsed.title ?? parsed.id),
178
+ description: String(parsed.description ?? ""),
179
+ severity: validateSeverity(parsed.severity),
180
+ condition: parseCondition(parsed.condition),
181
+ patches: parsePatchOps(parsed.patches),
182
+ };
183
+ rules.push(rule);
184
+ } catch {
185
+ // Skip malformed rule files — crash-only design
186
+ }
187
+ }
188
+
189
+ return rules;
190
+ }
191
+
192
+ function validateSeverity(val: unknown): "critical" | "warning" | "info" {
193
+ if (val === "critical" || val === "warning" || val === "info") return val;
194
+ return "warning";
195
+ }
196
+
197
+ function parseCondition(raw: unknown): RuleCondition {
198
+ if (!raw || typeof raw !== "object") return { type: "file-missing", path: "" };
199
+ const obj = raw as Record<string, unknown>;
200
+ const type = String(obj.type ?? "file-missing");
201
+ const path = String(obj.path ?? "");
202
+
203
+ switch (type) {
204
+ case "file-missing":
205
+ return { type: "file-missing", path };
206
+ case "file-empty":
207
+ return { type: "file-empty", path, minLength: Number(obj.minLength ?? 1) };
208
+ case "file-invalid-json":
209
+ return { type: "file-invalid-json", path };
210
+ case "file-missing-line":
211
+ return { type: "file-missing-line", path, pattern: String(obj.pattern ?? "") };
212
+ case "file-contains":
213
+ return { type: "file-contains", path, pattern: String(obj.pattern ?? "") };
214
+ default:
215
+ return { type: "file-missing", path };
216
+ }
217
+ }
218
+
219
+ function parsePatchOps(raw: unknown): PatchOp[] {
220
+ if (!Array.isArray(raw)) return [];
221
+ return raw
222
+ .filter((p): p is Record<string, unknown> => p && typeof p === "object")
223
+ .map(p => ({
224
+ op: (String(p.op ?? "add")) as PatchOp["op"],
225
+ path: String(p.path ?? ""),
226
+ value: p.value !== undefined ? String(p.value) : undefined,
227
+ }));
228
+ }
229
+
230
+ // ── Public API ──
231
+
232
+ const RULES_DIR = join(".afd", "rules");
233
+
234
+ export function loadAllRules(): DiagnosticRule[] {
235
+ const custom = loadYamlRules(RULES_DIR);
236
+ // Custom rules can override built-in by ID
237
+ const customIds = new Set(custom.map(r => r.id));
238
+ const builtins = BUILTIN_RULES.filter(r => !customIds.has(r.id));
239
+ return [...builtins, ...custom];
240
+ }
241
+
242
+ export function evaluateRules(
243
+ rules: DiagnosticRule[],
244
+ knownAntibodies: string[],
245
+ opts?: { raw?: boolean },
246
+ ): { symptoms: Symptom[]; healthy: string[] } {
247
+ const symptoms: Symptom[] = [];
248
+ const healthy: string[] = [];
249
+
250
+ for (const rule of rules) {
251
+ const result = evaluateCondition(rule.condition);
252
+
253
+ if (!result.triggered) {
254
+ healthy.push("OK");
255
+ continue;
256
+ }
257
+
258
+ // In raw mode, report all symptoms regardless of antibodies
259
+ if (!opts?.raw && knownAntibodies.includes(rule.id)) {
260
+ healthy.push(`${rule.id} (immunized)`);
261
+ continue;
262
+ }
263
+
264
+ symptoms.push({
265
+ id: rule.id,
266
+ patternType: rule.condition.type,
267
+ fileTarget: "path" in rule.condition ? rule.condition.path : "",
268
+ title: rule.title,
269
+ description: result.detail
270
+ ? `${rule.description} (${result.detail})`
271
+ : rule.description,
272
+ severity: rule.severity,
273
+ patches: rule.patches,
274
+ });
275
+ }
276
+
277
+ return { symptoms, healthy };
278
+ }
279
+
280
+ /** Convenience: load rules + evaluate in one call (replaces old diagnose()) */
281
+ export function diagnoseWithRules(
282
+ knownAntibodies: string[],
283
+ opts?: { raw?: boolean },
284
+ ): { symptoms: Symptom[]; healthy: string[] } {
285
+ const rules = loadAllRules();
286
+ return evaluateRules(rules, knownAntibodies, opts);
287
+ }
@@ -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
+ }