Explorar el Código

fix(v12): persisted plan reuse, cadence window, en cyclePhase

Three issues from code review:

1. Persisted plan reuse now restores cyclePhase from brief section
   and moodDirective/titleDirective/sceneDirective/arcDirective from
   the Structured Directives section. Previously these signals were
   silently lost on plan reload.

2. Cadence lookback window changed from 4 to 5 chapters to match
   the 5-chapter objective cycle analysis window.

3. CyclePhaseSchema expanded to accept both Chinese (蓄压/升级/爆发/后效)
   and English (build-up/escalation/climax/aftermath) values. English
   planner prompt now requests English values for consistency.
Ma hace 7 meses
padre
commit
02f13bff94

+ 2 - 2
packages/core/src/agents/planner-prompts.ts

@@ -116,7 +116,7 @@ but to decide that this chapter is written *right* — making it a proper micro-
 9. Supporting characters have purpose: they drive plot, not fill scenes. Show their B-side only when needed.
 9. Supporting characters have purpose: they drive plot, not fill scenes. Show their B-side only when needed.
 10. Payoffs have costs; knife-twists leave warmth.
 10. Payoffs have costs; knife-twists leave warmth.
 11. Rhythm is driven by objective cycles, not mood buckets. Every 3-5 chapters form a mini-arc:
 11. Rhythm is driven by objective cycles, not mood buckets. Every 3-5 chapters form a mini-arc:
-    蓄压 (build pressure — new obstacles, new info) → 升级 (escalate — raise stakes, widen gaps) → 爆发 (payoff — deliver on promises, exceed expectations) → 后效 (aftermath — write change: relationships shifted, status changed, costs visible).
+    build-up (new obstacles, new info) → escalation (raise stakes, widen gaps) → climax (deliver on promises, exceed expectations) → aftermath (write change: relationships shifted, status changed, costs visible).
     Don't rotate mechanically — if the story needs consecutive pressure-building, do it. But there must be at least one payoff within every 5 chapters.
     Don't rotate mechanically — if the story needs consecutive pressure-building, do it. But there must be at least one payoff within every 5 chapters.
 
 
 ## Opening types (toolbox, not mandatory)
 ## Opening types (toolbox, not mandatory)
