runtime-state-store.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { mkdir, readFile, writeFile } from "node:fs/promises";
  2. import { join } from "node:path";
  3. import {
  4. ChapterSummariesStateSchema,
  5. CurrentStateStateSchema,
  6. HooksStateSchema,
  7. StateManifestSchema,
  8. type RuntimeStateDelta,
  9. } from "../models/runtime-state.js";
  10. import type { Fact, StoredHook, StoredSummary } from "./memory-db.js";
  11. import { bootstrapStructuredStateFromMarkdown, parseCurrentStateFacts } from "./state-bootstrap.js";
  12. import { renderChapterSummariesProjection, renderCurrentStateProjection, renderHooksProjection } from "./state-projections.js";
  13. import { applyRuntimeStateDelta, type RuntimeStateSnapshot } from "./state-reducer.js";
  14. import { validateRuntimeState } from "./state-validator.js";
  15. import { arbitrateRuntimeStateDeltaHooks } from "../utils/hook-arbiter.js";
  16. export interface RuntimeStateArtifacts {
  17. readonly snapshot: RuntimeStateSnapshot;
  18. readonly resolvedDelta: RuntimeStateDelta;
  19. readonly currentStateMarkdown: string;
  20. readonly hooksMarkdown: string;
  21. readonly chapterSummariesMarkdown: string;
  22. }
  23. export interface NarrativeMemorySeed {
  24. readonly summaries: ReadonlyArray<StoredSummary>;
  25. readonly hooks: ReadonlyArray<StoredHook>;
  26. }
  27. export async function loadRuntimeStateSnapshot(bookDir: string): Promise<RuntimeStateSnapshot> {
  28. await bootstrapStructuredStateFromMarkdown({ bookDir });
  29. const stateDir = join(bookDir, "story", "state");
  30. const [manifest, currentState, hooks, chapterSummaries] = await Promise.all([
  31. readJson(join(stateDir, "manifest.json"), StateManifestSchema),
  32. readJson(join(stateDir, "current_state.json"), CurrentStateStateSchema),
  33. readJson(join(stateDir, "hooks.json"), HooksStateSchema),
  34. readJson(join(stateDir, "chapter_summaries.json"), ChapterSummariesStateSchema),
  35. ]);
  36. const snapshot = {
  37. manifest,
  38. currentState,
  39. hooks,
  40. chapterSummaries,
  41. };
  42. const issues = validateRuntimeState(snapshot);
  43. if (issues.length > 0) {
  44. const summary = issues
  45. .map((issue) => `${issue.code}${issue.path ? `@${issue.path}` : ""}`)
  46. .join(", ");
  47. throw new Error(`Invalid persisted runtime state: ${summary}`);
  48. }
  49. return snapshot;
  50. }
  51. export async function buildRuntimeStateArtifacts(params: {
  52. readonly bookDir: string;
  53. readonly delta: RuntimeStateDelta;
  54. readonly language: "zh" | "en";
  55. }): Promise<RuntimeStateArtifacts> {
  56. const snapshot = await loadRuntimeStateSnapshot(params.bookDir);
  57. const { resolvedDelta } = arbitrateRuntimeStateDeltaHooks({
  58. hooks: snapshot.hooks.hooks,
  59. delta: params.delta,
  60. });
  61. const next = applyRuntimeStateDelta({
  62. snapshot,
  63. delta: resolvedDelta,
  64. });
  65. return {
  66. snapshot: next,
  67. resolvedDelta,
  68. currentStateMarkdown: renderCurrentStateProjection(next.currentState, params.language),
  69. hooksMarkdown: renderHooksProjection(next.hooks, params.language),
  70. chapterSummariesMarkdown: renderChapterSummariesProjection(next.chapterSummaries, params.language),
  71. };
  72. }
  73. export async function saveRuntimeStateSnapshot(
  74. bookDir: string,
  75. snapshot: RuntimeStateSnapshot,
  76. ): Promise<void> {
  77. const stateDir = join(bookDir, "story", "state");
  78. await mkdir(stateDir, { recursive: true });
  79. await Promise.all([
  80. writeFile(join(stateDir, "manifest.json"), JSON.stringify(snapshot.manifest, null, 2), "utf-8"),
  81. writeFile(join(stateDir, "current_state.json"), JSON.stringify(snapshot.currentState, null, 2), "utf-8"),
  82. writeFile(join(stateDir, "hooks.json"), JSON.stringify(snapshot.hooks, null, 2), "utf-8"),
  83. writeFile(join(stateDir, "chapter_summaries.json"), JSON.stringify(snapshot.chapterSummaries, null, 2), "utf-8"),
  84. ]);
  85. }
  86. export async function loadNarrativeMemorySeed(bookDir: string): Promise<NarrativeMemorySeed> {
  87. const snapshot = await loadRuntimeStateSnapshot(bookDir);
  88. return {
  89. summaries: snapshot.chapterSummaries.rows.map((row) => ({
  90. chapter: row.chapter,
  91. title: row.title,
  92. characters: row.characters,
  93. events: row.events,
  94. stateChanges: row.stateChanges,
  95. hookActivity: row.hookActivity,
  96. mood: row.mood,
  97. chapterType: row.chapterType,
  98. })),
  99. hooks: snapshot.hooks.hooks.map((hook) => ({
  100. hookId: hook.hookId,
  101. startChapter: hook.startChapter,
  102. type: hook.type,
  103. status: hook.status,
  104. lastAdvancedChapter: hook.lastAdvancedChapter,
  105. expectedPayoff: hook.expectedPayoff,
  106. payoffTiming: hook.payoffTiming,
  107. notes: hook.notes,
  108. })),
  109. };
  110. }
  111. export async function loadSnapshotCurrentStateFacts(
  112. bookDir: string,
  113. chapterNumber: number,
  114. ): Promise<ReadonlyArray<Fact>> {
  115. const snapshotDir = join(bookDir, "story", "snapshots", String(chapterNumber));
  116. const structuredState = await readJsonOrNull(
  117. join(snapshotDir, "state", "current_state.json"),
  118. CurrentStateStateSchema,
  119. );
  120. if (structuredState) {
  121. return structuredState.facts;
  122. }
  123. const markdown = await readFile(join(snapshotDir, "current_state.md"), "utf-8").catch(() => "");
  124. return parseCurrentStateFacts(markdown, chapterNumber);
  125. }
  126. async function readJson<T>(
  127. path: string,
  128. schema: { parse(value: unknown): T },
  129. ): Promise<T> {
  130. const raw = await readFile(path, "utf-8");
  131. return schema.parse(JSON.parse(raw));
  132. }
  133. async function readJsonOrNull<T>(
  134. path: string,
  135. schema: { parse(value: unknown): T },
  136. ): Promise<T | null> {
  137. try {
  138. return await readJson(path, schema);
  139. } catch {
  140. return null;
  141. }
  142. }