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.
- package/CHANGELOG.md +85 -46
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -337
- package/mcp-config.json +10 -10
- package/package.json +14 -6
- package/src/adapters/index.ts +370 -159
- package/src/cli.ts +162 -57
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +190 -0
- package/src/commands/fix.ts +158 -138
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -41
- package/src/commands/mcp.ts +129 -0
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +276 -208
- package/src/commands/start.ts +155 -96
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +68 -49
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +567 -21
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +32 -8
- package/src/core/boast.ts +280 -265
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -46
- package/src/core/discovery.ts +65 -65
- package/src/core/evolution.ts +215 -0
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -0
- package/src/core/hologram/types.ts +27 -0
- package/src/core/hologram.ts +73 -243
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -266
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +74 -66
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-engine.ts +287 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +78 -37
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +376 -0
- package/src/daemon/mcp-handler.ts +575 -0
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -504
- package/src/daemon/types.ts +121 -0
- package/src/daemon/workspace-map.ts +104 -0
- package/src/platform.ts +60 -39
- package/src/version.ts +15 -0
- package/README.ko.md +0 -306
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vaccine Registry — central antibody package system
|
|
3
|
+
*
|
|
4
|
+
* Manages vaccine packages that can be published, searched, and installed.
|
|
5
|
+
* Registry index is stored at .afd/registry/index.json
|
|
6
|
+
* Each package is a directory under .afd/registry/packages/<name>/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { AFD_DIR } from "../constants";
|
|
12
|
+
|
|
13
|
+
export interface VaccinePackage {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
description: string;
|
|
17
|
+
author: string;
|
|
18
|
+
ecosystem: string;
|
|
19
|
+
antibodies: VaccineAntibodyDef[];
|
|
20
|
+
createdAt: string;
|
|
21
|
+
signature?: string; // future: cryptographic signature
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VaccineAntibodyDef {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
description: string;
|
|
28
|
+
severity: "critical" | "warning" | "info";
|
|
29
|
+
condition: {
|
|
30
|
+
type: string;
|
|
31
|
+
path: string;
|
|
32
|
+
pattern?: string;
|
|
33
|
+
minLength?: number;
|
|
34
|
+
};
|
|
35
|
+
patches: { op: string; path: string; value?: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RegistryIndex {
|
|
39
|
+
version: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
packages: RegistryEntry[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RegistryEntry {
|
|
45
|
+
name: string;
|
|
46
|
+
version: string;
|
|
47
|
+
description: string;
|
|
48
|
+
author: string;
|
|
49
|
+
ecosystem: string;
|
|
50
|
+
antibodyCount: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const REGISTRY_DIR = join(AFD_DIR, "registry");
|
|
54
|
+
const PACKAGES_DIR = join(REGISTRY_DIR, "packages");
|
|
55
|
+
const INDEX_FILE = join(REGISTRY_DIR, "index.json");
|
|
56
|
+
|
|
57
|
+
function ensureDirs() {
|
|
58
|
+
mkdirSync(PACKAGES_DIR, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadIndex(): RegistryIndex {
|
|
62
|
+
if (!existsSync(INDEX_FILE)) {
|
|
63
|
+
return { version: "1.0.0", updatedAt: new Date().toISOString(), packages: [] };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(INDEX_FILE, "utf-8"));
|
|
67
|
+
} catch {
|
|
68
|
+
return { version: "1.0.0", updatedAt: new Date().toISOString(), packages: [] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveIndex(index: RegistryIndex) {
|
|
73
|
+
ensureDirs();
|
|
74
|
+
index.updatedAt = new Date().toISOString();
|
|
75
|
+
writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), "utf-8");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Publish a vaccine package to the local registry */
|
|
79
|
+
export function publishPackage(pkg: VaccinePackage): { success: boolean; message: string } {
|
|
80
|
+
ensureDirs();
|
|
81
|
+
|
|
82
|
+
const pkgDir = join(PACKAGES_DIR, pkg.name);
|
|
83
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
84
|
+
writeFileSync(join(pkgDir, "vaccine.json"), JSON.stringify(pkg, null, 2), "utf-8");
|
|
85
|
+
|
|
86
|
+
// Update index
|
|
87
|
+
const index = loadIndex();
|
|
88
|
+
const existing = index.packages.findIndex(p => p.name === pkg.name);
|
|
89
|
+
const entry: RegistryEntry = {
|
|
90
|
+
name: pkg.name,
|
|
91
|
+
version: pkg.version,
|
|
92
|
+
description: pkg.description,
|
|
93
|
+
author: pkg.author,
|
|
94
|
+
ecosystem: pkg.ecosystem,
|
|
95
|
+
antibodyCount: pkg.antibodies.length,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (existing >= 0) {
|
|
99
|
+
index.packages[existing] = entry;
|
|
100
|
+
} else {
|
|
101
|
+
index.packages.push(entry);
|
|
102
|
+
}
|
|
103
|
+
saveIndex(index);
|
|
104
|
+
|
|
105
|
+
return { success: true, message: `Published ${pkg.name}@${pkg.version} (${pkg.antibodies.length} antibodies)` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Search packages in the registry */
|
|
109
|
+
export function searchPackages(query?: string): RegistryEntry[] {
|
|
110
|
+
const index = loadIndex();
|
|
111
|
+
if (!query) return index.packages;
|
|
112
|
+
const q = query.toLowerCase();
|
|
113
|
+
return index.packages.filter(p =>
|
|
114
|
+
p.name.toLowerCase().includes(q) ||
|
|
115
|
+
p.description.toLowerCase().includes(q) ||
|
|
116
|
+
p.ecosystem.toLowerCase().includes(q)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get full package details */
|
|
121
|
+
export function getPackage(name: string): VaccinePackage | null {
|
|
122
|
+
const pkgFile = join(PACKAGES_DIR, name, "vaccine.json");
|
|
123
|
+
if (!existsSync(pkgFile)) return null;
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(readFileSync(pkgFile, "utf-8"));
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Install a package — writes antibody rules to .afd/rules/ */
|
|
132
|
+
export function installPackage(name: string): { success: boolean; installed: number; message: string } {
|
|
133
|
+
const pkg = getPackage(name);
|
|
134
|
+
if (!pkg) return { success: false, installed: 0, message: `Package '${name}' not found` };
|
|
135
|
+
|
|
136
|
+
const rulesDir = join(AFD_DIR, "rules");
|
|
137
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
let installed = 0;
|
|
140
|
+
for (const ab of pkg.antibodies) {
|
|
141
|
+
const ruleContent = buildYamlRule(ab);
|
|
142
|
+
const ruleFile = join(rulesDir, `${pkg.name}-${ab.id}.yml`);
|
|
143
|
+
writeFileSync(ruleFile, ruleContent, "utf-8");
|
|
144
|
+
installed++;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
success: true,
|
|
149
|
+
installed,
|
|
150
|
+
message: `Installed ${pkg.name}@${pkg.version}: ${installed} rules added to .afd/rules/`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** List installed packages (by checking .afd/rules/ for package-prefixed files) */
|
|
155
|
+
export function listInstalled(): string[] {
|
|
156
|
+
const rulesDir = join(AFD_DIR, "rules");
|
|
157
|
+
if (!existsSync(rulesDir)) return [];
|
|
158
|
+
if (!existsSync(PACKAGES_DIR)) return [];
|
|
159
|
+
|
|
160
|
+
// Get all known package names from registry
|
|
161
|
+
let knownPackages: string[];
|
|
162
|
+
try {
|
|
163
|
+
knownPackages = readdirSync(PACKAGES_DIR);
|
|
164
|
+
} catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const files = readdirSync(rulesDir).filter(f => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
169
|
+
const installed = new Set<string>();
|
|
170
|
+
|
|
171
|
+
for (const pkgName of knownPackages) {
|
|
172
|
+
// Check if any rule file starts with this package name
|
|
173
|
+
if (files.some(f => f.startsWith(`${pkgName}-`))) {
|
|
174
|
+
installed.add(pkgName);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [...installed];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildYamlRule(ab: VaccineAntibodyDef): string {
|
|
182
|
+
const lines: string[] = [
|
|
183
|
+
`# Auto-generated by afd vaccine registry`,
|
|
184
|
+
`id: ${ab.id}`,
|
|
185
|
+
`title: "${ab.title}"`,
|
|
186
|
+
`description: "${ab.description}"`,
|
|
187
|
+
`severity: ${ab.severity}`,
|
|
188
|
+
`condition:`,
|
|
189
|
+
` type: ${ab.condition.type}`,
|
|
190
|
+
` path: ${ab.condition.path}`,
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
if (ab.condition.pattern) {
|
|
194
|
+
lines.push(` pattern: "${ab.condition.pattern}"`);
|
|
195
|
+
}
|
|
196
|
+
if (ab.condition.minLength !== undefined) {
|
|
197
|
+
lines.push(` minLength: ${ab.condition.minLength}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (ab.patches.length > 0) {
|
|
201
|
+
lines.push(`patches:`);
|
|
202
|
+
for (const p of ab.patches) {
|
|
203
|
+
lines.push(` - op: ${p.op}`);
|
|
204
|
+
lines.push(` path: ${p.path}`);
|
|
205
|
+
if (p.value !== undefined) {
|
|
206
|
+
lines.push(` value: "${p.value.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return lines.join("\n") + "\n";
|
|
212
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML parser — handles flat keys, nested objects, and simple arrays.
|
|
3
|
+
* No external dependencies. Sufficient for .afd/rules/*.yml diagnostic rule files.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* key: value
|
|
7
|
+
* key:
|
|
8
|
+
* nested: value
|
|
9
|
+
* items:
|
|
10
|
+
* - value
|
|
11
|
+
* items:
|
|
12
|
+
* - op: add
|
|
13
|
+
* path: /file
|
|
14
|
+
*
|
|
15
|
+
* Does NOT support: anchors, aliases, flow syntax, multi-line strings, tags.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
type YamlValue = string | number | boolean | null | YamlObject | YamlValue[];
|
|
19
|
+
interface YamlObject {
|
|
20
|
+
[key: string]: YamlValue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parse(text: string): YamlObject {
|
|
24
|
+
const lines = text.split("\n");
|
|
25
|
+
return parseBlock(lines, 0, 0).value as YamlObject;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ParseResult {
|
|
29
|
+
value: YamlValue;
|
|
30
|
+
nextLine: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getIndent(line: string): number {
|
|
34
|
+
const match = line.match(/^(\s*)/);
|
|
35
|
+
return match ? match[1].length : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isComment(line: string): boolean {
|
|
39
|
+
return line.trimStart().startsWith("#") || line.trim() === "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseScalar(raw: string): YamlValue {
|
|
43
|
+
const trimmed = raw.trim();
|
|
44
|
+
if (trimmed === "" || trimmed === "null" || trimmed === "~") return null;
|
|
45
|
+
if (trimmed === "true") return true;
|
|
46
|
+
if (trimmed === "false") return false;
|
|
47
|
+
// Remove quotes
|
|
48
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
49
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
50
|
+
return trimmed.slice(1, -1);
|
|
51
|
+
}
|
|
52
|
+
const num = Number(trimmed);
|
|
53
|
+
if (!isNaN(num) && trimmed !== "") return num;
|
|
54
|
+
return trimmed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseBlock(lines: string[], startLine: number, baseIndent: number): ParseResult {
|
|
58
|
+
const obj: YamlObject = {};
|
|
59
|
+
let i = startLine;
|
|
60
|
+
|
|
61
|
+
while (i < lines.length) {
|
|
62
|
+
const line = lines[i];
|
|
63
|
+
|
|
64
|
+
if (isComment(line)) { i++; continue; }
|
|
65
|
+
|
|
66
|
+
const indent = getIndent(line);
|
|
67
|
+
if (indent < baseIndent) break; // dedent → end of block
|
|
68
|
+
|
|
69
|
+
const content = line.trim();
|
|
70
|
+
|
|
71
|
+
// Array item at this level
|
|
72
|
+
if (content.startsWith("- ")) {
|
|
73
|
+
// This block is actually an array — delegate to array parser
|
|
74
|
+
const arr = parseArray(lines, i, indent);
|
|
75
|
+
return { value: arr.value, nextLine: arr.nextLine };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Key-value pair
|
|
79
|
+
const colonIdx = content.indexOf(":");
|
|
80
|
+
if (colonIdx === -1) { i++; continue; }
|
|
81
|
+
|
|
82
|
+
const key = content.slice(0, colonIdx).trim();
|
|
83
|
+
const rest = content.slice(colonIdx + 1).trim();
|
|
84
|
+
|
|
85
|
+
if (rest === "" || rest === "|" || rest === ">") {
|
|
86
|
+
// Check next line indent to determine nested type
|
|
87
|
+
const nextNonEmpty = findNextNonEmpty(lines, i + 1);
|
|
88
|
+
if (nextNonEmpty < lines.length) {
|
|
89
|
+
const nextIndent = getIndent(lines[nextNonEmpty]);
|
|
90
|
+
if (nextIndent > indent) {
|
|
91
|
+
const nextContent = lines[nextNonEmpty].trim();
|
|
92
|
+
if (nextContent.startsWith("- ")) {
|
|
93
|
+
const arr = parseArray(lines, nextNonEmpty, nextIndent);
|
|
94
|
+
obj[key] = arr.value;
|
|
95
|
+
i = arr.nextLine;
|
|
96
|
+
} else {
|
|
97
|
+
const nested = parseBlock(lines, nextNonEmpty, nextIndent);
|
|
98
|
+
obj[key] = nested.value;
|
|
99
|
+
i = nested.nextLine;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
obj[key] = null;
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
obj[key] = null;
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
obj[key] = parseScalar(rest);
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { value: obj, nextLine: i };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseArray(lines: string[], startLine: number, baseIndent: number): ParseResult {
|
|
119
|
+
const arr: YamlValue[] = [];
|
|
120
|
+
let i = startLine;
|
|
121
|
+
|
|
122
|
+
while (i < lines.length) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
if (isComment(line)) { i++; continue; }
|
|
125
|
+
|
|
126
|
+
const indent = getIndent(line);
|
|
127
|
+
if (indent < baseIndent) break;
|
|
128
|
+
|
|
129
|
+
const content = line.trim();
|
|
130
|
+
if (!content.startsWith("- ")) break;
|
|
131
|
+
|
|
132
|
+
const itemContent = content.slice(2).trim();
|
|
133
|
+
|
|
134
|
+
// Check if this is a simple value or an inline key-value
|
|
135
|
+
const colonIdx = itemContent.indexOf(":");
|
|
136
|
+
if (colonIdx > 0 && !itemContent.startsWith('"') && !itemContent.startsWith("'")) {
|
|
137
|
+
// Inline object start: - key: value
|
|
138
|
+
const itemObj: YamlObject = {};
|
|
139
|
+
const key = itemContent.slice(0, colonIdx).trim();
|
|
140
|
+
const val = itemContent.slice(colonIdx + 1).trim();
|
|
141
|
+
itemObj[key] = parseScalar(val);
|
|
142
|
+
i++;
|
|
143
|
+
|
|
144
|
+
// Continuation lines at deeper indent
|
|
145
|
+
const itemIndent = indent + 2;
|
|
146
|
+
while (i < lines.length) {
|
|
147
|
+
const nextLine = lines[i];
|
|
148
|
+
if (isComment(nextLine)) { i++; continue; }
|
|
149
|
+
const nextIndent = getIndent(nextLine);
|
|
150
|
+
if (nextIndent < itemIndent) break;
|
|
151
|
+
const nc = nextLine.trim();
|
|
152
|
+
if (nc.startsWith("- ")) break; // next array item
|
|
153
|
+
const ci = nc.indexOf(":");
|
|
154
|
+
if (ci > 0) {
|
|
155
|
+
itemObj[nc.slice(0, ci).trim()] = parseScalar(nc.slice(ci + 1).trim());
|
|
156
|
+
}
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
arr.push(itemObj);
|
|
161
|
+
} else {
|
|
162
|
+
// Simple value
|
|
163
|
+
arr.push(parseScalar(itemContent));
|
|
164
|
+
i++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { value: arr, nextLine: i };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function findNextNonEmpty(lines: string[], start: number): number {
|
|
172
|
+
for (let i = start; i < lines.length; i++) {
|
|
173
|
+
if (!isComment(lines[i])) return i;
|
|
174
|
+
}
|
|
175
|
+
return lines.length;
|
|
176
|
+
}
|