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,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
+ }