Sfoglia il codice sorgente

feat(interaction): add edit transaction planning

Ma 7 mesi fa
parent
commit
1b1846c973

+ 92 - 0
packages/core/src/__tests__/edit-controller.test.ts

@@ -0,0 +1,92 @@
+import { describe, expect, it } from "vitest";
+import {
+  classifyTruthAuthority,
+  normalizeTruthFileName,
+} from "../interaction/truth-authority.js";
+import {
+  planEditTransaction,
+  type EditRequest,
+} from "../interaction/edit-controller.js";
+
+describe("truth authority", () => {
+  it("normalizes supported truth files", () => {
+    expect(normalizeTruthFileName("story_bible")).toBe("story_bible.md");
+    expect(normalizeTruthFileName("current_state.md")).toBe("current_state.md");
+  });
+
+  it("classifies control and truth authority tiers", () => {
+    expect(classifyTruthAuthority("author_intent.md")).toBe("direction");
+    expect(classifyTruthAuthority("current_focus.md")).toBe("direction");
+    expect(classifyTruthAuthority("story_bible.md")).toBe("foundation");
+    expect(classifyTruthAuthority("book_rules.md")).toBe("rules");
+    expect(classifyTruthAuthority("current_state.md")).toBe("runtime-truth");
+  });
+});
+
+describe("edit controller", () => {
+  it("plans entity rename transactions", () => {
+    const result = planEditTransaction({
+      kind: "entity-rename",
+      bookId: "harbor",
+      entityType: "protagonist",
+      oldValue: "陆尘",
+      newValue: "林砚",
+    });
+
+    expect(result.transactionType).toBe("entity-rename");
+    expect(result.affectedScope).toBe("book");
+    expect(result.requiresTruthRebuild).toBe(true);
+  });
+
+  it("plans chapter rewrite transactions", () => {
+    const result = planEditTransaction({
+      kind: "chapter-rewrite",
+      bookId: "harbor",
+      chapterNumber: 3,
+      instruction: "Keep the ending reveal.",
+    });
+
+    expect(result.transactionType).toBe("chapter-rewrite");
+    expect(result.affectedScope).toBe("downstream");
+    expect(result.requiresTruthRebuild).toBe(true);
+  });
+
+  it("plans local text edits without forcing full-book rebuild", () => {
+    const result = planEditTransaction({
+      kind: "chapter-local-edit",
+      bookId: "harbor",
+      chapterNumber: 5,
+      instruction: "Only rewrite the final paragraph.",
+    });
+
+    expect(result.transactionType).toBe("chapter-local-edit");
+    expect(result.affectedScope).toBe("chapter");
+    expect(result.requiresTruthRebuild).toBe(true);
+  });
+
+  it("plans truth-file edits with authority metadata", () => {
+    const result = planEditTransaction({
+      kind: "truth-file-edit",
+      bookId: "harbor",
+      fileName: "book_rules",
+      instruction: "Lock the protagonist name to Lin Yan.",
+    });
+
+    expect(result.transactionType).toBe("truth-file-edit");
+    expect(result.truthAuthority).toBe("rules");
+    expect(result.affectedScope).toBe("book");
+  });
+
+  it("plans focus edits as direction-level transactions", () => {
+    const result = planEditTransaction({
+      kind: "focus-edit",
+      bookId: "harbor",
+      instruction: "Bring the story back to the old case.",
+    });
+
+    expect(result.transactionType).toBe("focus-edit");
+    expect(result.truthAuthority).toBe("direction");
+    expect(result.affectedScope).toBe("future");
+    expect(result.requiresTruthRebuild).toBe(false);
+  });
+});

+ 10 - 0
packages/core/src/index.ts

@@ -115,6 +115,16 @@ export {
 } from "./interaction/nl-router.js";
 } from "./interaction/nl-router.js";
 export { processProjectInteractionInput } from "./interaction/project-control.js";
 export { processProjectInteractionInput } from "./interaction/project-control.js";
 export { createInteractionToolsFromDeps } from "./interaction/project-tools.js";
 export { createInteractionToolsFromDeps } from "./interaction/project-tools.js";
