Kaynağa Gözat

feat(core): land 8 Fanqie writer rules across writer/planner/architect/validator

Eight hard rules synthesized from the 36 Fanqie tutorial articles, mapped to
the prompt + validator layer:

writer-prompts.ts
- Rule 1 字级看点密度: 300/500/1000 字硬尺 + "水文" 定义 (连续 300 字环境/回忆/心独视为水文)
- Rule 2 第一页反转: 开场 500 字内必须出现反转/反差; 开场人物上限同步收紧
- Rule 3 章节 80/20: 单章只讲 80%, 章末必须断在 action-climax; 反 "凑 2000 字"
- Rule 5 开篇人物 ≤ 2: ch1-2 ≤ 2 命名角色; ch3+ ≤ 3; 路人给身份标签不给名

planner-prompts.ts
- Rule 6 圆心法: 新增原则 13, 要求多角色同场事件给每个在场关键角色独立内心反应
- Rule 6 揭 1 埋 2 软建议: 新增原则 14 + open 段提示; 硬底线仍是 open ≥ resolve

architect.ts
- Rule 7 OKR 递归大纲: story_frame.4 末尾强制写全书 Objective (可验证终局状态);
  volume_map.3 从 "角色阶段目标" 升级为 "每卷 O + 3 KR" 递归分解
- Rule 8 前台/后台故事分层: story_frame.2 强制同时写表层冲突线与贯穿全书的暗线;
  volume_map.2 跨卷钩子必须同时覆盖前台 + 后台两层

hook-ledger-validator.ts
- Rule 4 揭 1 埋 1 硬底线: resolvedCount > 0 时 openedCount < resolvedCount 判违规
- 扩 HookLedger 接口加 newOpenCount 字段计入 [new] 占位行
- 新增 2 个测试用例覆盖揭 1 埋 1 规则正反情况, 调整既有 fixture 补 open 段

核心测试 933/933 通过; CLI/studio 的集成测试失败是此次改动前就存在的遗留问题.
Ma 6 ay önce
ebeveyn
işleme
b1cc3a7a71

+ 43 - 1
packages/core/src/__tests__/hook-ledger-validator.test.ts

