Răsfoiți Sursa

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 7 luni în urmă
părinte
comite
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.
 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:
-    蓄压 (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.
 
 ## 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).
   Omit other active hooks; explain in dormantReason why they stay dormant.
 - 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.
 - 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)
       .filter((summary) => summary.chapter < input.chapterNumber)
       .sort((left, right) => left.chapter - right.chapter)
-      .slice(-4);
+      .slice(-5);
     const cadenceRows = recentSummaries.map((summary) => ({
       chapter: summary.chapter,
       title: summary.title,
@@ -963,6 +963,7 @@ export class PlannerAgent extends BaseAgent {
         "## Chapter Brief",
         `- chapterType: ${brief.chapterType}`,
         `- isGoldenOpening: ${brief.isGoldenOpening ? "true" : "false"}`,
+        ...(brief.cyclePhase ? [`- cyclePhase: ${brief.cyclePhase}`] : []),
         `- dormantReason: ${brief.dormantReason ?? "(none)"}`,
         "",
         "### 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 const CyclePhaseSchema = z.enum(["蓄压", "升级", "爆发", "后效"]);
+export const CyclePhaseSchema = z.enum([
+  "蓄压", "升级", "爆发", "后效",
+  "build-up", "escalation", "climax", "aftermath",
+]);
 export type CyclePhase = z.infer<typeof CyclePhaseSchema>;
 
 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;
     }
 
+    // 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 {
       intent: ChapterIntentSchema.parse({
         chapter: chapterNumber,
@@ -49,6 +59,10 @@ export async function loadPersistedPlan(
         mustKeep: readIntentList(sections, "Must Keep"),
         mustAvoid: readIntentList(sections, "Must Avoid"),
         styleEmphasis: readIntentList(sections, "Style Emphasis"),
+        sceneDirective: directiveMap.get("scene"),
+        arcDirective: directiveMap.get("arc"),
+        moodDirective: directiveMap.get("mood"),
+        titleDirective: directiveMap.get("title"),
         conflicts,
       }),
       brief,
@@ -167,11 +181,16 @@ function parsePersistedChapterBrief(
     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({
     chapter: chapterNumber,
     goal: readIntentScalar(sections, "Goal") ?? scalarMap.get("goal"),
     chapterType: scalarMap.get("chapterType") ?? "unspecified",
     isGoldenOpening: scalarMap.get("isGoldenOpening") === "true",
+    cyclePhase,
     beatOutline,
     hookPlan,
     propsAndSetting,