+export {
+  normalizeTruthFileName,
+  classifyTruthAuthority,
+  type TruthAuthority,
+} from "./interaction/truth-authority.js";
+export {
+  planEditTransaction,
+  type EditRequest,
+  type PlannedEditTransaction,
+} from "./interaction/edit-controller.js";
 export {
 export {
   runInteractionRequest,
   runInteractionRequest,
   type InteractionRuntimeTools,
   type InteractionRuntimeTools,

+ 92 - 0
packages/core/src/interaction/edit-controller.ts

@@ -0,0 +1,92 @@
+import { classifyTruthAuthority, normalizeTruthFileName, type TruthAuthority } from "./truth-authority.js";
+
+export type EditRequest =
+  | {
+      readonly kind: "entity-rename";
+      readonly bookId: string;
+      readonly entityType: "protagonist" | "character" | "location" | "organization";
+      readonly oldValue: string;
+      readonly newValue: string;
+    }
+  | {
+      readonly kind: "chapter-rewrite";
+      readonly bookId: string;
+      readonly chapterNumber: number;
+      readonly instruction: string;
+    }
+  | {
+      readonly kind: "chapter-local-edit";
+      readonly bookId: string;
+      readonly chapterNumber: number;
+      readonly instruction: string;
+    }
+  | {
+      readonly kind: "truth-file-edit";
+      readonly bookId: string;
+      readonly fileName: string;
+      readonly instruction: string;
+    }
+  | {
+      readonly kind: "focus-edit";
+      readonly bookId: string;
+      readonly instruction: string;
+    };
+
+export interface PlannedEditTransaction {
+  readonly transactionType: EditRequest["kind"];
+  readonly bookId: string;
+  readonly chapterNumber?: number;
+  readonly truthAuthority?: TruthAuthority;
+  readonly normalizedFileName?: string;
+  readonly affectedScope: "chapter" | "downstream" | "future" | "book";
+  readonly requiresTruthRebuild: boolean;
+}
+
+export function planEditTransaction(request: EditRequest): PlannedEditTransaction {
+  switch (request.kind) {
+    case "entity-rename":
+      return {
+        transactionType: request.kind,
+        bookId: request.bookId,
+        affectedScope: "book",
+        requiresTruthRebuild: true,
+      };
+    case "chapter-rewrite":
+      return {
+        transactionType: request.kind,
+        bookId: request.bookId,
+        chapterNumber: request.chapterNumber,
+        affectedScope: "downstream",
+        requiresTruthRebuild: true,
+      };
+    case "chapter-local-edit":
+      return {
+        transactionType: request.kind,
+        bookId: request.bookId,
+        chapterNumber: request.chapterNumber,
+        affectedScope: "chapter",
+        requiresTruthRebuild: true,
+      };
+    case "truth-file-edit": {
+      const normalizedFileName = normalizeTruthFileName(request.fileName);
+      const truthAuthority = classifyTruthAuthority(normalizedFileName);
+      return {
+        transactionType: request.kind,
+        bookId: request.bookId,
+        normalizedFileName,
+        truthAuthority,
+        affectedScope: truthAuthority === "runtime-truth" ? "book" : truthAuthority === "memory" ? "book" : "book",
+        requiresTruthRebuild: truthAuthority === "runtime-truth" || truthAuthority === "memory",
+      };
+    }
+    case "focus-edit":
+      return {
+        transactionType: request.kind,
+        bookId: request.bookId,
+        truthAuthority: "direction",
+        normalizedFileName: "current_focus.md",
+        affectedScope: "future",
+        requiresTruthRebuild: false,
+      };
+  }
+}

+ 45 - 0
packages/core/src/interaction/truth-authority.ts

@@ -0,0 +1,45 @@
+export type TruthAuthority =
+  | "direction"
+  | "foundation"
+  | "rules"
+  | "runtime-truth"
+  | "memory";
+
+const NORMALIZED_TRUTH_FILES = new Set([
+  "author_intent.md",
+  "current_focus.md",
+  "story_bible.md",
+  "volume_outline.md",
+  "book_rules.md",
+  "current_state.md",
+  "pending_hooks.md",
+  "chapter_summaries.md",
+]);
+
+export function normalizeTruthFileName(fileName: string): string {
+  const trimmed = fileName.trim().toLowerCase();
+  const normalized = trimmed.endsWith(".md") ? trimmed : `${trimmed}.md`;
+  if (!NORMALIZED_TRUTH_FILES.has(normalized)) {
+    return normalized;
+  }
+  return normalized;
+}
+
+export function classifyTruthAuthority(fileName: string): TruthAuthority {
+  switch (normalizeTruthFileName(fileName)) {
+    case "author_intent.md":
+    case "current_focus.md":
+      return "direction";
+    case "story_bible.md":
+    case "volume_outline.md":
+      return "foundation";
+    case "book_rules.md":
+      return "rules";
+    case "current_state.md":
+    case "pending_hooks.md":
+      return "runtime-truth";
+    case "chapter_summaries.md":
+    default:
+      return "memory";
+  }
+}