Procházet zdrojové kódy

fix: make book_rules.md parsing resilient to missing YAML frontmatter

parseBookRules now returns default rules instead of throwing when
frontmatter is missing or malformed. Also searches for --- delimiters
anywhere in the text, not just at the start.

Fixes: architect-generated book_rules without frontmatter blocking
the entire write pipeline.
majx_mac před 7 měsíci
rodič
revize
fd4a58b5b8

+ 15 - 8
packages/core/src/models/book-rules.ts

@@ -46,14 +46,21 @@ export interface ParsedBookRules {
 export function parseBookRules(raw: string): ParsedBookRules {
 export function parseBookRules(raw: string): ParsedBookRules {
   // Strip markdown code block wrappers if present (LLM often wraps output in ```md ... ```)
   // Strip markdown code block wrappers if present (LLM often wraps output in ```md ... ```)
   const stripped = raw.replace(/^```(?:md|markdown|yaml)?\s*\n/, "").replace(/\n```\s*$/, "");
   const stripped = raw.replace(/^```(?:md|markdown|yaml)?\s*\n/, "").replace(/\n```\s*$/, "");
-  const fmMatch = stripped.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
-  if (!fmMatch) {
-    throw new Error("book_rules.md missing YAML frontmatter (--- ... ---)");
-  }
 
 
-  const frontmatter = yaml.load(fmMatch[1]) as Record<string, unknown>;
-  const rules = BookRulesSchema.parse(frontmatter);
-  const body = fmMatch[2].trim();
+  // Try to find YAML frontmatter anywhere in the text (not just at the start)
+  const fmMatch = stripped.match(/---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
+  if (fmMatch) {
+    try {
+      const frontmatter = yaml.load(fmMatch[1]) as Record<string, unknown>;
+      const rules = BookRulesSchema.parse(frontmatter);
+      const body = fmMatch[2].trim();
+      return { rules, body };
+    } catch {
+      // YAML parse failed — fall through to default
+    }
+  }
 
 
-  return { rules, body };
+  // No valid frontmatter found — return default rules with the raw content as body
+  const rules = BookRulesSchema.parse({});
+  return { rules, body: stripped.trim() };
 }
 }

+ 4 - 1
packages/core/src/state/manager.ts

@@ -50,7 +50,10 @@ export class StateManager {
   async loadBookConfig(bookId: string): Promise<BookConfig> {
   async loadBookConfig(bookId: string): Promise<BookConfig> {
     const configPath = join(this.bookDir(bookId), "book.json");
     const configPath = join(this.bookDir(bookId), "book.json");
     const raw = await readFile(configPath, "utf-8");
     const raw = await readFile(configPath, "utf-8");
-    return JSON.parse(raw);
+    if (!raw.trim()) {
+      throw new Error(`book.json is empty for book "${bookId}"`);
+    }
+    return JSON.parse(raw) as BookConfig;
   }
   }
 
 
   async saveBookConfig(bookId: string, config: BookConfig): Promise<void> {
   async saveBookConfig(bookId: string, config: BookConfig): Promise<void> {