Просмотр исходного кода

fix: normalize emphasized hook ids

Ma 7 месяцев назад
Родитель
Сommit
e2dae5ee34

+ 14 - 0
packages/core/src/__tests__/memory-retrieval.test.ts

@@ -1072,3 +1072,17 @@ describe("retrieveMemorySelection", () => {
     expect(result.hooks.map((hook) => hook.hookId)).not.toContain("old-seal");
   });
 });
+
+describe("parsePendingHooksMarkdown", () => {
+  it("strips markdown emphasis from hook ids in pending hooks tables", () => {
+    const hooks = memoryRetrieval.parsePendingHooksMarkdown([
+      "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
+      "| --- | --- | --- | --- | --- | --- | --- |",
+      "| **H009** | 3 | mystery | open | 3 | 9 | Bold markdown leaked into hook id |",
+      "| **H010** | 3 | threat | open | 3 | 6 | Another emphasized hook id |",
+      "",
+    ].join("\n"));
+
+    expect(hooks.map((hook) => hook.hookId)).toEqual(["H009", "H010"]);
+  });
+});

+ 49 - 0
packages/core/src/__tests__/state-manager.test.ts

@@ -831,5 +831,54 @@ describe("StateManager", () => {
       expect(summaries.rows.map((row) => row.chapter)).toEqual([1, 2]);
       expect(summaries.rows.at(-1)?.title).toBe("Lantern Wharf");
     });
+
+    it("normalizes emphasized hook ids when bootstrapping structured runtime state from markdown", async () => {
+      const bookId = "runtime-state-emphasized-hook-book";
+      const storyDir = join(manager.bookDir(bookId), "story");
+      await mkdir(storyDir, { recursive: true });
+      await Promise.all([
+        writeFile(
+          join(storyDir, "current_state.md"),
+          [
+            "# Current State",
+            "",
+            "| Field | Value |",
+            "| --- | --- |",
+            "| Current Chapter | 3 |",
+            "| Current Goal | Follow the ledger trail |",
+            "",
+          ].join("\n"),
+          "utf-8",
+        ),
+        writeFile(
+          join(storyDir, "pending_hooks.md"),
+          [
+            "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
+            "| --- | --- | --- | --- | --- | --- | --- |",
+            "| **H009** | 3 | mystery | open | 3 | 9 | Bold markdown leaked into hook id |",
+            "",
+          ].join("\n"),
+          "utf-8",
+        ),
+        writeFile(
+          join(storyDir, "chapter_summaries.md"),
+          [
+            "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
+            "| --- | --- | --- | --- | --- | --- | --- | --- |",
+            "| 3 | Lantern Wharf | Lin Yue | Follows the ledger trail | Goal narrows to the ledger trail | H009 advanced | wary | investigation |",
+            "",
+          ].join("\n"),
+          "utf-8",
+        ),
+      ]);
+
+      await manager.ensureRuntimeState(bookId, 3);
+
+      const hooks = JSON.parse(
+        await readFile(join(manager.stateDir(bookId), "hooks.json"), "utf-8"),
+      ) as { hooks: Array<{ hookId: string }> };
+
+      expect(hooks.hooks.map((hook) => hook.hookId)).toEqual(["H009"]);
+    });
   });
 });

+ 22 - 4
packages/core/src/state/state-bootstrap.ts

@@ -185,9 +185,9 @@ export function parsePendingHooksMarkdown(markdown: string): StoredHook[] {
 
   if (tableRows.length > 0) {
     return tableRows
-      .filter((row) => (row[0] ?? "").length > 0)
+      .filter((row) => normalizeHookId(row[0]).length > 0)
       .map((row) => ({
-        hookId: row[0] ?? "",
+        hookId: normalizeHookId(row[0]),
         startChapter: parseInteger(row[1]),
         type: row[2] ?? "",
         status: row[3] ?? "open",
@@ -330,9 +330,9 @@ function parsePendingHooksStateMarkdown(markdown: string, warnings: string[]) {
   if (tableRows.length > 0) {
     return HooksStateSchema.parse({
       hooks: tableRows
-        .filter((row) => (row[0] ?? "").length > 0)
+        .filter((row) => normalizeHookId(row[0]).length > 0)
         .map((row) => {
-          const hookId = row[0] ?? "";
+          const hookId = normalizeHookId(row[0]);
           return {
             hookId,
             startChapter: parseIntegerWithWarning(row[1], warnings, `${hookId}:startChapter`),
@@ -581,6 +581,24 @@ function maxHookChapter(hooks: ReadonlyArray<StoredHook>): number {
   );
 }
 
+export function normalizeHookId(value: string | undefined): string {
+  let normalized = (value ?? "").trim();
+  let previous = "";
+  while (normalized && normalized !== previous) {
+    previous = normalized;
+    normalized = normalized
+      .replace(/^\[(.+?)\]\([^)]+\)$/u, "$1")
+      .replace(/^\*\*(.+)\*\*$/u, "$1")
+      .replace(/^__(.+)__$/u, "$1")
+      .replace(/^\*(.+)\*$/u, "$1")
+      .replace(/^_(.+)_$/u, "$1")
+      .replace(/^`(.+)`$/u, "$1")
+      .replace(/^~~(.+)~~$/u, "$1")
+      .trim();
+  }
+  return normalized;
+}
+
 function normalizeHookStatus(value: string | undefined, warnings: string[], hookId: string): HookStatus {
   const normalized = (value ?? "").trim().toLowerCase();
   if (!normalized) return "open";

+ 3 - 3
packages/core/src/utils/memory-retrieval.ts

@@ -9,7 +9,7 @@ import {
   type HookStatus,
 } from "../models/runtime-state.js";
 import { MemoryDB, type Fact, type StoredHook, type StoredSummary } from "../state/memory-db.js";
-import { bootstrapStructuredStateFromMarkdown } from "../state/state-bootstrap.js";
+import { bootstrapStructuredStateFromMarkdown, normalizeHookId } from "../state/state-bootstrap.js";
 import { collectStaleHookDebt } from "./hook-governance.js";
 
 export interface MemorySelection {
@@ -386,9 +386,9 @@ export function parsePendingHooksMarkdown(markdown: string): StoredHook[] {
 
   if (tableRows.length > 0) {
     return tableRows
-      .filter((row) => (row[0] ?? "").length > 0)
+      .filter((row) => normalizeHookId(row[0]).length > 0)
       .map((row) => ({
-        hookId: row[0] ?? "",
+        hookId: normalizeHookId(row[0]),
         startChapter: parseInteger(row[1]),
         type: row[2] ?? "",
         status: row[3] ?? "open",