Ver Fonte

feat(v13): extend pending_hooks.md with upstream and payoff-arc columns [phase 7/8 part 3]

Both renderers (architect-side renderHookSnapshot and runtime-side
renderHooksProjection) now emit three visible Phase 7 columns:
  - depends_on / 上游依赖 — upstream hook ids or "none"/"无"
  - pays_off_in_arc / 回收卷 — free-form prose pointer
  - core_hook / 核心 — true|false / 是|否

The parser accepts the 11-column ledger shape (no half_life) and the
12-column architect shape (with half_life), alongside the legacy 7/8
column formats for pre-Phase-7 books. Three architect round-trip tests,
the state-projection fixture, and the writer governed-settlement test
were updated to assert the new column layout.
Ma há 6 meses atrás
pai
commit
c9e8536210

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

@@ -339,7 +339,8 @@ describe("ArchitectAgent", () => {
 
     const result = await agent.generateFoundation(book);
 
-    expect(result.pendingHooks).toContain("| H01 | 1 | 主线 | 未开启 | 0 | 10章 | 中程 | 主线核心钩子 |");
+    // Phase 7: ledger renders extended columns — depends_on, pays_off_in_arc, core_hook.
+    expect(result.pendingHooks).toContain("| H01 | 1 | 主线 | 未开启 | 0 | 10章 | 中程 | 无 |  | 否 | 主线核心钩子 |");
     expect(result.pendingHooks).not.toContain("如果你愿意");
     expect(result.pendingHooks).not.toContain("前10章逐章细纲");
   });
