Przeglądaj źródła

fix(v13): architect parser accepts 5 required sections, legacy compat [phase 5 consolidation 2]

parseSections requires 5 sections: story_frame (or legacy story_bible),
volume_map (or legacy volume_outline), roles, book_rules, pending_hooks.
current_state and rhythm_principles are no longer required — they are
accepted silently when a legacy / migration output still emits them.

Roles requirement only fires when the new section names are used; v12
books that used story_bible + embedded character matrix still parse
without a roles block (readers fall back to character_matrix shim).

writeFoundationFiles now seeds current_state.md with a minimal placeholder
when architect produces no content, so isCompleteBookDirectory() still
resolves and the first consolidator pass has a file to append to.
Ma 6 miesięcy temu
rodzic
commit
2d688a2e56
1 zmienionych plików z 44 dodań i 14 usunięć
  1. 44 14
      packages/core/src/agents/architect.ts

+ 44 - 14
packages/core/src/agents/architect.ts

@@ -527,24 +527,41 @@ Rules:
     const legacyStoryBible = parsedSections.get("story_bible") ?? "";
     const legacyStoryBible = parsedSections.get("story_bible") ?? "";
     const legacyVolumeOutline = parsedSections.get("volume_outline") ?? "";
     const legacyVolumeOutline = parsedSections.get("volume_outline") ?? "";
     const bookRules = parsedSections.get("book_rules");
     const bookRules = parsedSections.get("book_rules");
-    const currentState = parsedSections.get("current_state");
+    // Phase 5 consolidation: current_state is no longer a required section.
+    // Legacy books (v12 / Phase 5 initial) and import/fanfic regenerations
+    // may still produce it — accept the value when present, fall through to
+    // empty seed when absent (consolidator will populate at runtime).
+    const currentStateLegacy = parsedSections.get("current_state") ?? "";
     const pendingHooksRaw = parsedSections.get("pending_hooks");
     const pendingHooksRaw = parsedSections.get("pending_hooks");
 
 
-    // Book-rules, current-state and pending-hooks are still required.
-    if (!bookRules) throw new Error("Architect output missing required section: book_rules");
-    if (!currentState) throw new Error("Architect output missing required section: current_state");
-    if (!pendingHooksRaw) throw new Error("Architect output missing required section: pending_hooks");
-
-    // At least one of the new prose sections or their legacy equivalents
-    // must be present — otherwise the foundation is unusable.
+    // 5-section required contract: story_frame (or legacy story_bible),
+    // volume_map (or legacy volume_outline), roles, book_rules, pending_hooks.
+    //
+    // Backward compat: v12 outputs used story_bible/volume_outline and
+    // embedded character data inside story_bible — they had no roles block.
+    // When the model uses ONLY legacy section names, we accept an empty roles
+    // list (consolidator/readers fall back to the character_matrix shim).
+    // When the new story_frame / volume_map names are used we require roles.
+    const usingLegacyOutlineNames = !storyFrame && !volumeMap
+      && (legacyStoryBible.length > 0 || legacyVolumeOutline.length > 0);
+
+    const missing: string[] = [];
     const effectiveStoryFrame = storyFrame || legacyStoryBible;
     const effectiveStoryFrame = storyFrame || legacyStoryBible;
     const effectiveVolumeMap = volumeMap || legacyVolumeOutline;
     const effectiveVolumeMap = volumeMap || legacyVolumeOutline;
-    if (!effectiveStoryFrame) throw new Error("Architect output missing required section: story_frame");
-    if (!effectiveVolumeMap) throw new Error("Architect output missing required section: volume_map");
+    if (!effectiveStoryFrame) missing.push("story_frame");
+    if (!effectiveVolumeMap) missing.push("volume_map");
+    if (!rolesRaw.trim() && !usingLegacyOutlineNames) missing.push("roles");
+    if (!bookRules) missing.push("book_rules");
+    if (!pendingHooksRaw) missing.push("pending_hooks");
+    if (missing.length > 0) {
+      throw new Error(
+        `Architect output missing required section${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`,
+      );
+    }
 
 
     const roles = this.parseRoles(rolesRaw);
     const roles = this.parseRoles(rolesRaw);
     const pendingHooks = this.normalizePendingHooksSection(
     const pendingHooks = this.normalizePendingHooksSection(
-      this.stripTrailingAssistantCoda(pendingHooksRaw),
+      this.stripTrailingAssistantCoda(pendingHooksRaw!),
       effectiveVolumeMap,
       effectiveVolumeMap,
     );
     );
 
 
@@ -556,8 +573,11 @@ Rules:
     return {
     return {
       storyBible,
       storyBible,
       volumeOutline,
       volumeOutline,
-      bookRules,
-      currentState,
+      bookRules: bookRules!,
+      // currentState: empty string when architect no longer emits the section;
+      // writeFoundationFiles seeds current_state.md with a placeholder so
+      // consolidator / state-bootstrap readers find a valid file on first boot.
+      currentState: currentStateLegacy,
       pendingHooks,
       pendingHooks,
       storyFrame: effectiveStoryFrame,
       storyFrame: effectiveStoryFrame,
       volumeMap: effectiveVolumeMap,
       volumeMap: effectiveVolumeMap,
@@ -712,7 +732,17 @@ Rules:
     ));
     ));
 
 
     // Runtime state files (untouched by Phase 5)
     // Runtime state files (untouched by Phase 5)
-    writes.push(writeFile(join(storyDir, "current_state.md"), output.currentState, "utf-8"));
+    // Phase 5 consolidation: architect no longer emits a current_state section,
+    // so output.currentState is usually empty. We still write the file — with
+    // a minimal seed — so isCompleteBookDirectory() sees it and the first
+    // consolidator pass has somewhere to append. Legacy books / imports that
+    // still produced the section keep their content as-is.
+    const currentStateSeed = output.currentState?.trim()
+      ? output.currentState
+      : (language === "en"
+          ? "# Current State\n\n> Seeded at book creation. Runtime state is appended by the consolidator after each chapter. Initial per-character state lives in roles/*.Current_State; load-bearing initial world facts live in pending_hooks rows with start_chapter=0.\n"
+          : "# 当前状态\n\n> 建书时占位。运行时每章之后由 consolidator 追加最新状态。每个角色的初始状态详见 roles/*.当前现状;承重的初始世界设定见 pending_hooks 里 startChapter=0 的行。\n");
+    writes.push(writeFile(join(storyDir, "current_state.md"), currentStateSeed, "utf-8"));
     writes.push(writeFile(join(storyDir, "pending_hooks.md"), output.pendingHooks, "utf-8"));
     writes.push(writeFile(join(storyDir, "pending_hooks.md"), output.pendingHooks, "utf-8"));
     writes.push(writeFile(
     writes.push(writeFile(
       join(storyDir, "emotional_arcs.md"),
       join(storyDir, "emotional_arcs.md"),