Quellcode durchsuchen

refactor(v13): remove volume_outline.md mirror, all readers use volume_map [phase 5/8 cleanup 1]

Phase 5 migrated the authoritative outline to outline/volume_map.md but
left a compat mirror at volume_outline.md written by the architect. That
mirror was pure debt: readVolumeMap() already handles the legacy path as
a fallback for pre-Phase-5 books, and nothing writable needs both.

- architect.ts: drop the mirror write
- consolidator.ts: read via readVolumeMap() instead of raw volume_outline.md
- state/manager.ts: isCompleteBookDirectory accepts either outline/volume_map.md or legacy volume_outline.md (and same for story_frame/story_bible)
- pipeline/agent.ts: remove volume_outline.md from write_truth_file whitelist; update tool description
- studio/api/server.ts: remove volume_outline.md from TRUTH_FILES list
- architect-phase5 test: assert the mirror is NOT written
Ma vor 6 Monaten
Ursprung
Commit
16a78b561d

+ 4 - 3
packages/core/src/__tests__/architect-phase5.test.ts

@@ -249,9 +249,10 @@ describe("ArchitectAgent — Phase 5 prose output", () => {
     const pendingHooks = await readFile(join(storyDir, "pending_hooks.md"), "utf-8");
     const pendingHooks = await readFile(join(storyDir, "pending_hooks.md"), "utf-8");
     expect(pendingHooks).toContain("H01");
     expect(pendingHooks).toContain("H01");
 
 
-    // Legacy volume_outline.md kept as a mirror of volume_map for back-compat
-    const legacyVolume = await readFile(join(storyDir, "volume_outline.md"), "utf-8");
-    expect(legacyVolume).toBe(volumeMap);
+    // Cleanup #1: volume_outline.md mirror is NOT written anymore. All
+    // readers flow through readVolumeMap() which falls back to the legacy
+    // path only for pre-Phase-5 books that still have the file on disk.
+    await expect(readFile(join(storyDir, "volume_outline.md"), "utf-8")).rejects.toThrow();
   });
   });
 
 
   it("still requires book_rules / current_state / pending_hooks to be present", async () => {
   it("still requires book_rules / current_state / pending_hooks to be present", async () => {

+ 4 - 4
packages/core/src/agents/architect.ts

@@ -673,10 +673,10 @@ Rules:
       "utf-8",
       "utf-8",
     ));
     ));
 
 
-    // Legacy volume_outline.md retained as a compat mirror of volume_map (so
-    // older reader paths still land on real content). Runtime settlement
-    // continues to ignore this file.
-    writes.push(writeFile(join(storyDir, "volume_outline.md"), volumeMap, "utf-8"));
+    // Cleanup #1: volume_outline.md mirror removed. All readers now resolve
+    // through readVolumeMap() in utils/outline-paths.ts, which prefers
+    // outline/volume_map.md and falls back to legacy volume_outline.md for
+    // books initialized before Phase 5.
 
 
     // book_rules.md is still produced — readBookRules() depends on YAML.
     // book_rules.md is still produced — readBookRules() depends on YAML.
     writes.push(writeFile(join(storyDir, "book_rules.md"), output.bookRules, "utf-8"));
     writes.push(writeFile(join(storyDir, "book_rules.md"), output.bookRules, "utf-8"));

+ 4 - 3
packages/core/src/agents/consolidator.ts

@@ -1,6 +1,7 @@
 import { BaseAgent } from "./base.js";
 import { BaseAgent } from "./base.js";
 import { readFile, writeFile, mkdir } from "node:fs/promises";
 import { readFile, writeFile, mkdir } from "node:fs/promises";
 import { join } from "node:path";
 import { join } from "node:path";