@@ -401,7 +402,7 @@ describe("ArchitectAgent", () => {
 
     const result = await agent.generateFoundation(book);
 
-    expect(result.pendingHooks).toContain("| H13 | 22 | 舆情操盘 | 待推进 | 0 | 51-60章 | 中程 | 庄蔓出场后逐步揭露(初始线索:一家自媒体公司在多个旧案节点同步接单) |");
+    expect(result.pendingHooks).toContain("| H13 | 22 | 舆情操盘 | 待推进 | 0 | 51-60章 | 中程 | 无 |  | 否 | 庄蔓出场后逐步揭露(初始线索:一家自媒体公司在多个旧案节点同步接单) |");
   });
 
   it("accepts section labels with spacing and punctuation drift from non-strict models", async () => {
@@ -465,7 +466,7 @@ describe("ArchitectAgent", () => {
     expect(result.volumeOutline).toBe("# 卷纲");
     expect(result.bookRules).toContain("version: \"1.0\"");
     expect(result.currentState).toBe("# 当前状态");
-    expect(result.pendingHooks).toContain("| H01 | 1 | mystery | open | 0 | 10章 | 中程 | 初始钩子 |");
+    expect(result.pendingHooks).toContain("| H01 | 1 | mystery | open | 0 | 10章 | 中程 | 无 |  | 否 | 初始钩子 |");
   });
 
   it("throws when a required foundation section is missing", async () => {

+ 4 - 4
packages/core/src/__tests__/state-projections.test.ts

@@ -33,10 +33,10 @@ describe("state projections", () => {
     expect(markdown).toBe([
       "# Pending Hooks",
       "",
-      "| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | payoff_timing | notes |",
-      "| --- | --- | --- | --- | --- | --- | --- | --- |",
-      "| a-debt | 4 | relationship | progressing | 11 | Reveal the debt. | mid-arc | Old oath token resurfaces. |",
-      "| b-courier | 12 | mystery | open | 13 | Identify the courier. | mid-arc | The seal is still broken. |",
+      "| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | payoff_timing | depends_on | pays_off_in_arc | core_hook | notes |",
+      "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
+      "| a-debt | 4 | relationship | progressing | 11 | Reveal the debt. | mid-arc | none |  | false | Old oath token resurfaces. |",
+      "| b-courier | 12 | mystery | open | 13 | Identify the courier. | mid-arc | none |  | false | The seal is still broken. |",
       "",
     ].join("\n"));
   });

+ 1 - 1
packages/core/src/__tests__/writer.test.ts

@@ -231,7 +231,7 @@ describe("WriterAgent", () => {
       expect(settlePrompt).toContain("| 99 | Locked Gate |");
       expect(settlePrompt).toContain("## Hook Debt Briefs");
       expect(settlePrompt).toContain("mentor-oath | cadence: slow-burn");
-      expect(settlePrompt).toContain("| stale-ledger | 14 | mystery | open | 70 | 120 | 中程 | Old ledger debt is dormant but unresolved |");
+      expect(settlePrompt).toContain("| stale-ledger | 14 | mystery | open | 70 | 120 | 中程 | 无 |  | 否 | Old ledger debt is dormant but unresolved |");
       expect(settlePrompt).not.toContain("| 1 | Guild Trail |");
       expect(settlePrompt).not.toContain("old-seal");
       expect(settlePrompt).not.toContain("Guildmaster Ren");

+ 19 - 4
packages/core/src/state/state-projections.ts

@@ -13,14 +13,16 @@ export function renderHooksProjection(
   language: "zh" | "en" = "zh",
 ): string {
   const title = language === "en" ? "# Pending Hooks" : "# 伏笔池";
+  // Phase 7: depends_on / pays_off_in_arc / core_hook are visible columns,
+  // so writer and reviewer both see the causal chain and planned payoff arc.
   const headers = language === "en"
     ? [
-      "| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | payoff_timing | notes |",
-      "| --- | --- | --- | --- | --- | --- | --- | --- |",
+      "| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | payoff_timing | depends_on | pays_off_in_arc | core_hook | notes |",
+      "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
     ]
     : [
-      "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 备注 |",
-      "| --- | --- | --- | --- | --- | --- | --- | --- |",
+      "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 上游依赖 | 回收卷 | 核心 | 备注 |",
+      "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
     ];
 
   const rows = [...state.hooks]
@@ -38,6 +40,9 @@ export function renderHooksProjection(
         hook.lastAdvancedChapter,
         hook.expectedPayoff,
         localizeHookPayoffTiming(resolveHookPayoffTiming(hook), language),
+        renderDependsOnCell(hook.dependsOn ?? [], language),
+        hook.paysOffInArc ?? "",
+        renderCoreHookCell(hook.coreHook === true, language),
         hook.notes,
       ].map(escapeTableCell).join(" | ")
     } |`);
@@ -45,6 +50,16 @@ export function renderHooksProjection(
   return [title, "", ...headers, ...rows, ""].join("\n");
 }
 
+function renderDependsOnCell(ids: ReadonlyArray<string>, language: "zh" | "en"): string {
+  if (ids.length === 0) return language === "en" ? "none" : "无";
+  return `[${ids.join(", ")}]`;
+}
+
+function renderCoreHookCell(isCore: boolean, language: "zh" | "en"): string {
+  if (language === "en") return isCore ? "true" : "false";
+  return isCore ? "是" : "否";
+}
+
 export function renderChapterSummariesProjection(
   state: ChapterSummariesState,
   language: "zh" | "en" = "zh",

+ 31 - 12
packages/core/src/utils/story-markdown.ts

@@ -44,12 +44,12 @@ export function renderHookSnapshot(
 
   const headers = language === "en"
     ? [
-      "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | payoff_timing | notes |",
-      "| --- | --- | --- | --- | --- | --- | --- | --- |",
+      "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | payoff_timing | depends_on | pays_off_in_arc | core_hook | notes |",
+      "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
     ]
     : [
-      "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 备注 |",
-      "| --- | --- | --- | --- | --- | --- | --- | --- |",
+      "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 回收节奏 | 上游依赖 | 回收卷 | 核心 | 备注 |",
+      "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
     ];
 
   return [
@@ -62,11 +62,24 @@ export function renderHookSnapshot(
       hook.lastAdvancedChapter,
       hook.expectedPayoff,
       localizeHookPayoffTiming(resolveHookPayoffTiming(hook), language),
+      renderDependsOnCell(hook.dependsOn ?? [], language),
+      hook.paysOffInArc ?? "",
+      renderCoreHookCell(hook.coreHook === true, language),
       hook.notes,
     ].map((cell) => escapeTableCell(String(cell))).join(" | ")).map((row) => `| ${row} |`),
   ].join("\n");
 }
 
+function renderDependsOnCell(ids: ReadonlyArray<string>, language: "zh" | "en"): string {
+  if (ids.length === 0) return language === "en" ? "none" : "无";
+  return `[${ids.join(", ")}]`;
+}
+
+function renderCoreHookCell(isCore: boolean, language: "zh" | "en"): string {
+  if (language === "en") return isCore ? "true" : "false";
+  return isCore ? "是" : "否";
+}
+
 export function parseChapterSummariesMarkdown(markdown: string): StoredSummary[] {
   const rows = parseMarkdownTableRows(markdown)
     .filter((row) => /^\d+$/.test(row[0] ?? ""));
@@ -229,16 +242,22 @@ function parsePendingHookRow(row: ReadonlyArray<string | undefined>): StoredHook
   // Row shapes by length:
   //   7 (legacy pre-timing): id, ch, type, status, last_adv, expected, notes
   //   8 (Phase 5/6):          id, ch, type, status, last_adv, expected, timing, notes
-  //  12 (Phase 7 extended):   id, ch, type, status, last_adv, expected, timing,
-  //                           depends_on, pays_off_in_arc, core_hook, half_life, notes
-  const phase7 = row.length >= 12;
+  //  11 (Phase 7 ledger):     ... + depends_on, pays_off_in_arc, core_hook, notes
+  //  12 (Phase 7 architect):  ... + depends_on, pays_off_in_arc, core_hook, half_life, notes
+  //  additional trailing columns (e.g. stale/blocked diagnostic columns) are
+  //  allowed — the parser skips past them to the notes column.
+  const phase7Full = row.length >= 12;
+  const phase7Compact = row.length === 11;
+  const phase7 = phase7Full || phase7Compact;
   const legacyShape = row.length < 8;
   const payoffTiming = legacyShape ? undefined : normalizeHookPayoffTiming(row[6]);
-  const notes = phase7
+  const notes = phase7Full
     ? (row[11] ?? "")
-    : legacyShape
-      ? (row[6] ?? "")
-      : (row[7] ?? "");
+    : phase7Compact
+      ? (row[10] ?? "")
+      : legacyShape
+        ? (row[6] ?? "")
+        : (row[7] ?? "");
 
   const base = {
     hookId: normalizeHookId(row[0]),
@@ -258,7 +277,7 @@ function parsePendingHookRow(row: ReadonlyArray<string | undefined>): StoredHook
     dependsOn: parseDependsOn(row[7] ?? ""),
     paysOffInArc: (row[8] ?? "").trim(),
     coreHook: parseBooleanCell(row[9]),
-    halfLifeChapters: parseOptionalInt(row[10]),
+    halfLifeChapters: phase7Full ? parseOptionalInt(row[10]) : undefined,
   };
 }