@@ -75,7 +75,20 @@ describe("parseHookLedger", () => {
 
 
   it("returns empty lists when no ledger section is present", () => {
   it("returns empty lists when no ledger section is present", () => {
     const ledger = parseHookLedger("## 当前任务\n正文\n\n## 不要做\n- 无");
     const ledger = parseHookLedger("## 当前任务\n正文\n\n## 不要做\n- 无");
-    expect(ledger).toEqual({ open: [], advance: [], resolve: [], defer: [] });
+    expect(ledger).toEqual({ open: [], advance: [], resolve: [], defer: [], newOpenCount: 0 });
+  });
+
+  it("counts [new] placeholder lines under open as new hooks opened", () => {
+    const memo = `## 本章 hook 账
+open:
+- [new] 下一卷伏笔 || 理由
+- [new] 第二条埋点 || 理由
+advance:
+- H001 "x" → y
+`;
+    const ledger = parseHookLedger(memo);
+    expect(ledger.open).toEqual([]); // [new] lines have no id → not in .open
+    expect(ledger.newOpenCount).toBe(2);
   });
   });
 
 
   it("stops at the next H2 heading and does not pollute across sections", () => {
   it("stops at the next H2 heading and does not pollute across sections", () => {
@@ -170,8 +183,37 @@ advance:
     expect(violations).toEqual([]);
     expect(violations).toEqual([]);
   });
   });
 
 
+  it("flags 揭 1 埋 1 violation when a chapter resolves hooks without opening any", () => {
+    const memo = `## 本章 hook 账
+advance:
+- H007 "胖虎借条" → planted
+resolve:
+- H003 "杂役腰牌" → 林秋主动摘下
+`;
+    const draft = "林秋翻看胖虎借条,随后摘下杂役腰牌。";
+    const violations = validateHookLedger(memo, draft);
+    expect(violations).toHaveLength(1);
+    expect(violations[0]!.category).toContain("揭 1 埋 1");
+  });
+
+  it("accepts 揭 1 埋 1 floor when a [new] line balances the resolved hook", () => {
+    const memo = `## 本章 hook 账
+open:
+- [new] 母亲留下的半枚玉佩 || 理由:下一卷线索
+advance:
+- H007 "胖虎借条" → planted
+resolve:
+- H003 "杂役腰牌" → 林秋主动摘下
+`;
+    const draft = "林秋翻看胖虎借条,随后摘下杂役腰牌。";
+    const violations = validateHookLedger(memo, draft);
+    expect(violations).toEqual([]);
+  });
+
   it("does not let placeholder 无 raise a false critical", () => {
   it("does not let placeholder 无 raise a false critical", () => {
     const memo = `## 本章 hook 账
     const memo = `## 本章 hook 账
+open:
+- [new] 下一卷伏笔 || 理由
 advance:
 advance:
 - 无
 - 无
 resolve:
 resolve:

+ 46 - 10
packages/core/src/agents/architect.ts

@@ -240,15 +240,23 @@ ${eraBlock}
 ### 段 1:主题与基调
 ### 段 1:主题与基调
 写这本书到底讲的是什么——不是"讲主角如何从弱到强"这种空话,而是具体的命题("一个被时代按在泥里的人,如何选择不被改写"、"当所有人都在撒谎时,坚持记录真相要付出什么代价")。主题下面跟着基调——温情冷冽悲壮肃杀,哪一种?为什么是这种而不是另一种?结尾用一句话指向主角并引向 roles(例:"本书主角是林辞,完整弧线详见 roles/主要角色/林辞.md")。
 写这本书到底讲的是什么——不是"讲主角如何从弱到强"这种空话,而是具体的命题("一个被时代按在泥里的人,如何选择不被改写"、"当所有人都在撒谎时,坚持记录真相要付出什么代价")。主题下面跟着基调——温情冷冽悲壮肃杀,哪一种?为什么是这种而不是另一种?结尾用一句话指向主角并引向 roles(例:"本书主角是林辞,完整弧线详见 roles/主要角色/林辞.md")。
 
 
-### 段 2:核心冲突与对手定性
+### 段 2:核心冲突、对手定性、前台/后台双层故事
 这本书的主要矛盾是什么?不是"正邪对抗",而是"因为 A 相信 X、B 相信 Y,所以他们一定会在某件事上对撞"。主要对手是谁(至少 2 个:一个显性对手 + 一个结构性对手/体制),他们的动机从哪里长出来。对手不是工具,对手有自己的逻辑。
 这本书的主要矛盾是什么?不是"正邪对抗",而是"因为 A 相信 X、B 相信 Y,所以他们一定会在某件事上对撞"。主要对手是谁(至少 2 个:一个显性对手 + 一个结构性对手/体制),他们的动机从哪里长出来。对手不是工具,对手有自己的逻辑。
 
 
+**本段必须显式写出"前台故事 / 后台故事"两条线**(番茄老师弈青锋的"台前台后"分层法):
+- **前台故事**:读者每章看得到的表层冲突(查案、打怪、升级、谈恋爱、搞事业等),每个卷/arc 有独立的显性目标和完结点
+- **后台故事**:贯穿全书的暗线——藏在所有前台事件背后的那台"机器"(幕后黑手、阴谋、身世秘密、体制压迫、命运诅咒等),读者只能通过碎片拼出来,大结局时才整体兑现
+
+两条线必须有因果关联,不能是平行宇宙——每一段前台冲突的背后都应该能追溯到后台故事的某个齿轮在转。**如果只有前台没有后台,故事会散成"独立事件集",没有往前拉的引力;如果只有后台没有前台,故事会憋闷、看不到爽感**。本段用散文明确写出:本书前台是什么、后台是什么、两者怎么咬合。
+
 ### 段 3:世界观底色(铁律 + 质感 + 本书专属规则)
 ### 段 3:世界观底色(铁律 + 质感 + 本书专属规则)
 这个世界的运行规则是什么?3-5 条**不可违反的铁律**——以 prose 写出,不要 bullet。这个世界的质感是什么——湿的还是干的、快的还是慢的、噪的还是静的?给 writer 一个明确的感官锚(这是原来 particle_ledger 承载的基调部分)。**这一段同时承担原先 book_rules 正文里写的"叙事视角 / 本书专属规则 / 核心冲突驱动"等 prose 内容**——全部合并到这里写一次就够,不要再去 book_rules 重复。
 这个世界的运行规则是什么?3-5 条**不可违反的铁律**——以 prose 写出,不要 bullet。这个世界的质感是什么——湿的还是干的、快的还是慢的、噪的还是静的?给 writer 一个明确的感官锚(这是原来 particle_ledger 承载的基调部分)。**这一段同时承担原先 book_rules 正文里写的"叙事视角 / 本书专属规则 / 核心冲突驱动"等 prose 内容**——全部合并到这里写一次就够,不要再去 book_rules 重复。
 
 
-### 段 4:终局方向
+### 段 4:终局方向 + 全书 Objective(OKR 大纲的根)
 这本书最后一章大概是什么感觉——不是"主角登顶"、"大结局"这种套话,而是**最后一个镜头**大致长什么样。主角最后在哪、做什么、身边有谁、心里想什么。这是给全书所有后面的规划一个远方靶子。
 这本书最后一章大概是什么感觉——不是"主角登顶"、"大结局"这种套话,而是**最后一个镜头**大致长什么样。主角最后在哪、做什么、身边有谁、心里想什么。这是给全书所有后面的规划一个远方靶子。
 
 
+**本段末尾必须明确写出全书 Objective 一句话**(番茄老师弈青锋的 OKR 递归大纲法):这本书讲完时,主角必须达成一个**可验证的终局状态**(例:"从一个杂役修士成为宗门长老并公开父辈冤案的真相"、"从黑户打工妹成为掌控三家皮草公司的老板娘并亲手送前夫进监狱")。不要写"变强"、"复仇"这类抽象词,要写**一个能被外部观察者判定"达成 / 未达成"的具体状态**。这个 Objective 是全书 OKR 递归大纲的根——下面 volume_map 的每一卷会分解出这个 O 对应的 Key Results。
+
 === SECTION: volume_map ===
 === SECTION: volume_map ===
 
 
 这是分卷散文地图,**5 段主体 + 1 段节奏原则尾段**。**关键要求:只写到卷级 prose**——写清楚每卷的主题、情绪曲线、卷间钩子、角色阶段目标、卷尾不可逆事件。**禁止指定具体章号任务**(不要写"第 17 章让他回家"这种章级布局)。章级规划是 Phase 3 planner 的职责,架构师只搭骨架、不编章目。
 这是分卷散文地图,**5 段主体 + 1 段节奏原则尾段**。**关键要求:只写到卷级 prose**——写清楚每卷的主题、情绪曲线、卷间钩子、角色阶段目标、卷尾不可逆事件。**禁止指定具体章号任务**(不要写"第 17 章让他回家"这种章级布局)。章级规划是 Phase 3 planner 的职责,架构师只搭骨架、不编章目。
@@ -256,11 +264,21 @@ ${eraBlock}
 ### 段 1:各卷主题与情绪曲线
 ### 段 1:各卷主题与情绪曲线
 有几卷?每卷的主题一句话,每卷的情绪曲线一段(哪里压、哪里爽、哪里冷、哪里暖)。不要机械的"第一卷打小怪第二卷打大怪",写情绪的流动。
 有几卷?每卷的主题一句话,每卷的情绪曲线一段(哪里压、哪里爽、哪里冷、哪里暖)。不要机械的"第一卷打小怪第二卷打大怪",写情绪的流动。
 
 
-### 段 2:卷间钩子与回收承诺
+### 段 2:卷间钩子与回收承诺(前台/后台双层都要覆盖)
 第 1 卷埋什么钩子、在哪一卷回收;第 2 卷埋什么、在哪一卷回收。散文写,不要表格。**只写卷级**(如"第 1 卷埋的身世之谜在第 3 卷回收"),不要写具体章号。
 第 1 卷埋什么钩子、在哪一卷回收;第 2 卷埋什么、在哪一卷回收。散文写,不要表格。**只写卷级**(如"第 1 卷埋的身世之谜在第 3 卷回收"),不要写具体章号。
 
 
-### 段 3:角色阶段性目标
-主角在第 1 卷末要到什么状态?第 2 卷末?每一卷结束时主角的身份/关系/能力/心境应该是什么。次要角色的阶段性变化也要点到(师父在第 2 卷会死、对手在第 3 卷会黑化等)。写阶段性,不写完整弧线(完整弧线在 roles)。
+**钩子必须覆盖前台 + 后台两层**(对应 story_frame.段 2 建立的双层故事):
+- 前台钩子:当前卷内 arc 层面的短期钩子(查案谜题、对手身份、资源争夺等),预期在 1-2 卷内回收
+- 后台钩子:贯穿全书的主线钩子(幕后真相、身世、体制秘密等),预期在终卷前后回收,核心的 3-7 条属于 core_hook=true
+
+**如果本段只写前台钩子、没有后台钩子暗桩,说明你漏了整本书的引力轴,必须补上。**
+
+### 段 3:各卷 OKR(Objective + Key Results)
+用 OKR 递归大纲法分解全书 Objective(story_frame.段 4 末尾定的根 O):每一卷都必须明确给出:
+- **Objective(卷级目标)**:本卷结束时主角必须达成的**可验证状态**,一句话,与全书 Objective 逻辑递进相连(例:全书 O = "成为宗门长老并公开冤案";卷 1 O = "从杂役转入正式弟子籍并拿到第一份能指向真相的线索")
+- **Key Results(3 条,可量化/可观察)**:支撑该 O 达成的三个关键子成果,每条必须是外部观察者能判定是否完成的状态变更(例 KR1 = "拿下药园执事位置"、KR2 = "与灵安峰结成稳定盟约"、KR3 = "发现父辈案卷的第一半页残片")。不要写"变强"、"成长"这类模糊 KR
+
+次要角色的阶段性变化也要点到(师父在第 2 卷会死、对手在第 3 卷会黑化等),写在 KR 条目下作为附注。写阶段性,不写完整弧线(完整弧线在 roles)。**每一卷 3 个 KR 是下游 planner 分解章节任务的直接依据——planner 拿到一卷的 3 个 KR 后,按每 3-5 章推进一个 KR 的节奏排章。**
 
 
 ### 段 4:卷尾必须发生的改变
 ### 段 4:卷尾必须发生的改变
 每一卷最后一章必须发生什么不可逆的事——权力结构改变、关系破裂、秘密暴露、主角身份重定位。写散文,一卷一段。**只写"必须发生什么",不指定是第几章**。
 每一卷最后一章必须发生什么不可逆的事——权力结构改变、关系破裂、秘密暴露、主角身份重定位。写散文,一卷一段。**只写"必须发生什么",不指定是第几章**。
@@ -421,15 +439,23 @@ Four prose sections, ~600-900 chars each. No tables. No bullet lists. Real parag
 ## 01_Theme_and_Tonal_Ground
 ## 01_Theme_and_Tonal_Ground
 What is this book actually about — not "hero grows from weak to strong" (empty), but a concrete proposition. Then the tonal ground: warm / cold / fierce / severe — which, and why this and not another. End with a one-line pointer to the protagonist role file.
 What is this book actually about — not "hero grows from weak to strong" (empty), but a concrete proposition. Then the tonal ground: warm / cold / fierce / severe — which, and why this and not another. End with a one-line pointer to the protagonist role file.
 
 
-## 02_Core_Conflict_and_Opponent
+## 02_Core_Conflict_and_Foreground_Background_Story_Layers
 The book's main tension — not "good vs evil" but "because A believes X and B believes Y, they will inevitably collide on Z". At least two opponents: one visible, one structural/systemic. Opponents have their own logic.
 The book's main tension — not "good vs evil" but "because A believes X and B believes Y, they will inevitably collide on Z". At least two opponents: one visible, one structural/systemic. Opponents have their own logic.
 
 
+**This section must explicitly write out the foreground story / background story layers**:
+- **Foreground story**: the surface conflict the reader sees every chapter (cases, combat, leveling up, romance, business moves). Each volume / arc has its own visible goal and closure point.
+- **Background story**: the hidden machine running through the whole book — the puppet master, conspiracy, origin secret, systemic oppression, fated curse. The reader assembles it from fragments; full payoff lands near the finale.
+
+The two layers must be causally linked, not parallel universes — every foreground conflict should trace back to some gear of the background machine turning. **Foreground-only story collapses into a set of disconnected episodes with no forward pull; background-only story is suffocating and never delivers. Write both in prose here, and name how they interlock.**
+
 ## 03_World_Tonal_Ground (hard rules + sensory tone + book-specific rules)
 ## 03_World_Tonal_Ground (hard rules + sensory tone + book-specific rules)
 The world's operating rules. 3-5 unbreakable laws written as prose, not bullets. Sensory texture: wet or dry, fast or slow, noisy or quiet — give the writer an anchor. **This paragraph also absorbs the narrative prose that used to live in book_rules (narrative perspective, core conflict driver, book-specific rules).** Write them all here once. Do not repeat them in book_rules.
 The world's operating rules. 3-5 unbreakable laws written as prose, not bullets. Sensory texture: wet or dry, fast or slow, noisy or quiet — give the writer an anchor. **This paragraph also absorbs the narrative prose that used to live in book_rules (narrative perspective, core conflict driver, book-specific rules).** Write them all here once. Do not repeat them in book_rules.
 
 
-## 04_Endgame_Direction
+## 04_Endgame_Direction_and_Book_Objective
 What the last chapter roughly feels like. The final shot: where, doing what, around whom, thinking what. A distant target for every planner call downstream.
 What the last chapter roughly feels like. The final shot: where, doing what, around whom, thinking what. A distant target for every planner call downstream.
 
 
+**End this paragraph with a one-sentence Book Objective** (the root of the recursive OKR outline): when this book is done, the protagonist must reach a **verifiable end-state** (e.g., "rise from errand disciple to sect elder and publicly vindicate the parental case", "go from undocumented migrant worker to running three fur-trade companies and personally putting the ex-husband in prison"). Do NOT use vague words like "grow stronger" or "take revenge" — write a concrete state an outside observer can check "achieved / not achieved". This Book Objective is the root of the full-book OKR outline; volume_map will decompose it per volume below.
+
 === SECTION: volume_map ===
 === SECTION: volume_map ===
 
 
 Prose volume map, **5 sections + 1 closing rhythm paragraph**. **Critical requirement: stay at volume-level prose only** — specify each volume's theme, emotional curve, cross-volume hooks, character stage goals, and volume-end irreversible changes. **Do NOT prescribe chapter-level tasks** (no "chapter 17 sends him home"). Chapter planning is the Phase 3 planner's job; the architect builds the skeleton, not the chapter list.
 Prose volume map, **5 sections + 1 closing rhythm paragraph**. **Critical requirement: stay at volume-level prose only** — specify each volume's theme, emotional curve, cross-volume hooks, character stage goals, and volume-end irreversible changes. **Do NOT prescribe chapter-level tasks** (no "chapter 17 sends him home"). Chapter planning is the Phase 3 planner's job; the architect builds the skeleton, not the chapter list.
@@ -437,11 +463,21 @@ Prose volume map, **5 sections + 1 closing rhythm paragraph**. **Critical requir
 ## 01_Volume_Themes_and_Emotional_Curves
 ## 01_Volume_Themes_and_Emotional_Curves
 How many volumes? Each volume's theme in one sentence; each volume's emotional curve as a paragraph (where pressured, where rewarding, where cold, where warm). Not mechanical rotation.
 How many volumes? Each volume's theme in one sentence; each volume's emotional curve as a paragraph (where pressured, where rewarding, where cold, where warm). Not mechanical rotation.
 
 
-## 02_Cross_Volume_Hooks_and_Payoff_Promises
+## 02_Cross_Volume_Hooks_and_Payoff_Promises (cover BOTH foreground and background layers)
 Volume 1 plants hook A, paid off in volume N; volume 2 plants hook B, paid off in volume M. Prose, not tables. **Stay at volume-level** (e.g., "the origin mystery planted in volume 1 pays off in volume 3"); do not specify chapter numbers.
 Volume 1 plants hook A, paid off in volume N; volume 2 plants hook B, paid off in volume M. Prose, not tables. **Stay at volume-level** (e.g., "the origin mystery planted in volume 1 pays off in volume 3"); do not specify chapter numbers.
 
 
-## 03_Character_Stage_Goals
-Protagonist's state at end of vol 1, vol 2, ... Supporting characters' stage changes (master dies end of vol 2, opponent breaks bad in vol 3). Stage goals only — full arc lives in roles.
+**Hooks must cover BOTH foreground and background layers** (matching the two-layer story established in story_frame.02):
+- Foreground hooks: short-range arc-level hooks (case mystery, opponent identity, resource grab), paid off within 1-2 volumes
+- Background hooks: full-book main-line hooks (ultimate truth, origin, systemic secret), paid off near the finale. The 3-7 load-bearing ones are core_hook=true
+
+**If this paragraph only carries foreground hooks with no background seeds, you have lost the book's forward pull axis. Add them.**
+
+## 03_Per_Volume_OKRs (Objective + 3 Key Results)
+Recursive OKR outline that decomposes the Book Objective (root O set at the end of story_frame.04): every volume must explicitly state:
+- **Objective (volume-level goal)**: a **verifiable state** the protagonist must reach by volume end, one sentence, logically chained to the Book Objective (e.g., if Book O = "become sect elder and vindicate the parental case", then Vol 1 O = "move from errand disciple into the registered disciple roster and recover the first lead pointing to the truth")
+- **Key Results (3 items, quantifiable / observable)**: three concrete sub-achievements whose completion can be checked by an outside observer (e.g., KR1 = "take over the pharmacy garden steward seat", KR2 = "lock in a stable alliance with Lingan Peak", KR3 = "uncover the first half-page fragment of the parental case file"). No vague KRs like "gets stronger" / "matures".
+
+Supporting characters' stage changes (master dies end of vol 2, opponent breaks bad in vol 3) go as notes under the relevant KR. Stage only — full arc lives in roles. **The 3 KRs per volume are the direct input for the planner: once it sees 3 KRs for a volume, it paces chapter tasks at roughly one KR advanced every 3-5 chapters.**
 
 
 ## 04_Volume_End_Mandatory_Changes
 ## 04_Volume_End_Mandatory_Changes
 Each volume's last chapter must contain an irreversible event. Prose, one paragraph per volume. **Write what must happen, not which chapter**.
 Each volume's last chapter must contain an irreversible event. Prose, one paragraph per volume. **Write what must happen, not which chapter**.

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

@@ -22,6 +22,8 @@ export const PLANNER_MEMO_SYSTEM_PROMPT = `你是这本小说的创作总编,
 10. 五感具体化:场景描写必须有具体可视化感官细节
 10. 五感具体化:场景描写必须有具体可视化感官细节
 11. 钩子承接:每章章尾留钩
 11. 钩子承接:每章章尾留钩
 12. 钩子账本必须结账:每章对活跃 hook 做明确动作(open/advance/resolve/defer),不允许"新开一堆不回收"
 12. 钩子账本必须结账:每章对活跃 hook 做明确动作(open/advance/resolve/defer),不允许"新开一堆不回收"
+13. 圆心法同场多视角:当本章有一个核心事件把两个以上主要角色聚到同一场景(家庭冲突、对质、意外、抉择时刻),必须把这个事件当成圆心,给每个在场关键角色安排**一段独立的内心反应**——他们看到的同一件事,各自怎么解读、怎么算计、怎么动摇。memo 里用 "## 当前任务" 或 "## 日常/过渡承担什么任务" 显式说明"本章 X/Y/Z 各从自己角度过一次",不要只写一个视角
+14. 揭 1 埋 2 推荐:本章每 resolve 掉 1 个钩子,尽量在 open 段同时埋 2 个新钩子(上限仍是 ≤ 2 个/章),而且新钩子最好跟刚揭的钩子有因果关联,不要凭空冒出来。硬底线是"揭 1 埋 1"——resolve 了 N 个,open 至少 N 个,下游 validator 会卡
 
 
 ## 输出格式(严格遵守)
 ## 输出格式(严格遵守)
 
 
@@ -71,7 +73,7 @@ threadRefs:
 **这是本章对活跃伏笔的账本,写手必须按这份账动作。格式如下(每个分类下用 - 列表):**
 **这是本章对活跃伏笔的账本,写手必须按这份账动作。格式如下(每个分类下用 - 列表):**
 
 
 open:
 open:
-- [new] 新钩子描述(<=30字)|| 理由:为什么是现在开,不在本章点破(要求:本章新开钩子 ≤ 2 个)
+- [new] 新钩子描述(<=30字)|| 理由:为什么是现在开,不在本章点破(上限 ≤ 2 个;推荐:本章每 resolve 1 个钩子,open 段埋 2 个新钩子,硬底线是 open ≥ resolve)
 
 
 advance:
 advance:
 - H007 "胖虎借条" → 林秋第一次想撕,被阻止(planted → pressured)
 - H007 "胖虎借条" → 林秋第一次想撕,被阻止(planted → pressured)
@@ -124,6 +126,8 @@ Your working principles (internalize them — do not cite by number in the memo)
 10. Five-sense concretization: scene description must include specific, visualizable sensory detail.
 10. Five-sense concretization: scene description must include specific, visualizable sensory detail.
 11. Hook-passing: every chapter ends with a hook for the next.
 11. Hook-passing: every chapter ends with a hook for the next.
 12. Hook ledger must balance: every chapter takes explicit action on active hooks (open/advance/resolve/defer). "Open a pile of hooks and never resolve any" is forbidden.
 12. Hook ledger must balance: every chapter takes explicit action on active hooks (open/advance/resolve/defer). "Open a pile of hooks and never resolve any" is forbidden.
+13. Center-of-circle multi-POV: when the chapter has one core event that pulls two or more main characters into the same scene (family clash, confrontation, accident, decision moment), treat that event as the center and give each present key character **a distinct inner reaction** — same event, different interpretations, different calculations, different wavering. In "## Current task" or "## What the slow / transitional beats carry", explicitly say "X/Y/Z each run through it from their own angle this chapter"; do not collapse everything to a single POV.
+14. Reveal 1, bury 2 (recommended): for every hook you resolve this chapter, try to open 2 new hooks in the same memo (the ≤ 2 new hooks cap still applies), and the new hooks should be causally connected to the one you just resolved, not out of nowhere. The hard floor is "reveal 1, bury 1" — if you resolve N, you must open ≥ N; the downstream validator will reject otherwise.
 
 
 ## Output format (strict)
 ## Output format (strict)
 
 
@@ -173,7 +177,7 @@ if this is a pressure / conflict chapter, write "n/a — pressure chapter, no tr
 **The per-chapter accounting of active foreshadows. The writer must act on this ledger. Format (use "-" bullets under each subsection):**
 **The per-chapter accounting of active foreshadows. The writer must act on this ledger. Format (use "-" bullets under each subsection):**
 
 
 open:
 open:
-- [new] new hook description (<=30 chars) || reason: why open it now, do not pay it off this chapter (limit: ≤ 2 new hooks per chapter)
+- [new] new hook description (<=30 chars) || reason: why open it now, do not pay it off this chapter (cap ≤ 2; recommended: for each hook resolved this chapter, open 2 new hooks; hard floor is open ≥ resolve)
 
 
 advance:
 advance:
 - H007 "Huzi's IOU" → Lin Qiu tries to tear it, gets stopped (planted → pressured)
 - H007 "Huzi's IOU" → Lin Qiu tries to tear it, gets stopped (planted → pressured)

+ 27 - 8
packages/core/src/agents/writer-prompts.ts

@@ -210,6 +210,23 @@ function buildCoreRules(lengthSpec: LengthSpec): string {
 - 描写必须服务叙事:环境描写烘托氛围或暗示情节,一笔带过即可;禁止无效描写
 - 描写必须服务叙事:环境描写烘托氛围或暗示情节,一笔带过即可;禁止无效描写
 - 日常/过渡段落必须为后续剧情服务:或埋伏笔,或推进关系,或建立反差。纯填充式日常是流水账的温床
 - 日常/过渡段落必须为后续剧情服务:或埋伏笔,或推进关系,或建立反差。纯填充式日常是流水账的温床
 
 
+## 看点密集度(番茄老师鎏旗,硬尺)
+
+本章正文从头到尾必须满足以下节奏,写完后自检:
+
+- **每 300 字至少 1 个爽点**:小看点、有趣的梗、炸裂的小情节、反套路小动作、暧昧台词、情绪拉扯都算
+- **每 500 字至少 1 个钩子**:引发读者"接下来怎样"的小悬念;不要求揭开,要求抛出
+- **每 1000-1500 字至少 1 个完整悬念**:一组"问题—蓄力—未解"的结构,给读者追下去的理由
+- 不靠密度堆砌糊弄——单章里的爽点/钩子/悬念必须服务于本章 goal,不能是和主线无关的孤立段落
+- 如果某段连续 300 字以上是环境、回忆、议论、心理独白而没有推进主线或制造看点,就是水文,必须删或改
+
+## 章节 80/20 断章(番茄老师弈青锋,硬尺)
+
+- **永远不要在一章里把本章故事讲完**:本章的主剧情写到 80%,剩下 20% 留给下一章开头消化/揭示/后果
+- 章末必须断在 action-climax 的那一刻:主角刚放大招尚未见效 / 刚拔刀尚未落下 / 刚塞出银行卡尚未转身——不给结果,让读者到下一章才看到
+- 章节结构优先于字数:宁可超出目标字数几百字去完成一个完整的小高潮+断章,也不要为了卡字数切断节奏
+- 不要为了"凑 2000 字"硬加无关对话/描写;也不要为了"不超 2000 字"提前把高潮讲完
+
 ## 逻辑自洽
 ## 逻辑自洽
 
 
 - 三连反问自检:每写一个情节,反问"他为什么要这么做?""这符合他的利益吗?""这符合他之前的人设吗?"
 - 三连反问自检:每写一个情节,反问"他为什么要这么做?""这符合他的利益吗?""这符合他之前的人设吗?"
@@ -498,16 +515,16 @@ export function buildGoldenOpeningDiscipline(
   if (language === "en") {
   if (language === "en") {
     return `## Golden Opening Discipline — Chapter ${chapterNumber}
     return `## Golden Opening Discipline — Chapter ${chapterNumber}
 
 
-This is chapter ${chapterNumber} of the opening three — your prose directly decides whether the reader stays. The Golden Three Chapters rule from new.txt is a hard constraint on your sentences, not advice. Chapter 1: within the first 800 words the protagonist must trip the main-line conflict (chase, dead-end, dispossession, transmigration-as-crisis); long background paragraphs are forbidden, and worldbuilding rides on the protagonist's actions instead of being explained in a block. Chapter 2: the edge — power, system, rebirth-memory, information advantage — must be **performed** (one concrete event of using it, with a visible consequence), not **announced** (a narrator paragraph saying it exists). Chapter 3: somewhere in this chapter the protagonist's next quantifiable short-term goal must surface, so the reader can name what comes next when they close the page.
+This is chapter ${chapterNumber} of the opening three — your prose directly decides whether the reader stays. The Golden Three Chapters rule from new.txt is a hard constraint on your sentences, not advice. Chapter 1: within the first 800 words the protagonist must trip the main-line conflict (chase, dead-end, dispossession, transmigration-as-crisis); long background paragraphs are forbidden, and worldbuilding rides on the protagonist's actions instead of being explained in a block. **The last sentence of the first 300 words (the reader's first phone screen) must land a dramatic / reversal / striking beat — "Officer, I transmigrated"-level, "I'll probably die tomorrow"-level, "I'm attending my own funeral"-level — not background or scene-setting. When the reader scrolls to the bottom of the first screen they must feel pulled into the next line.** Chapter 2: the edge — power, system, rebirth-memory, information advantage — must be **performed** (one concrete event of using it, with a visible consequence), not **announced** (a narrator paragraph saying it exists). Chapter 3: somewhere in this chapter the protagonist's next quantifiable short-term goal must surface, so the reader can name what comes next when they close the page.
 
 
-The discipline that runs across all three opening chapters: paragraphs of three to five lines (mobile reading), verbs over adjectives, and every chapter ends on a small hook — a cliff, an unresolved question, or an emotional gap. At most three scenes and at most three named characters this chapter (extra people get a name only, no expansion). Information is layered into action: basic facts (looks, status, situation) emerge from what the protagonist does; key world rules (system mechanics, the deeper logic) attach to plot triggers; a paragraph of pure exposition is forbidden.`;
+The discipline that runs across all three opening chapters: paragraphs of three to five lines (mobile reading), verbs over adjectives, and every chapter ends on a small hook — a cliff, an unresolved question, or an emotional gap. **At most two scenes and at most two named characters who actually clash in the chapter (protagonist + one trigger/opponent; walk-on roles get a role label only, no name, no expansion). Editor Cong Yue's rule tightens the cap from 3 to 2 — readers already mix up 3.** Information is layered into action: basic facts (looks, status, situation) emerge from what the protagonist does; key world rules (system mechanics, the deeper logic) attach to plot triggers; a paragraph of pure exposition is forbidden.`;
   }
   }
 
 
   return `## 黄金三章写作纪律 — 第 ${chapterNumber} 章
   return `## 黄金三章写作纪律 — 第 ${chapterNumber} 章
 
 
-这是开篇三章中的第 ${chapterNumber} 章——你写出的每一句话都直接决定读者是否留下来。new.txt 的黄金三章法则对你不是建议,是对句子的硬约束。第 1 章:主角出场 800 字以内必须触发主线冲突(追杀、死局、被夺权、穿越即危机),禁止长段背景铺垫,世界观要通过主角的行动自然带出,不要整段解释。第 2 章:金手指/能力/系统/重生记忆/信息差必须"做出来"——一次具体使用的事件、一个看得见的后果——而不是"说出来"——旁白介绍它存在。第 3 章:本章中段必须让主角下一个可量化的短期目标浮上水面,读者合上页面要能说出"接下来他要干什么"。
+这是开篇三章中的第 ${chapterNumber} 章——你写出的每一句话都直接决定读者是否留下来。new.txt 的黄金三章法则对你不是建议,是对句子的硬约束。第 1 章:主角出场 800 字以内必须触发主线冲突(追杀、死局、被夺权、穿越即危机),禁止长段背景铺垫,世界观要通过主角的行动自然带出,不要整段解释。**第 1 章正文前 300 字(手机屏第一页)的最后一句必须是带戏剧性/反差/反转的收尾——警察叔叔我穿越了这类、我大概明天就要死了这类、我躺在自己的葬礼上这类——而不是介绍背景或交代环境。读者第一屏刷到页尾时必须产生"下一句是什么"的拉力。** 第 2 章:金手指/能力/系统/重生记忆/信息差必须"做出来"——一次具体使用的事件、一个看得见的后果——而不是"说出来"——旁白介绍它存在。第 3 章:本章中段必须让主角下一个可量化的短期目标浮上水面,读者合上页面要能说出"接下来他要干什么"。
 
 
-贯穿开篇三章的纪律:段落 3-5 行(手机阅读节奏),动词压过形容词,每一章结尾必有小钩子——小悬念、未解之问、情绪缺口。本章场景 ≤ 3 个、人物 ≤ 3 个(多出来的人物只报名字、不展开)。信息分层植入到动作里:基础信息(外貌、身份、处境)通过主角行动自然带出;关键设定(系统规则、世界底层)结合剧情节点揭示;禁止整段 exposition。`;
+贯穿开篇三章的纪律:段落 3-5 行(手机阅读节奏),动词压过形容词,每一章结尾必有小钩子——小悬念、未解之问、情绪缺口。**本章场景 ≤ 2 个、有名有姓参与正面冲突的人物 ≤ 2 个(主角 + 1 个触发者或对手;路人甲乙只报身份不给名字,不展开)。番茄老师丛月把开篇人物上限从 3 收紧到 2——3 个已经够读者记混,2 个最稳。** 信息分层植入到动作里:基础信息(外貌、身份、处境)通过主角行动自然带出;关键设定(系统规则、世界底层)结合剧情节点揭示;禁止整段 exposition。`;
 }
 }
 
 
 // ---------------------------------------------------------------------------
 // ---------------------------------------------------------------------------
@@ -523,7 +540,8 @@ function buildGoldenChaptersRules(chapterNumber?: number, language?: string): st
     1: `### 第一章:抛出核心冲突
     1: `### 第一章:抛出核心冲突
 - 开篇直接进入冲突场景,禁止用背景介绍/世界观设定开头
 - 开篇直接进入冲突场景,禁止用背景介绍/世界观设定开头
 - 第一段必须有动作或对话,让读者"看到"画面
 - 第一段必须有动作或对话,让读者"看到"画面
-- 开篇场景限制:最多1-2个场景,最多3个角色
+- **手机屏第一页(正文约前 300 字)的最后一句必须是戏剧性反转/反差句**,不是铺垫——警察叔叔我穿越了、我大概明天就要死了、我躺在自己的葬礼上、妻子和婆婆同时掉水里了,类似这种一句话的钩子
+- **开篇场景限制:最多 1-2 个场景,有名有姓参与正面冲突的人物上限 2 个(主角 + 1 个触发者/对手)**;路人甲乙只给身份标签("穿红衣的女人""跛脚老头")不给名字
 - 主角身份/外貌/背景通过行动自然带出,禁止资料卡式罗列
 - 主角身份/外貌/背景通过行动自然带出,禁止资料卡式罗列
 - 本章结束前,核心矛盾必须浮出水面
 - 本章结束前,核心矛盾必须浮出水面
 - 一句对话能交代的信息不要用一段叙述,角色身份、性格、地位都可以从一句有特色的台词中带出`,
 - 一句对话能交代的信息不要用一段叙述,角色身份、性格、地位都可以从一句有特色的台词中带出`,
@@ -544,7 +562,8 @@ function buildGoldenChaptersRules(chapterNumber?: number, language?: string): st
     1: `### Chapter 1: Drop into conflict
     1: `### Chapter 1: Drop into conflict
 - Open with action or dialogue — no worldbuilding preamble
 - Open with action or dialogue — no worldbuilding preamble
 - First paragraph must show a scene, not tell backstory
 - First paragraph must show a scene, not tell backstory
-- Max 1-2 locations, max 3 characters
+- **The last sentence of the first 300 words (first phone screen) must be a dramatic reversal / striking beat** — "Officer, I transmigrated"-level, "I'll probably die tomorrow"-level — not scene-setting
+- **Max 1-2 locations; max 2 named characters who actually clash in the chapter (protagonist + one trigger/opponent)**. Walk-ons get a role tag ("the woman in red", "the limping old man"), no name
 - Protagonist identity revealed through behavior, not info-dump
 - Protagonist identity revealed through behavior, not info-dump
 - Core conflict must surface before chapter end`,
 - Core conflict must surface before chapter end`,
     2: `### Chapter 2: Reveal the edge
     2: `### Chapter 2: Reveal the edge
@@ -576,7 +595,7 @@ The opening ${goldenLimit} chapters determine whether readers stay or leave. Bef
 
 
 - Start from an explosion, not the first brick
 - Start from an explosion, not the first brick
 - No info-dumps: worldbuilding reveals through action
 - No info-dumps: worldbuilding reveals through action
-- Each chapter: 1 storyline, max 3 characters
+- Each chapter: 1 storyline; **ch1-ch2 keep named characters in conflict ≤ 2** (protagonist + one), ch3+ relax to ≤ 3
 - Lead with strong emotion: injustice, danger, mystery, desire`
 - Lead with strong emotion: injustice, danger, mystery, desire`
     : `## 黄金${goldenLimit}章特殊指令(当前第${chapterNumber}章)
     : `## 黄金${goldenLimit}章特殊指令(当前第${chapterNumber}章)
 
 
@@ -584,7 +603,7 @@ The opening ${goldenLimit} chapters determine whether readers stay or leave. Bef
 
 
 - 开篇不要从第一块砖头开始砌楼——从炸了一栋楼开始写
 - 开篇不要从第一块砖头开始砌楼——从炸了一栋楼开始写
 - 禁止信息轰炸:世界观、力量体系等设定随剧情自然揭示
 - 禁止信息轰炸:世界观、力量体系等设定随剧情自然揭示
-- 每章聚焦1条故事线,人物数量控制在3个以内
+- 每章聚焦 1 条故事线;**第 1-2 章有名有姓参与正面冲突的人物 ≤ 2 个(主角 + 1 个触发者/对手),第 3 章起可放宽到 ≤ 3 个**
 - 强情绪优先:利用读者共情(亲情纽带、不公待遇、被低估)快速建立代入感`;
 - 强情绪优先:利用读者共情(亲情纽带、不公待遇、被低估)快速建立代入感`;
 
 
   return `${header}
   return `${header}

+ 51 - 8
packages/core/src/utils/hook-ledger-validator.ts

@@ -34,6 +34,14 @@ export interface HookLedger {
   readonly advance: ReadonlyArray<HookLedgerEntry>;
   readonly advance: ReadonlyArray<HookLedgerEntry>;
   readonly resolve: ReadonlyArray<HookLedgerEntry>;
   readonly resolve: ReadonlyArray<HookLedgerEntry>;
   readonly defer: ReadonlyArray<HookLedgerEntry>;
   readonly defer: ReadonlyArray<HookLedgerEntry>;
+  /**
+   * Count of `[new] ...` placeholder lines in the `open:` subsection. These
+   * are brand-new hooks declared by the planner that have no pre-existing
+   * hook_id (extractLedgerEntry rejects them because they carry no id to
+   * match downstream), but they still count as "a new hook opened" for the
+   * 揭 1 埋 1 floor check.
+   */
+  readonly newOpenCount: number;
 }
 }
 
 
 const LEDGER_HEADING_PATTERNS = [
 const LEDGER_HEADING_PATTERNS = [
@@ -56,35 +64,45 @@ const SUBSECTION_WORDS = /^(open|advance|resolve|defer|new)$/i;
 export function parseHookLedger(memoBody: string): HookLedger {
 export function parseHookLedger(memoBody: string): HookLedger {
   const section = extractLedgerSection(memoBody);
   const section = extractLedgerSection(memoBody);
   if (!section) {
   if (!section) {
-    return { open: [], advance: [], resolve: [], defer: [] };
+    return { open: [], advance: [], resolve: [], defer: [], newOpenCount: 0 };
   }
   }
 
 
-  const result: Record<keyof HookLedger, HookLedgerEntry[]> = {
+  const result: Record<"open" | "advance" | "resolve" | "defer", HookLedgerEntry[]> = {
     open: [],
     open: [],
     advance: [],
     advance: [],
     resolve: [],
     resolve: [],
     defer: [],
     defer: [],
   };
   };
+  let newOpenCount = 0;
 
 
-  let current: keyof HookLedger | null = null;
+  let current: "open" | "advance" | "resolve" | "defer" | null = null;
   for (const rawLine of section.split(/\r?\n/)) {
   for (const rawLine of section.split(/\r?\n/)) {
     const line = rawLine.trim();
     const line = rawLine.trim();
     if (line.length === 0) continue;
     if (line.length === 0) continue;
 
 
     const subHeadingMatch = line.match(/^(open|advance|resolve|defer)\s*[::]?\s*$/i);
     const subHeadingMatch = line.match(/^(open|advance|resolve|defer)\s*[::]?\s*$/i);
     if (subHeadingMatch) {
     if (subHeadingMatch) {
-      current = subHeadingMatch[1]!.toLowerCase() as keyof HookLedger;
+      current = subHeadingMatch[1]!.toLowerCase() as typeof current;
       continue;
       continue;
     }
     }
 
 
     if (!current) continue;
     if (!current) continue;
     if (!line.startsWith("-")) continue;
     if (!line.startsWith("-")) continue;
 
 
+    // `[new]` placeholder lines have no hook_id but still count as a new hook
+    // opened (揭 1 埋 1 floor check). extractLedgerEntry filters them out for
+    // advance/resolve evidence matching; we tally them separately here.
+    const cleaned = line.replace(/^-+\s*/, "").trim();
+    if (current === "open" && /^\[new\]/i.test(cleaned)) {
+      newOpenCount += 1;
+      continue;
+    }
+
     const entry = extractLedgerEntry(line);
     const entry = extractLedgerEntry(line);
     if (entry) result[current].push(entry);
     if (entry) result[current].push(entry);
   }
   }
 
 
-  return result;
+  return { ...result, newOpenCount };
 }
 }
 
 
 /**
 /**
@@ -92,16 +110,24 @@ export function parseHookLedger(memoBody: string): HookLedger {
  * evidence in the draft text. We do NOT validate `open` (new hooks don't have
  * evidence in the draft text. We do NOT validate `open` (new hooks don't have
  * a pre-existing id/descriptor to echo) or `defer` (deferred = deliberately
  * a pre-existing id/descriptor to echo) or `defer` (deferred = deliberately
  * not touched).
  * not touched).
+ *
+ * Additionally enforces the "揭 1 埋 1" hard floor (Xu Er Jia De Mao, 番茄文章
+ * 10): whenever a chapter resolves one or more hooks, it must open at least
+ * as many new hooks in the same memo. "Resolve without opening" leaves the
+ * reader feeling "解完即索然无味" — the story loses forward pull. The softer
+ * "揭 1 埋 2" rule is a planner-prompt recommendation, not a hard gate here,
+ * because enforcing ×2 would conflict with the "≤ 2 new hooks per chapter"
+ * cap on the planner side when resolve=2.
  */
  */
 export function validateHookLedger(
 export function validateHookLedger(
   memoBody: string,
   memoBody: string,
   draftContent: string,
   draftContent: string,
 ): ReadonlyArray<HookLedgerViolation> {
 ): ReadonlyArray<HookLedgerViolation> {
   const ledger = parseHookLedger(memoBody);
   const ledger = parseHookLedger(memoBody);
-  const committed = dedupeById([...ledger.advance, ...ledger.resolve]);
-  if (committed.length === 0) return [];
-
   const violations: HookLedgerViolation[] = [];
   const violations: HookLedgerViolation[] = [];
+
+  // Evidence check for everything the memo committed to land in prose.
+  const committed = dedupeById([...ledger.advance, ...ledger.resolve]);
   for (const entry of committed) {
   for (const entry of committed) {
     if (!draftEchoesEntry(draftContent, entry)) {
     if (!draftEchoesEntry(draftContent, entry)) {
       violations.push({
       violations.push({
@@ -112,6 +138,23 @@ export function validateHookLedger(
       });
       });
     }
     }
   }
   }
+
+  // "揭 1 埋 1" hard floor: when anything was resolved, at least the same
+  // number of new hooks must have been opened. We count both `[new]`
+  // placeholder lines (newOpenCount — the normal way planners declare fresh
+  // hooks without an id) and any id-bearing lines under `open:` (rare, but
+  // legal if a planner re-opens a previously paused hook).
+  const resolvedCount = ledger.resolve.length;
+  const openedCount = ledger.open.length + ledger.newOpenCount;
+  if (resolvedCount > 0 && openedCount < resolvedCount) {
+    violations.push({
+      severity: "critical",
+      category: "hook 账揭 1 埋 1 违规",
+      description: `本章 resolve 了 ${resolvedCount} 个钩子,但 open 只有 ${openedCount} 个新钩子。只揭不埋会让读者豁然开朗后索然无味,本书的前进拉力被削弱。`,
+      suggestion: `在 memo 的 open 段下至少再埋 ${resolvedCount - openedCount} 个与本章已揭钩子相关的新钩子(番茄老师徐二家的猫:"掀开一个伏笔的同时,再埋两个伏笔")。新钩子最好与已揭钩子彼此关联,不要凭空冒出来。`,
+    });
+  }
+
   return violations;
   return violations;
 }
 }