+import { readVolumeMap } from "../utils/outline-paths.js";
 
 
 export interface ConsolidationResult {
 export interface ConsolidationResult {
   readonly volumeSummaries: string;
   readonly volumeSummaries: string;
@@ -19,19 +20,19 @@ export class ConsolidatorAgent extends BaseAgent {
 
 
   /**
   /**
    * Consolidate chapter summaries by volume.
    * Consolidate chapter summaries by volume.
-   * - Reads volume_outline to determine volume boundaries
+   * - Reads outline/volume_map.md (fallback: legacy volume_outline.md) to
+   *   determine volume boundaries
    * - For each completed volume, LLM compresses chapter summaries into a narrative paragraph
    * - For each completed volume, LLM compresses chapter summaries into a narrative paragraph
    * - Archives detailed summaries, keeps only recent volume's per-chapter rows
    * - Archives detailed summaries, keeps only recent volume's per-chapter rows
    */
    */
   async consolidate(bookDir: string): Promise<ConsolidationResult> {
   async consolidate(bookDir: string): Promise<ConsolidationResult> {
     const storyDir = join(bookDir, "story");
     const storyDir = join(bookDir, "story");
     const summariesPath = join(storyDir, "chapter_summaries.md");
     const summariesPath = join(storyDir, "chapter_summaries.md");
-    const outlinePath = join(storyDir, "volume_outline.md");
     const volumeSummariesPath = join(storyDir, "volume_summaries.md");
     const volumeSummariesPath = join(storyDir, "volume_summaries.md");
 
 
     const [summariesRaw, outlineRaw] = await Promise.all([
     const [summariesRaw, outlineRaw] = await Promise.all([
       readFile(summariesPath, "utf-8").catch(() => ""),
       readFile(summariesPath, "utf-8").catch(() => ""),
-      readFile(outlinePath, "utf-8").catch(() => ""),
+      readVolumeMap(bookDir, ""),
     ]);
     ]);
 
 
     if (!summariesRaw || !outlineRaw) {
     if (!summariesRaw || !outlineRaw) {

+ 2 - 2
packages/core/src/pipeline/agent.ts

@@ -210,7 +210,7 @@ const TOOLS: ReadonlyArray<ToolDefinition> = [
       type: "object",
       type: "object",
       properties: {
       properties: {
         bookId: { type: "string", description: "书籍ID" },
         bookId: { type: "string", description: "书籍ID" },
-        fileName: { type: "string", description: "文件名(如 outline/volume_map.md、outline/story_frame.md、book_rules.md、current_state.md、pending_hooks.md;story_bible.md 和 volume_outline.md 为兼容指针,也允许写入)" },
+        fileName: { type: "string", description: "文件名(如 outline/volume_map.md、outline/story_frame.md、book_rules.md、current_state.md、pending_hooks.md;story_bible.md 为兼容指针,也允许写入)" },
         content: { type: "string", description: "新的完整文件内容" },
         content: { type: "string", description: "新的完整文件内容" },
       },
       },
       required: ["bookId", "fileName", "content"],
       required: ["bookId", "fileName", "content"],
@@ -565,7 +565,7 @@ export async function executeAgentTool(
 
 
       // Whitelist allowed truth files
       // Whitelist allowed truth files
       const ALLOWED_FILES = [
       const ALLOWED_FILES = [
-        "story_bible.md", "volume_outline.md", "book_rules.md",
+        "story_bible.md", "book_rules.md",
         "current_state.md", "particle_ledger.md", "pending_hooks.md",
         "current_state.md", "particle_ledger.md", "pending_hooks.md",
         "chapter_summaries.md", "subplot_board.md", "emotional_arcs.md",
         "chapter_summaries.md", "subplot_board.md", "emotional_arcs.md",
         "character_matrix.md", "style_guide.md",
         "character_matrix.md", "style_guide.md",

+ 1 - 1
packages/core/src/pipeline/runner.ts

@@ -1880,7 +1880,7 @@ ${matrix}`,
    * Import existing chapters into a book. Reverse-engineers all truth files
    * Import existing chapters into a book. Reverse-engineers all truth files
    * via sequential replay so the Writer and Auditor can continue naturally.
    * via sequential replay so the Writer and Auditor can continue naturally.
    *
    *
-   * Step 1: Generate foundation (story_bible, volume_outline, book_rules) from all chapters.
+   * Step 1: Generate foundation (story_frame, volume_map, book_rules) from all chapters.
    * Step 2: Sequentially replay each chapter through ChapterAnalyzer to build truth files.
    * Step 2: Sequentially replay each chapter through ChapterAnalyzer to build truth files.
    */
    */
   async importChapters(input: ImportChaptersInput): Promise<ImportChaptersResult> {
   async importChapters(input: ImportChaptersInput): Promise<ImportChaptersResult> {

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

@@ -322,17 +322,30 @@ export class StateManager {
   }
   }
 
 
   async isCompleteBookDirectory(bookDir: string): Promise<boolean> {
   async isCompleteBookDirectory(bookDir: string): Promise<boolean> {
-    const requiredPaths = [
+    // Phase 5 cleanup: prefer outline/* paths, fall back to legacy flat files
+    // so older books on disk still resolve as complete.
+    const requiredSingle = [
       join(bookDir, "book.json"),
       join(bookDir, "book.json"),
-      join(bookDir, "story", "story_bible.md"),
-      join(bookDir, "story", "volume_outline.md"),
       join(bookDir, "story", "book_rules.md"),
       join(bookDir, "story", "book_rules.md"),
       join(bookDir, "story", "current_state.md"),
       join(bookDir, "story", "current_state.md"),
       join(bookDir, "story", "pending_hooks.md"),
       join(bookDir, "story", "pending_hooks.md"),
       join(bookDir, "chapters", "index.json"),
       join(bookDir, "chapters", "index.json"),
     ];
     ];
 
 
-    for (const requiredPath of requiredPaths) {
+    const eitherOr: Array<ReadonlyArray<string>> = [
+      // story_frame (new) OR story_bible (legacy)
+      [
+        join(bookDir, "story", "outline", "story_frame.md"),
+        join(bookDir, "story", "story_bible.md"),
+      ],
+      // volume_map (new) OR volume_outline (legacy)
+      [
+        join(bookDir, "story", "outline", "volume_map.md"),
+        join(bookDir, "story", "volume_outline.md"),
+      ],
+    ];
+
+    for (const requiredPath of requiredSingle) {
       try {
       try {
         await stat(requiredPath);
         await stat(requiredPath);
       } catch {
       } catch {
@@ -340,6 +353,20 @@ export class StateManager {
       }
       }
     }
     }
 
 
+    for (const alternatives of eitherOr) {
+      let found = false;
+      for (const candidate of alternatives) {
+        try {
+          await stat(candidate);
+          found = true;
+          break;
+        } catch {
+          // try next alternative
+        }
+      }
+      if (!found) return false;
+    }
+
     return true;
     return true;
   }
   }
 
 

+ 1 - 1
packages/studio/src/api/server.ts

@@ -249,7 +249,7 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
   // --- Truth files ---
   // --- Truth files ---
 
 
   const TRUTH_FILES = [
   const TRUTH_FILES = [
-    "story_bible.md", "volume_outline.md", "current_state.md",
+    "story_bible.md", "current_state.md",
     "particle_ledger.md", "pending_hooks.md", "chapter_summaries.md",
     "particle_ledger.md", "pending_hooks.md", "chapter_summaries.md",
     "subplot_board.md", "emotional_arcs.md", "character_matrix.md",
     "subplot_board.md", "emotional_arcs.md", "character_matrix.md",
     "style_guide.md", "parent_canon.md", "fanfic_canon.md", "book_rules.md",
     "style_guide.md", "parent_canon.md", "fanfic_canon.md", "book_rules.md",