@@ -281,6 +281,6 @@ const EN_OUTPUT_REQUIREMENTS = `Produce ChapterBrief JSON following the core pri
   Each object must have hookId (string), movement (string), targetEffect (string).
   Each object must have hookId (string), movement (string), targetEffect (string).
   Omit other active hooks; explain in dormantReason why they stay dormant.
   Omit other active hooks; explain in dormantReason why they stay dormant.
 - propsAndSetting (string[]): People/items/locations on-page, as a flat string array.
 - propsAndSetting (string[]): People/items/locations on-page, as a flat string array.
-- cyclePhase (string, optional): This chapter's position in the mini objective cycle — "蓄压" / "升级" / "爆发" / "后效"
+- cyclePhase (string, optional): This chapter's position in the mini objective cycle — "build-up" / "escalation" / "climax" / "aftermath"
 - dormantReason (string, optional): Why other hooks stay dormant.
 - dormantReason (string, optional): Why other hooks stay dormant.
 - isGoldenOpening (boolean): true for first 5 chapters, false otherwise.`;
 - isGoldenOpening (boolean): true for first 5 chapters, false otherwise.`;

+ 2 - 1
packages/core/src/agents/planner.ts

@@ -285,7 +285,7 @@ export class PlannerAgent extends BaseAgent {
     const recentSummaries = parseChapterSummariesMarkdown(input.chapterSummaries)
     const recentSummaries = parseChapterSummariesMarkdown(input.chapterSummaries)
       .filter((summary) => summary.chapter < input.chapterNumber)
       .filter((summary) => summary.chapter < input.chapterNumber)
       .sort((left, right) => left.chapter - right.chapter)
       .sort((left, right) => left.chapter - right.chapter)
-      .slice(-4);
+      .slice(-5);
     const cadenceRows = recentSummaries.map((summary) => ({
     const cadenceRows = recentSummaries.map((summary) => ({
       chapter: summary.chapter,
       chapter: summary.chapter,
       title: summary.title,
       title: summary.title,
@@ -963,6 +963,7 @@ export class PlannerAgent extends BaseAgent {
         "## Chapter Brief",
         "## Chapter Brief",
         `- chapterType: ${brief.chapterType}`,
         `- chapterType: ${brief.chapterType}`,
         `- isGoldenOpening: ${brief.isGoldenOpening ? "true" : "false"}`,
         `- isGoldenOpening: ${brief.isGoldenOpening ? "true" : "false"}`,
+        ...(brief.cyclePhase ? [`- cyclePhase: ${brief.cyclePhase}`] : []),
         `- dormantReason: ${brief.dormantReason ?? "(none)"}`,
         `- dormantReason: ${brief.dormantReason ?? "(none)"}`,
         "",
         "",
         "### Beat Outline",
         "### Beat Outline",

+ 4 - 1
packages/core/src/models/input-governance.ts

@@ -79,7 +79,10 @@ export const ChapterHookMoveSchema = z.object({
 });
 });
 export type ChapterHookMove = z.infer<typeof ChapterHookMoveSchema>;
 export type ChapterHookMove = z.infer<typeof ChapterHookMoveSchema>;
 
 
-export const CyclePhaseSchema = z.enum(["蓄压", "升级", "爆发", "后效"]);
+export const CyclePhaseSchema = z.enum([
+  "蓄压", "升级", "爆发", "后效",
+  "build-up", "escalation", "climax", "aftermath",
+]);
 export type CyclePhase = z.infer<typeof CyclePhaseSchema>;
 export type CyclePhase = z.infer<typeof CyclePhaseSchema>;
 
 
 export const ChapterBriefSchema = z.object({
 export const ChapterBriefSchema = z.object({

+ 19 - 0
packages/core/src/pipeline/persisted-governed-plan.ts

@@ -41,6 +41,16 @@ export async function loadPersistedPlan(
       return null;
       return null;
     }
     }
 
 
+    // Restore structured directives (mood, scene, arc, title) from persisted markdown
+    const directiveLines = readIntentList(sections, "Structured Directives");
+    const directiveMap = new Map<string, string>();
+    for (const line of directiveLines) {
+      const sep = line.indexOf(":");
+      if (sep > 0) {
+        directiveMap.set(line.slice(0, sep).trim(), line.slice(sep + 1).trim());
+      }
+    }
+
     return {
     return {
       intent: ChapterIntentSchema.parse({
       intent: ChapterIntentSchema.parse({
         chapter: chapterNumber,
         chapter: chapterNumber,
@@ -49,6 +59,10 @@ export async function loadPersistedPlan(
         mustKeep: readIntentList(sections, "Must Keep"),
         mustKeep: readIntentList(sections, "Must Keep"),
         mustAvoid: readIntentList(sections, "Must Avoid"),
         mustAvoid: readIntentList(sections, "Must Avoid"),
         styleEmphasis: readIntentList(sections, "Style Emphasis"),
         styleEmphasis: readIntentList(sections, "Style Emphasis"),
+        sceneDirective: directiveMap.get("scene"),
+        arcDirective: directiveMap.get("arc"),
+        moodDirective: directiveMap.get("mood"),
+        titleDirective: directiveMap.get("title"),
         conflicts,
         conflicts,
       }),
       }),
       brief,
       brief,
@@ -167,11 +181,16 @@ function parsePersistedChapterBrief(
     return undefined;
     return undefined;
   }
   }
 
 
+  const cyclePhaseRaw = scalarMap.get("cyclePhase");
+  const validCyclePhases = new Set(["蓄压", "升级", "爆发", "后效", "build-up", "escalation", "climax", "aftermath"]);
+  const cyclePhase = cyclePhaseRaw && validCyclePhases.has(cyclePhaseRaw) ? cyclePhaseRaw : undefined;
+
   return ChapterBriefSchema.parse({
   return ChapterBriefSchema.parse({
     chapter: chapterNumber,
     chapter: chapterNumber,
     goal: readIntentScalar(sections, "Goal") ?? scalarMap.get("goal"),
     goal: readIntentScalar(sections, "Goal") ?? scalarMap.get("goal"),
     chapterType: scalarMap.get("chapterType") ?? "unspecified",
     chapterType: scalarMap.get("chapterType") ?? "unspecified",
     isGoldenOpening: scalarMap.get("isGoldenOpening") === "true",
     isGoldenOpening: scalarMap.get("isGoldenOpening") === "true",
+    cyclePhase,
     beatOutline,
     beatOutline,
     hookPlan,
     hookPlan,
     propsAndSetting,
     propsAndSetting,