@actalk/inkos-core 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/agents/architect.d.ts +6 -1
  2. package/dist/agents/architect.d.ts.map +1 -1
  3. package/dist/agents/architect.js +362 -83
  4. package/dist/agents/architect.js.map +1 -1
  5. package/dist/agents/chapter-analyzer.d.ts +6 -0
  6. package/dist/agents/chapter-analyzer.d.ts.map +1 -1
  7. package/dist/agents/chapter-analyzer.js +220 -17
  8. package/dist/agents/chapter-analyzer.js.map +1 -1
  9. package/dist/agents/composer.d.ts +28 -0
  10. package/dist/agents/composer.d.ts.map +1 -0
  11. package/dist/agents/composer.js +154 -0
  12. package/dist/agents/composer.js.map +1 -0
  13. package/dist/agents/consolidator.d.ts.map +1 -1
  14. package/dist/agents/consolidator.js +17 -8
  15. package/dist/agents/consolidator.js.map +1 -1
  16. package/dist/agents/continuity.d.ts +10 -0
  17. package/dist/agents/continuity.d.ts.map +1 -1
  18. package/dist/agents/continuity.js +312 -133
  19. package/dist/agents/continuity.js.map +1 -1
  20. package/dist/agents/en-prompt-sections.d.ts.map +1 -1
  21. package/dist/agents/en-prompt-sections.js +1 -0
  22. package/dist/agents/en-prompt-sections.js.map +1 -1
  23. package/dist/agents/length-normalizer.d.ts +32 -0
  24. package/dist/agents/length-normalizer.d.ts.map +1 -0
  25. package/dist/agents/length-normalizer.js +156 -0
  26. package/dist/agents/length-normalizer.js.map +1 -0
  27. package/dist/agents/planner.d.ts +42 -0
  28. package/dist/agents/planner.d.ts.map +1 -0
  29. package/dist/agents/planner.js +382 -0
  30. package/dist/agents/planner.js.map +1 -0
  31. package/dist/agents/post-write-validator.d.ts +6 -1
  32. package/dist/agents/post-write-validator.d.ts.map +1 -1
  33. package/dist/agents/post-write-validator.js +88 -2
  34. package/dist/agents/post-write-validator.js.map +1 -1
  35. package/dist/agents/reviser.d.ts +10 -1
  36. package/dist/agents/reviser.d.ts.map +1 -1
  37. package/dist/agents/reviser.js +151 -36
  38. package/dist/agents/reviser.js.map +1 -1
  39. package/dist/agents/rules-reader.d.ts +1 -0
  40. package/dist/agents/rules-reader.d.ts.map +1 -1
  41. package/dist/agents/rules-reader.js +13 -0
  42. package/dist/agents/rules-reader.js.map +1 -1
  43. package/dist/agents/settler-delta-parser.d.ts +7 -0
  44. package/dist/agents/settler-delta-parser.d.ts.map +1 -0
  45. package/dist/agents/settler-delta-parser.js +35 -0
  46. package/dist/agents/settler-delta-parser.js.map +1 -0
  47. package/dist/agents/settler-prompts.d.ts +2 -0
  48. package/dist/agents/settler-prompts.d.ts.map +1 -1
  49. package/dist/agents/settler-prompts.js +77 -63
  50. package/dist/agents/settler-prompts.js.map +1 -1
  51. package/dist/agents/writer-parser.d.ts +3 -2
  52. package/dist/agents/writer-parser.d.ts.map +1 -1
  53. package/dist/agents/writer-parser.js +44 -13
  54. package/dist/agents/writer-parser.js.map +1 -1
  55. package/dist/agents/writer-prompts.d.ts +2 -1
  56. package/dist/agents/writer-prompts.d.ts.map +1 -1
  57. package/dist/agents/writer-prompts.js +65 -21
  58. package/dist/agents/writer-prompts.js.map +1 -1
  59. package/dist/agents/writer.d.ts +28 -1
  60. package/dist/agents/writer.d.ts.map +1 -1
  61. package/dist/agents/writer.js +426 -67
  62. package/dist/agents/writer.js.map +1 -1
  63. package/dist/index.d.ts +18 -3
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +17 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/llm/provider.d.ts +1 -0
  68. package/dist/llm/provider.d.ts.map +1 -1
  69. package/dist/llm/provider.js +19 -6
  70. package/dist/llm/provider.js.map +1 -1
  71. package/dist/models/chapter.d.ts +71 -0
  72. package/dist/models/chapter.d.ts.map +1 -1
  73. package/dist/models/chapter.js +3 -0
  74. package/dist/models/chapter.js.map +1 -1
  75. package/dist/models/input-governance.d.ts +351 -0
  76. package/dist/models/input-governance.d.ts.map +1 -0
  77. package/dist/models/input-governance.js +78 -0
  78. package/dist/models/input-governance.js.map +1 -0
  79. package/dist/models/length-governance.d.ts +93 -0
  80. package/dist/models/length-governance.d.ts.map +1 -0
  81. package/dist/models/length-governance.js +34 -0
  82. package/dist/models/length-governance.js.map +1 -0
  83. package/dist/models/project.d.ts +5 -0
  84. package/dist/models/project.d.ts.map +1 -1
  85. package/dist/models/project.js +2 -0
  86. package/dist/models/project.js.map +1 -1
  87. package/dist/models/runtime-state.d.ts +521 -0
  88. package/dist/models/runtime-state.d.ts.map +1 -0
  89. package/dist/models/runtime-state.js +78 -0
  90. package/dist/models/runtime-state.js.map +1 -0
  91. package/dist/pipeline/agent.d.ts +2 -1
  92. package/dist/pipeline/agent.d.ts.map +1 -1
  93. package/dist/pipeline/agent.js +90 -5
  94. package/dist/pipeline/agent.js.map +1 -1
  95. package/dist/pipeline/runner.d.ts +65 -1
  96. package/dist/pipeline/runner.d.ts.map +1 -1
  97. package/dist/pipeline/runner.js +1029 -73
  98. package/dist/pipeline/runner.js.map +1 -1
  99. package/dist/state/manager.d.ts +14 -0
  100. package/dist/state/manager.d.ts.map +1 -1
  101. package/dist/state/manager.js +114 -0
  102. package/dist/state/manager.js.map +1 -1
  103. package/dist/state/memory-db.d.ts +15 -0
  104. package/dist/state/memory-db.d.ts.map +1 -1
  105. package/dist/state/memory-db.js +119 -10
  106. package/dist/state/memory-db.js.map +1 -1
  107. package/dist/state/runtime-state-store.d.ts +23 -0
  108. package/dist/state/runtime-state-store.d.ts.map +1 -0
  109. package/dist/state/runtime-state-store.js +100 -0
  110. package/dist/state/runtime-state-store.js.map +1 -0
  111. package/dist/state/state-bootstrap.d.ts +19 -0
  112. package/dist/state/state-bootstrap.d.ts.map +1 -0
  113. package/dist/state/state-bootstrap.js +394 -0
  114. package/dist/state/state-bootstrap.js.map +1 -0
  115. package/dist/state/state-projections.d.ts +5 -0
  116. package/dist/state/state-projections.d.ts.map +1 -0
  117. package/dist/state/state-projections.js +164 -0
  118. package/dist/state/state-projections.js.map +1 -0
  119. package/dist/state/state-reducer.d.ts +12 -0
  120. package/dist/state/state-reducer.d.ts.map +1 -0
  121. package/dist/state/state-reducer.js +146 -0
  122. package/dist/state/state-reducer.js.map +1 -0
  123. package/dist/state/state-validator.d.ts +12 -0
  124. package/dist/state/state-validator.d.ts.map +1 -0
  125. package/dist/state/state-validator.js +56 -0
  126. package/dist/state/state-validator.js.map +1 -0
  127. package/dist/utils/chapter-splitter.d.ts +2 -0
  128. package/dist/utils/chapter-splitter.d.ts.map +1 -1
  129. package/dist/utils/chapter-splitter.js +22 -4
  130. package/dist/utils/chapter-splitter.js.map +1 -1
  131. package/dist/utils/config-loader.d.ts +3 -1
  132. package/dist/utils/config-loader.d.ts.map +1 -1
  133. package/dist/utils/config-loader.js +14 -3
  134. package/dist/utils/config-loader.js.map +1 -1
  135. package/dist/utils/context-filter.js +1 -1
  136. package/dist/utils/context-filter.js.map +1 -1
  137. package/dist/utils/governed-context.d.ts +7 -0
  138. package/dist/utils/governed-context.d.ts.map +1 -0
  139. package/dist/utils/governed-context.js +22 -0
  140. package/dist/utils/governed-context.js.map +1 -0
  141. package/dist/utils/governed-working-set.d.ts +18 -0
  142. package/dist/utils/governed-working-set.d.ts.map +1 -0
  143. package/dist/utils/governed-working-set.js +295 -0
  144. package/dist/utils/governed-working-set.js.map +1 -0
  145. package/dist/utils/hook-governance.d.ts +26 -0
  146. package/dist/utils/hook-governance.d.ts.map +1 -0
  147. package/dist/utils/hook-governance.js +128 -0
  148. package/dist/utils/hook-governance.js.map +1 -0
  149. package/dist/utils/hook-health.d.ts +14 -0
  150. package/dist/utils/hook-health.d.ts.map +1 -0
  151. package/dist/utils/hook-health.js +68 -0
  152. package/dist/utils/hook-health.js.map +1 -0
  153. package/dist/utils/length-metrics.d.ts +10 -0
  154. package/dist/utils/length-metrics.d.ts.map +1 -0
  155. package/dist/utils/length-metrics.js +85 -0
  156. package/dist/utils/length-metrics.js.map +1 -0
  157. package/dist/utils/long-span-fatigue.d.ts +28 -0
  158. package/dist/utils/long-span-fatigue.d.ts.map +1 -0
  159. package/dist/utils/long-span-fatigue.js +359 -0
  160. package/dist/utils/long-span-fatigue.js.map +1 -0
  161. package/dist/utils/memory-retrieval.d.ts +39 -0
  162. package/dist/utils/memory-retrieval.d.ts.map +1 -0
  163. package/dist/utils/memory-retrieval.js +574 -0
  164. package/dist/utils/memory-retrieval.js.map +1 -0
  165. package/dist/utils/spot-fix-patches.d.ts +14 -0
  166. package/dist/utils/spot-fix-patches.d.ts.map +1 -0
  167. package/dist/utils/spot-fix-patches.js +75 -0
  168. package/dist/utils/spot-fix-patches.js.map +1 -0
  169. package/package.json +1 -1
@@ -1,26 +1,72 @@
1
1
  import { chatCompletion, createLLMClient } from "../llm/provider.js";
2
2
  import { ArchitectAgent } from "../agents/architect.js";
3
+ import { PlannerAgent } from "../agents/planner.js";
4
+ import { ComposerAgent } from "../agents/composer.js";
3
5
  import { WriterAgent } from "../agents/writer.js";
6
+ import { LengthNormalizerAgent } from "../agents/length-normalizer.js";
4
7
  import { ChapterAnalyzerAgent } from "../agents/chapter-analyzer.js";
5
8
  import { ContinuityAuditor } from "../agents/continuity.js";
6
- import { ReviserAgent } from "../agents/reviser.js";
9
+ import { ReviserAgent, DEFAULT_REVISE_MODE } from "../agents/reviser.js";
7
10
  import { StateValidatorAgent } from "../agents/state-validator.js";
8
11
  import { RadarAgent } from "../agents/radar.js";
9
12
  import { readGenreProfile } from "../agents/rules-reader.js";
10
13
  import { analyzeAITells } from "../agents/ai-tells.js";
11
14
  import { analyzeSensitiveWords } from "../agents/sensitive-words.js";
12
15
  import { StateManager } from "../state/manager.js";
16
+ import { MemoryDB } from "../state/memory-db.js";
13
17
  import { dispatchNotification, dispatchWebhookEvent } from "../notify/dispatcher.js";
14
- import { readFile, readdir, writeFile, mkdir } from "node:fs/promises";
18
+ import { ChapterIntentSchema } from "../models/input-governance.js";
19
+ import { buildLengthSpec, countChapterLength, formatLengthCount, isOutsideHardRange, isOutsideSoftRange, resolveLengthCountingMode } from "../utils/length-metrics.js";
20
+ import { analyzeLongSpanFatigue } from "../utils/long-span-fatigue.js";
21
+ import { loadNarrativeMemorySeed, loadSnapshotCurrentStateFacts } from "../state/runtime-state-store.js";
22
+ import { rewriteStructuredStateFromMarkdown } from "../state/state-bootstrap.js";
23
+ import { readFile, readdir, writeFile, mkdir, rm } from "node:fs/promises";
15
24
  import { join } from "node:path";
16
25
  export class PipelineRunner {
17
26
  state;
18
27
  config;
19
28
  agentClients = new Map();
29
+ memoryIndexFallbackWarned = false;
20
30
  constructor(config) {
21
31
  this.config = config;
22
32
  this.state = new StateManager(config.projectRoot);
23
33
  }
34
+ localize(language, messages) {
35
+ return language === "en" ? messages.en : messages.zh;
36
+ }
37
+ async resolveBookLanguage(book) {
38
+ if (book.language) {
39
+ return book.language;
40
+ }
41
+ try {
42
+ const { profile } = await this.loadGenreProfile(book.genre);
43
+ return profile.language;
44
+ }
45
+ catch {
46
+ return "zh";
47
+ }
48
+ }
49
+ async resolveBookLanguageById(bookId) {
50
+ try {
51
+ const book = await this.state.loadBookConfig(bookId);
52
+ return await this.resolveBookLanguage(book);
53
+ }
54
+ catch {
55
+ return "zh";
56
+ }
57
+ }
58
+ languageFromLengthSpec(lengthSpec) {
59
+ return lengthSpec.countingMode === "en_words" ? "en" : "zh";
60
+ }
61
+ logStage(language, message) {
62
+ this.config.logger?.info(`${this.localize(language, { zh: "阶段:", en: "Stage: " })}${this.localize(language, message)}`);
63
+ }
64
+ logInfo(language, message) {
65
+ this.config.logger?.info(this.localize(language, message));
66
+ }
67
+ logWarn(language, message) {
68
+ this.config.logger?.warn(this.localize(language, message));
69
+ }
24
70
  agentCtx(bookId) {
25
71
  return {
26
72
  client: this.config.client,
@@ -102,14 +148,21 @@ export class PipelineRunner {
102
148
  async initBook(book) {
103
149
  const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
104
150
  const bookDir = this.state.bookDir(book.id);
151
+ const stageLanguage = await this.resolveBookLanguage(book);
152
+ this.logStage(stageLanguage, { zh: "保存书籍配置", en: "saving book config" });
105
153
  await this.state.saveBookConfig(book.id, book);
154
+ this.logStage(stageLanguage, { zh: "生成基础设定", en: "generating foundation" });
106
155
  const { profile: gp } = await this.loadGenreProfile(book.genre);
107
156
  const foundation = await architect.generateFoundation(book, this.config.externalContext);
108
- await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem);
157
+ this.logStage(stageLanguage, { zh: "写入基础设定文件", en: "writing foundation files" });
158
+ await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, book.language ?? gp.language);
159
+ this.logStage(stageLanguage, { zh: "初始化控制文档", en: "initializing control documents" });
160
+ await this.state.ensureControlDocuments(book.id, this.config.externalContext);
109
161
  // Ensure chapters directory exists (prevents ENOENT if init was previously interrupted)
110
162
  await mkdir(join(bookDir, "chapters"), { recursive: true });
111
163
  await this.state.saveChapterIndex(book.id, []);
112
164
  // Snapshot initial state so rewrite of chapter 1 can restore to pre-chapter state
165
+ this.logStage(stageLanguage, { zh: "创建初始快照", en: "creating initial snapshot" });
113
166
  await this.state.snapshotState(book.id, 0);
114
167
  }
115
168
  /** Import external source material and generate fanfic_canon.md */
@@ -126,15 +179,23 @@ export class PipelineRunner {
126
179
  /** One-step fanfic book creation: create book + import canon + generate foundation */
127
180
  async initFanficBook(book, sourceText, sourceName, fanficMode) {
128
181
  const bookDir = this.state.bookDir(book.id);
182
+ const stageLanguage = await this.resolveBookLanguage(book);
183
+ this.logStage(stageLanguage, { zh: "保存书籍配置", en: "saving book config" });
129
184
  await this.state.saveBookConfig(book.id, book);
130
185
  // Step 1: Import source material → fanfic_canon.md
186
+ this.logStage(stageLanguage, { zh: "导入同人正典", en: "importing fanfic canon" });
131
187
  const fanficCanon = await this.importFanficCanon(book.id, sourceText, sourceName, fanficMode);
132
188
  // Step 2: Generate foundation from fanfic canon (not from scratch)
133
189
  const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
190
+ this.logStage(stageLanguage, { zh: "生成同人基础设定", en: "generating fanfic foundation" });
134
191
  const { profile: gp } = await this.loadGenreProfile(book.genre);
135
192
  const foundation = await architect.generateFanficFoundation(book, fanficCanon, fanficMode);
136
- await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem);
193
+ this.logStage(stageLanguage, { zh: "写入基础设定文件", en: "writing foundation files" });
194
+ await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, book.language ?? gp.language);
195
+ this.logStage(stageLanguage, { zh: "初始化控制文档", en: "initializing control documents" });
196
+ await this.state.ensureControlDocuments(book.id, this.config.externalContext);
137
197
  // Step 3: Initialize chapters directory + snapshot
198
+ this.logStage(stageLanguage, { zh: "创建初始快照", en: "creating initial snapshot" });
138
199
  await mkdir(join(bookDir, "chapters"), { recursive: true });
139
200
  await this.state.saveChapterIndex(book.id, []);
140
201
  await this.state.snapshotState(book.id, 0);
@@ -143,58 +204,147 @@ export class PipelineRunner {
143
204
  async writeDraft(bookId, context, wordCount) {
144
205
  const releaseLock = await this.state.acquireBookLock(bookId);
145
206
  try {
207
+ await this.state.ensureControlDocuments(bookId);
146
208
  const book = await this.state.loadBookConfig(bookId);
147
209
  const bookDir = this.state.bookDir(bookId);
148
210
  const chapterNumber = await this.state.getNextChapterNumber(bookId);
211
+ const stageLanguage = await this.resolveBookLanguage(book);
212
+ this.logStage(stageLanguage, { zh: "准备章节输入", en: "preparing chapter inputs" });
213
+ const writeInput = await this.prepareWriteInput(book, bookDir, chapterNumber, context ?? this.config.externalContext);
149
214
  const { profile: gp } = await this.loadGenreProfile(book.genre);
215
+ const lengthSpec = buildLengthSpec(wordCount ?? book.chapterWordCount, book.language ?? gp.language);
150
216
  const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
217
+ this.logStage(stageLanguage, { zh: "撰写章节草稿", en: "writing chapter draft" });
151
218
  const output = await writer.writeChapter({
152
219
  book,
153
220
  bookDir,
154
221
  chapterNumber,
155
- externalContext: context ?? this.config.externalContext,
222
+ ...writeInput,
223
+ lengthSpec,
156
224
  ...(wordCount ? { wordCountOverride: wordCount } : {}),
157
225
  });
226
+ const writerCount = countChapterLength(output.content, lengthSpec.countingMode);
227
+ let totalUsage = output.tokenUsage ?? {
228
+ promptTokens: 0,
229
+ completionTokens: 0,
230
+ totalTokens: 0,
231
+ };
232
+ const normalizedDraft = await this.normalizeDraftLengthIfNeeded({
233
+ bookId,
234
+ chapterNumber,
235
+ chapterContent: output.content,
236
+ lengthSpec,
237
+ chapterIntent: writeInput.chapterIntent,
238
+ });
239
+ totalUsage = PipelineRunner.addUsage(totalUsage, normalizedDraft.tokenUsage);
240
+ const draftOutput = {
241
+ ...output,
242
+ content: normalizedDraft.content,
243
+ wordCount: normalizedDraft.wordCount,
244
+ tokenUsage: totalUsage,
245
+ };
246
+ const lengthWarnings = this.buildLengthWarnings(chapterNumber, draftOutput.wordCount, lengthSpec);
247
+ const lengthTelemetry = this.buildLengthTelemetry({
248
+ lengthSpec,
249
+ writerCount,
250
+ postWriterNormalizeCount: normalizedDraft.wordCount,
251
+ postReviseCount: 0,
252
+ finalCount: draftOutput.wordCount,
253
+ normalizeApplied: normalizedDraft.applied,
254
+ lengthWarning: lengthWarnings.length > 0,
255
+ });
256
+ this.logLengthWarnings(lengthWarnings);
158
257
  // Save chapter file
159
258
  const chaptersDir = join(bookDir, "chapters");
160
259
  const paddedNum = String(chapterNumber).padStart(4, "0");
161
- const sanitized = output.title.replace(/[/\\?%*:|"<>]/g, "").replace(/\s+/g, "_").slice(0, 50);
260
+ const sanitized = draftOutput.title.replace(/[/\\?%*:|"<>]/g, "").replace(/\s+/g, "_").slice(0, 50);
162
261
  const filename = `${paddedNum}_${sanitized}.md`;
163
262
  const filePath = join(chaptersDir, filename);
164
263
  const resolvedLang = book.language ?? gp.language;
165
264
  const heading = resolvedLang === "en"
166
- ? `# Chapter ${chapterNumber}: ${output.title}`
167
- : `# 第${chapterNumber}章 ${output.title}`;
168
- await writeFile(filePath, `${heading}\n\n${output.content}`, "utf-8");
265
+ ? `# Chapter ${chapterNumber}: ${draftOutput.title}`
266
+ : `# 第${chapterNumber}章 ${draftOutput.title}`;
267
+ await writeFile(filePath, `${heading}\n\n${draftOutput.content}`, "utf-8");
169
268
  // Save truth files
170
- await writer.saveChapter(bookDir, output, gp.numericalSystem, resolvedLang);
171
- await writer.saveNewTruthFiles(bookDir, output);
269
+ this.logStage(stageLanguage, { zh: "落盘草稿与真相文件", en: "persisting draft and truth files" });
270
+ await writer.saveChapter(bookDir, draftOutput, gp.numericalSystem, resolvedLang);
271
+ await writer.saveNewTruthFiles(bookDir, draftOutput, resolvedLang);
272
+ await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, draftOutput);
273
+ await this.syncNarrativeMemoryIndex(bookId);
172
274
  // Update index
173
275
  const existingIndex = await this.state.loadChapterIndex(bookId);
174
276
  const now = new Date().toISOString();
175
277
  const newEntry = {
176
278
  number: chapterNumber,
177
- title: output.title,
279
+ title: draftOutput.title,
178
280
  status: "drafted",
179
- wordCount: output.wordCount,
281
+ wordCount: draftOutput.wordCount,
180
282
  createdAt: now,
181
283
  updatedAt: now,
182
284
  auditIssues: [],
183
- ...(output.tokenUsage ? { tokenUsage: output.tokenUsage } : {}),
285
+ lengthWarnings,
286
+ lengthTelemetry,
287
+ ...(draftOutput.tokenUsage ? { tokenUsage: draftOutput.tokenUsage } : {}),
184
288
  };
185
289
  await this.state.saveChapterIndex(bookId, [...existingIndex, newEntry]);
290
+ await this.markBookActiveIfNeeded(bookId);
186
291
  // Snapshot
292
+ this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" });
187
293
  await this.state.snapshotState(bookId, chapterNumber);
294
+ await this.syncCurrentStateFactHistory(bookId, chapterNumber);
188
295
  await this.emitWebhook("chapter-complete", bookId, chapterNumber, {
189
- title: output.title,
190
- wordCount: output.wordCount,
296
+ title: draftOutput.title,
297
+ wordCount: draftOutput.wordCount,
191
298
  });
192
- return { chapterNumber, title: output.title, wordCount: output.wordCount, filePath, tokenUsage: output.tokenUsage };
299
+ return {
300
+ chapterNumber,
301
+ title: draftOutput.title,
302
+ wordCount: draftOutput.wordCount,
303
+ filePath,
304
+ lengthWarnings,
305
+ lengthTelemetry,
306
+ tokenUsage: draftOutput.tokenUsage,
307
+ };
193
308
  }
194
309
  finally {
195
310
  await releaseLock();
196
311
  }
197
312
  }
313
+ async planChapter(bookId, context) {
314
+ await this.state.ensureControlDocuments(bookId);
315
+ const book = await this.state.loadBookConfig(bookId);
316
+ const bookDir = this.state.bookDir(bookId);
317
+ const chapterNumber = await this.state.getNextChapterNumber(bookId);
318
+ const stageLanguage = await this.resolveBookLanguage(book);
319
+ this.logStage(stageLanguage, { zh: "规划下一章意图", en: "planning next chapter intent" });
320
+ const { plan } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, context ?? this.config.externalContext, { reuseExistingIntentWhenContextMissing: false });
321
+ return {
322
+ bookId,
323
+ chapterNumber,
324
+ intentPath: this.relativeToBookDir(bookDir, plan.runtimePath),
325
+ goal: plan.intent.goal,
326
+ conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
327
+ };
328
+ }
329
+ async composeChapter(bookId, context) {
330
+ await this.state.ensureControlDocuments(bookId);
331
+ const book = await this.state.loadBookConfig(bookId);
332
+ const bookDir = this.state.bookDir(bookId);
333
+ const chapterNumber = await this.state.getNextChapterNumber(bookId);
334
+ const stageLanguage = await this.resolveBookLanguage(book);
335
+ this.logStage(stageLanguage, { zh: "组装章节运行时上下文", en: "composing chapter runtime context" });
336
+ const { plan, composed } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, context ?? this.config.externalContext, { reuseExistingIntentWhenContextMissing: true });
337
+ return {
338
+ bookId,
339
+ chapterNumber,
340
+ intentPath: this.relativeToBookDir(bookDir, plan.runtimePath),
341
+ goal: plan.intent.goal,
342
+ conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
343
+ contextPath: this.relativeToBookDir(bookDir, composed.contextPath),
344
+ ruleStackPath: this.relativeToBookDir(bookDir, composed.ruleStackPath),
345
+ tracePath: this.relativeToBookDir(bookDir, composed.tracePath),
346
+ };
347
+ }
198
348
  /** Audit the latest (or specified) chapter. Read-only, no lock needed. */
199
349
  async auditDraft(bookId, chapterNumber) {
200
350
  const book = await this.state.loadBookConfig(bookId);
@@ -205,22 +355,21 @@ export class PipelineRunner {
205
355
  }
206
356
  const content = await this.readChapterContent(bookDir, targetChapter);
207
357
  const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
208
- const llmResult = await auditor.auditChapter(bookDir, content, targetChapter, book.genre);
209
- // Merge rule-based AI-tell detection
210
- const aiTells = analyzeAITells(content);
211
- // Merge sensitive word detection
212
- const sensitiveResult = analyzeSensitiveWords(content);
213
- const hasBlockedWords = sensitiveResult.found.some((f) => f.severity === "block");
214
- const mergedIssues = [
215
- ...llmResult.issues,
216
- ...aiTells.issues,
217
- ...sensitiveResult.issues,
218
- ];
219
- const result = {
220
- passed: hasBlockedWords ? false : llmResult.passed,
221
- issues: mergedIssues,
222
- summary: llmResult.summary,
223
- };
358
+ const { profile: gp } = await this.loadGenreProfile(book.genre);
359
+ const language = book.language ?? gp.language;
360
+ this.logStage(language, {
361
+ zh: `审计第${targetChapter}章`,
362
+ en: `auditing chapter ${targetChapter}`,
363
+ });
364
+ const evaluation = await this.evaluateMergedAudit({
365
+ auditor,
366
+ book,
367
+ bookDir,
368
+ chapterContent: content,
369
+ chapterNumber: targetChapter,
370
+ language,
371
+ });
372
+ const result = evaluation.auditResult;
224
373
  // Update index with audit result
225
374
  const index = await this.state.loadChapterIndex(bookId);
226
375
  const updated = index.map((ch) => ch.number === targetChapter
@@ -236,7 +385,7 @@ export class PipelineRunner {
236
385
  return { ...result, chapterNumber: targetChapter };
237
386
  }
238
387
  /** Revise the latest (or specified) chapter based on audit issues. */
239
- async reviseDraft(bookId, chapterNumber, mode = "rewrite") {
388
+ async reviseDraft(bookId, chapterNumber, mode = DEFAULT_REVISE_MODE) {
240
389
  const releaseLock = await this.state.acquireBookLock(bookId);
241
390
  try {
242
391
  const book = await this.state.loadBookConfig(bookId);
@@ -245,7 +394,12 @@ export class PipelineRunner {
245
394
  if (targetChapter < 1) {
246
395
  throw new Error(`No chapters to revise for "${bookId}"`);
247
396
  }
397
+ const stageLanguage = await this.resolveBookLanguage(book);
248
398
  // Read the current audit issues from index
399
+ this.logStage(stageLanguage, {
400
+ zh: `加载第${targetChapter}章修订上下文`,
401
+ en: `loading revision context for chapter ${targetChapter}`,
402
+ });
249
403
  const index = await this.state.loadChapterIndex(bookId);
250
404
  const chapterMeta = index.find((ch) => ch.number === targetChapter);
251
405
  if (!chapterMeta) {
@@ -254,17 +408,129 @@ export class PipelineRunner {
254
408
  // Re-audit to get structured issues (index only stores strings)
255
409
  const content = await this.readChapterContent(bookDir, targetChapter);
256
410
  const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
257
- const auditResult = await auditor.auditChapter(bookDir, content, targetChapter, book.genre);
258
- if (auditResult.passed && auditResult.issues.filter(i => i.severity === "warning" || i.severity === "critical").length === 0) {
259
- return { chapterNumber: targetChapter, wordCount: content.length, fixedIssues: [] };
260
- }
261
411
  const { profile: gp } = await this.loadGenreProfile(book.genre);
412
+ const language = book.language ?? gp.language;
413
+ const countingMode = resolveLengthCountingMode(language);
414
+ const reviseControlInput = (this.config.inputGovernanceMode ?? "v2") === "legacy"
415
+ ? undefined
416
+ : await this.createGovernedArtifacts(book, bookDir, targetChapter, undefined, { reuseExistingIntentWhenContextMissing: true });
417
+ const preRevision = await this.evaluateMergedAudit({
418
+ auditor,
419
+ book,
420
+ bookDir,
421
+ chapterContent: content,
422
+ chapterNumber: targetChapter,
423
+ language,
424
+ auditOptions: reviseControlInput
425
+ ? {
426
+ chapterIntent: reviseControlInput.plan.intentMarkdown,
427
+ contextPackage: reviseControlInput.composed.contextPackage,
428
+ ruleStack: reviseControlInput.composed.ruleStack,
429
+ }
430
+ : undefined,
431
+ });
432
+ if (preRevision.blockingCount === 0 && preRevision.aiTellCount === 0) {
433
+ return {
434
+ chapterNumber: targetChapter,
435
+ wordCount: countChapterLength(content, countingMode),
436
+ fixedIssues: [],
437
+ applied: false,
438
+ status: "unchanged",
439
+ skippedReason: "No warning, critical, or AI-tell issues to fix.",
440
+ };
441
+ }
442
+ const chapterLengthTarget = chapterMeta.lengthTelemetry?.target ?? book.chapterWordCount;
443
+ const lengthLanguage = chapterMeta.lengthTelemetry?.countingMode === "en_words"
444
+ ? "en"
445
+ : language;
446
+ const lengthSpec = buildLengthSpec(chapterLengthTarget, lengthLanguage);
262
447
  const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
263
- const reviseOutput = await reviser.reviseChapter(bookDir, content, targetChapter, auditResult.issues, mode, book.genre);
448
+ this.logStage(stageLanguage, {
449
+ zh: `修订第${targetChapter}章`,
450
+ en: `revising chapter ${targetChapter}`,
451
+ });
452
+ const reviseOutput = await reviser.reviseChapter(bookDir, content, targetChapter, preRevision.auditResult.issues, mode, book.genre, reviseControlInput
453
+ ? {
454
+ chapterIntent: reviseControlInput.plan.intentMarkdown,
455
+ contextPackage: reviseControlInput.composed.contextPackage,
456
+ ruleStack: reviseControlInput.composed.ruleStack,
457
+ lengthSpec,
458
+ }
459
+ : { lengthSpec });
264
460
  if (reviseOutput.revisedContent.length === 0) {
265
461
  throw new Error("Reviser returned empty content");
266
462
  }
463
+ const normalizedRevision = await this.normalizeDraftLengthIfNeeded({
464
+ bookId,
465
+ chapterNumber: targetChapter,
466
+ chapterContent: reviseOutput.revisedContent,
467
+ lengthSpec,
468
+ });
469
+ const postRevision = await this.evaluateMergedAudit({
470
+ auditor,
471
+ book,
472
+ bookDir,
473
+ chapterContent: normalizedRevision.content,
474
+ chapterNumber: targetChapter,
475
+ language,
476
+ auditOptions: reviseControlInput
477
+ ? {
478
+ temperature: 0,
479
+ chapterIntent: reviseControlInput.plan.intentMarkdown,
480
+ contextPackage: reviseControlInput.composed.contextPackage,
481
+ ruleStack: reviseControlInput.composed.ruleStack,
482
+ truthFileOverrides: {
483
+ currentState: reviseOutput.updatedState !== "(状态卡未更新)" ? reviseOutput.updatedState : undefined,
484
+ ledger: reviseOutput.updatedLedger !== "(账本未更新)" ? reviseOutput.updatedLedger : undefined,
485
+ hooks: reviseOutput.updatedHooks !== "(伏笔池未更新)" ? reviseOutput.updatedHooks : undefined,
486
+ },
487
+ }
488
+ : {
489
+ temperature: 0,
490
+ truthFileOverrides: {
491
+ currentState: reviseOutput.updatedState !== "(状态卡未更新)" ? reviseOutput.updatedState : undefined,
492
+ ledger: reviseOutput.updatedLedger !== "(账本未更新)" ? reviseOutput.updatedLedger : undefined,
493
+ hooks: reviseOutput.updatedHooks !== "(伏笔池未更新)" ? reviseOutput.updatedHooks : undefined,
494
+ },
495
+ },
496
+ });
497
+ const effectivePostRevision = this.restoreActionableAuditIfLost(preRevision, postRevision);
498
+ const revisionBaseCount = countChapterLength(content, lengthSpec.countingMode);
499
+ const lengthWarnings = this.buildLengthWarnings(targetChapter, normalizedRevision.wordCount, lengthSpec);
500
+ const lengthTelemetry = this.buildLengthTelemetry({
501
+ lengthSpec,
502
+ writerCount: revisionBaseCount,
503
+ postWriterNormalizeCount: 0,
504
+ postReviseCount: normalizedRevision.wordCount,
505
+ finalCount: normalizedRevision.wordCount,
506
+ normalizeApplied: normalizedRevision.applied,
507
+ lengthWarning: lengthWarnings.length > 0,
508
+ });
509
+ const improvedBlocking = effectivePostRevision.blockingCount < preRevision.blockingCount;
510
+ const improvedAITells = effectivePostRevision.aiTellCount < preRevision.aiTellCount;
511
+ const blockingDidNotWorsen = effectivePostRevision.blockingCount <= preRevision.blockingCount;
512
+ const criticalDidNotWorsen = effectivePostRevision.criticalCount <= preRevision.criticalCount;
513
+ const aiDidNotWorsen = effectivePostRevision.aiTellCount <= preRevision.aiTellCount;
514
+ const shouldApplyRevision = blockingDidNotWorsen
515
+ && criticalDidNotWorsen
516
+ && aiDidNotWorsen
517
+ && (improvedBlocking || improvedAITells);
518
+ if (!shouldApplyRevision) {
519
+ return {
520
+ chapterNumber: targetChapter,
521
+ wordCount: revisionBaseCount,
522
+ fixedIssues: [],
523
+ applied: false,
524
+ status: "unchanged",
525
+ skippedReason: "Manual revision did not improve merged audit or AI-tell metrics; kept original chapter.",
526
+ };
527
+ }
528
+ this.logLengthWarnings(lengthWarnings);
267
529
  // Save revised chapter file
530
+ this.logStage(stageLanguage, {
531
+ zh: `落盘第${targetChapter}章修订结果`,
532
+ en: `persisting revision for chapter ${targetChapter}`,
533
+ });
268
534
  const chaptersDir = join(bookDir, "chapters");
269
535
  const files = await readdir(chaptersDir);
270
536
  const paddedNum = String(targetChapter).padStart(4, "0");
@@ -276,7 +542,7 @@ export class PipelineRunner {
276
542
  const reviseHeading = reviseLang === "en"
277
543
  ? `# Chapter ${targetChapter}: ${chapterMeta.title}`
278
544
  : `# 第${targetChapter}章 ${chapterMeta.title}`;
279
- await writeFile(join(chaptersDir, existingFile), `${reviseHeading}\n\n${reviseOutput.revisedContent}`, "utf-8");
545
+ await writeFile(join(chaptersDir, existingFile), `${reviseHeading}\n\n${normalizedRevision.content}`, "utf-8");
280
546
  // Update truth files
281
547
  const storyDir = join(bookDir, "story");
282
548
  if (reviseOutput.updatedState !== "(状态卡未更新)") {
@@ -288,26 +554,40 @@ export class PipelineRunner {
288
554
  if (reviseOutput.updatedHooks !== "(伏笔池未更新)") {
289
555
  await writeFile(join(storyDir, "pending_hooks.md"), reviseOutput.updatedHooks, "utf-8");
290
556
  }
557
+ await this.syncLegacyStructuredStateFromMarkdown(bookDir, targetChapter);
291
558
  // Update index
292
559
  const updatedIndex = index.map((ch) => ch.number === targetChapter
293
560
  ? {
294
561
  ...ch,
295
- status: "ready-for-review",
296
- wordCount: reviseOutput.wordCount,
562
+ status: (effectivePostRevision.auditResult.passed ? "ready-for-review" : "audit-failed"),
563
+ wordCount: normalizedRevision.wordCount,
297
564
  updatedAt: new Date().toISOString(),
565
+ auditIssues: effectivePostRevision.auditResult.issues.map((i) => `[${i.severity}] ${i.description}`),
566
+ lengthWarnings,
567
+ lengthTelemetry,
298
568
  }
299
569
  : ch);
300
570
  await this.state.saveChapterIndex(bookId, updatedIndex);
301
571
  // Re-snapshot
572
+ this.logStage(stageLanguage, {
573
+ zh: `更新第${targetChapter}章索引与快照`,
574
+ en: `updating chapter index and snapshots for chapter ${targetChapter}`,
575
+ });
302
576
  await this.state.snapshotState(bookId, targetChapter);
577
+ await this.syncNarrativeMemoryIndex(bookId);
578
+ await this.syncCurrentStateFactHistory(bookId, targetChapter);
303
579
  await this.emitWebhook("revision-complete", bookId, targetChapter, {
304
- wordCount: reviseOutput.wordCount,
580
+ wordCount: normalizedRevision.wordCount,
305
581
  fixedCount: reviseOutput.fixedIssues.length,
306
582
  });
307
583
  return {
308
584
  chapterNumber: targetChapter,
309
- wordCount: reviseOutput.wordCount,
585
+ wordCount: normalizedRevision.wordCount,
310
586
  fixedIssues: reviseOutput.fixedIssues,
587
+ applied: true,
588
+ status: effectivePostRevision.auditResult.passed ? "ready-for-review" : "audit-failed",
589
+ lengthWarnings,
590
+ lengthTelemetry,
311
591
  };
312
592
  }
313
593
  finally {
@@ -367,28 +647,49 @@ export class PipelineRunner {
367
647
  }
368
648
  }
369
649
  async _writeNextChapterLocked(bookId, wordCount, temperatureOverride) {
650
+ await this.state.ensureControlDocuments(bookId);
370
651
  const book = await this.state.loadBookConfig(bookId);
371
652
  const bookDir = this.state.bookDir(bookId);
372
653
  const chapterNumber = await this.state.getNextChapterNumber(bookId);
654
+ const stageLanguage = await this.resolveBookLanguage(book);
655
+ this.logStage(stageLanguage, { zh: "准备章节输入", en: "preparing chapter inputs" });
656
+ const writeInput = await this.prepareWriteInput(book, bookDir, chapterNumber, this.config.externalContext);
657
+ const reducedControlInput = writeInput.chapterIntent && writeInput.contextPackage && writeInput.ruleStack
658
+ ? {
659
+ chapterIntent: writeInput.chapterIntent,
660
+ contextPackage: writeInput.contextPackage,
661
+ ruleStack: writeInput.ruleStack,
662
+ }
663
+ : undefined;
373
664
  const { profile: gp } = await this.loadGenreProfile(book.genre);
665
+ const pipelineLang = book.language ?? gp.language;
666
+ const lengthSpec = buildLengthSpec(wordCount ?? book.chapterWordCount, pipelineLang);
374
667
  // 1. Write chapter
375
668
  const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
669
+ this.logStage(stageLanguage, { zh: "撰写章节草稿", en: "writing chapter draft" });
376
670
  const output = await writer.writeChapter({
377
671
  book,
378
672
  bookDir,
379
673
  chapterNumber,
380
- externalContext: this.config.externalContext,
674
+ ...writeInput,
675
+ lengthSpec,
381
676
  ...(wordCount ? { wordCountOverride: wordCount } : {}),
382
677
  ...(temperatureOverride ? { temperatureOverride } : {}),
383
678
  });
679
+ const writerCount = countChapterLength(output.content, lengthSpec.countingMode);
384
680
  // Token usage accumulator
385
681
  let totalUsage = output.tokenUsage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
682
+ let postReviseCount = 0;
683
+ let normalizeApplied = false;
386
684
  // 2a. Post-write error gate: if deterministic rules found errors, auto-fix before LLM audit
387
685
  let finalContent = output.content;
388
686
  let finalWordCount = output.wordCount;
389
687
  let revised = false;
390
688
  if (output.postWriteErrors.length > 0) {
391
- this.config.logger?.warn(`${output.postWriteErrors.length} post-write errors detected, triggering spot-fix before audit`);
689
+ this.logWarn(pipelineLang, {
690
+ zh: `检测到 ${output.postWriteErrors.length} 个后写错误,审计前触发 spot-fix 修补`,
691
+ en: `${output.postWriteErrors.length} post-write errors detected, triggering spot-fix before audit`,
692
+ });
392
693
  const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
393
694
  const spotFixIssues = output.postWriteErrors.map((v) => ({
394
695
  severity: "critical",
@@ -396,7 +697,10 @@ export class PipelineRunner {
396
697
  description: v.description,
397
698
  suggestion: v.suggestion,
398
699
  }));
399
- const fixResult = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, spotFixIssues, "spot-fix", book.genre);
700
+ const fixResult = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, spotFixIssues, "spot-fix", book.genre, {
701
+ ...reducedControlInput,
702
+ lengthSpec,
703
+ });
400
704
  totalUsage = PipelineRunner.addUsage(totalUsage, fixResult.tokenUsage);
401
705
  if (fixResult.revisedContent.length > 0) {
402
706
  finalContent = fixResult.revisedContent;
@@ -404,9 +708,21 @@ export class PipelineRunner {
404
708
  revised = true;
405
709
  }
406
710
  }
711
+ const normalizedBeforeAudit = await this.normalizeDraftLengthIfNeeded({
712
+ bookId,
713
+ chapterNumber,
714
+ chapterContent: finalContent,
715
+ lengthSpec,
716
+ chapterIntent: writeInput.chapterIntent,
717
+ });
718
+ totalUsage = PipelineRunner.addUsage(totalUsage, normalizedBeforeAudit.tokenUsage);
719
+ finalContent = normalizedBeforeAudit.content;
720
+ finalWordCount = normalizedBeforeAudit.wordCount;
721
+ normalizeApplied = normalizeApplied || normalizedBeforeAudit.applied;
407
722
  // 2b. LLM audit
408
723
  const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
409
- const llmAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre);
724
+ this.logStage(stageLanguage, { zh: "审计草稿", en: "auditing draft" });
725
+ const llmAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, reducedControlInput);
410
726
  totalUsage = PipelineRunner.addUsage(totalUsage, llmAudit.tokenUsage);
411
727
  const aiTellsResult = analyzeAITells(finalContent);
412
728
  const sensitiveWriteResult = analyzeSensitiveWords(finalContent);
@@ -421,42 +737,84 @@ export class PipelineRunner {
421
737
  const criticalIssues = auditResult.issues.filter((i) => i.severity === "critical");
422
738
  if (criticalIssues.length > 0) {
423
739
  const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
424
- const reviseOutput = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, auditResult.issues, "spot-fix", book.genre);
740
+ this.logStage(stageLanguage, { zh: "自动修复关键问题", en: "auto-revising critical issues" });
741
+ const reviseOutput = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, auditResult.issues, "spot-fix", book.genre, {
742
+ ...reducedControlInput,
743
+ lengthSpec,
744
+ });
425
745
  totalUsage = PipelineRunner.addUsage(totalUsage, reviseOutput.tokenUsage);
426
746
  if (reviseOutput.revisedContent.length > 0) {
747
+ const normalizedRevision = await this.normalizeDraftLengthIfNeeded({
748
+ bookId,
749
+ chapterNumber,
750
+ chapterContent: reviseOutput.revisedContent,
751
+ lengthSpec,
752
+ chapterIntent: writeInput.chapterIntent,
753
+ });
754
+ totalUsage = PipelineRunner.addUsage(totalUsage, normalizedRevision.tokenUsage);
755
+ postReviseCount = normalizedRevision.wordCount;
756
+ normalizeApplied = normalizeApplied || normalizedRevision.applied;
427
757
  // Guard: reject revision if AI markers increased
428
758
  const preMarkers = analyzeAITells(finalContent);
429
- const postMarkers = analyzeAITells(reviseOutput.revisedContent);
759
+ const postMarkers = analyzeAITells(normalizedRevision.content);
430
760
  const preCount = preMarkers.issues.length;
431
761
  const postCount = postMarkers.issues.length;
432
762
  if (postCount > preCount) {
433
763
  // Revision made text MORE AI-like — discard it, keep original
434
764
  }
435
765
  else {
436
- finalContent = reviseOutput.revisedContent;
437
- finalWordCount = reviseOutput.wordCount;
766
+ finalContent = normalizedRevision.content;
767
+ finalWordCount = normalizedRevision.wordCount;
438
768
  revised = true;
439
769
  }
440
770
  // Re-audit the (possibly revised) content
441
- const reAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, { temperature: 0 });
771
+ const reAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, { ...reducedControlInput, temperature: 0 });
442
772
  totalUsage = PipelineRunner.addUsage(totalUsage, reAudit.tokenUsage);
443
773
  const reAITells = analyzeAITells(finalContent);
444
774
  const reSensitive = analyzeSensitiveWords(finalContent);
445
775
  const reHasBlocked = reSensitive.found.some((f) => f.severity === "block");
446
- auditResult = {
776
+ auditResult = this.restoreLostAuditIssues(auditResult, {
447
777
  passed: reHasBlocked ? false : reAudit.passed,
448
778
  issues: [...reAudit.issues, ...reAITells.issues, ...reSensitive.issues],
449
779
  summary: reAudit.summary,
450
- };
780
+ });
451
781
  }
452
782
  }
453
783
  }
454
784
  // 4. Save the final chapter and truth files from a single persistence source
785
+ this.logStage(stageLanguage, { zh: "落盘最终章节", en: "persisting final chapter" });
786
+ this.logStage(stageLanguage, { zh: "生成最终真相文件", en: "rebuilding final truth files" });
455
787
  const persistenceOutput = await this.buildPersistenceOutput(bookId, book, bookDir, chapterNumber, output, finalContent);
788
+ const longSpanFatigue = await analyzeLongSpanFatigue({
789
+ bookDir,
790
+ chapterNumber,
791
+ chapterContent: finalContent,
792
+ chapterSummary: persistenceOutput.chapterSummary,
793
+ language: pipelineLang,
794
+ });
795
+ auditResult = {
796
+ ...auditResult,
797
+ issues: [
798
+ ...auditResult.issues,
799
+ ...longSpanFatigue.issues,
800
+ ...(persistenceOutput.hookHealthIssues ?? []),
801
+ ],
802
+ };
456
803
  finalWordCount = persistenceOutput.wordCount;
457
- const pipelineLang = book.language ?? gp.language;
804
+ const lengthWarnings = this.buildLengthWarnings(chapterNumber, finalWordCount, lengthSpec);
805
+ const lengthTelemetry = this.buildLengthTelemetry({
806
+ lengthSpec,
807
+ writerCount,
808
+ postWriterNormalizeCount: normalizedBeforeAudit.wordCount,
809
+ postReviseCount,
810
+ finalCount: finalWordCount,
811
+ normalizeApplied,
812
+ lengthWarning: lengthWarnings.length > 0,
813
+ });
814
+ this.logLengthWarnings(lengthWarnings);
458
815
  // 4.1 Validate settler output before writing (non-blocking)
459
816
  try {
817
+ this.logStage(stageLanguage, { zh: "校验真相文件变更", en: "validating truth file updates" });
460
818
  const storyDir = join(bookDir, "story");
461
819
  const [oldState, oldHooks] = await Promise.all([
462
820
  readFile(join(storyDir, "current_state.md"), "utf-8").catch(() => ""),
@@ -465,17 +823,26 @@ export class PipelineRunner {
465
823
  const validator = new StateValidatorAgent(this.agentCtxFor("state-validator", bookId));
466
824
  const validation = await validator.validate(finalContent, chapterNumber, oldState, persistenceOutput.updatedState, oldHooks, persistenceOutput.updatedHooks, pipelineLang);
467
825
  if (validation.warnings.length > 0) {
468
- this.config.logger?.warn(`State validation: ${validation.warnings.length} warning(s) for chapter ${chapterNumber}`);
826
+ this.logWarn(pipelineLang, {
827
+ zh: `状态校验:第${chapterNumber}章发现 ${validation.warnings.length} 条警告`,
828
+ en: `State validation: ${validation.warnings.length} warning(s) for chapter ${chapterNumber}`,
829
+ });
469
830
  for (const w of validation.warnings) {
470
831
  this.config.logger?.warn(` [${w.category}] ${w.description}`);
471
832
  }
472
833
  }
473
834
  }
474
835
  catch (e) {
475
- this.config.logger?.warn(`State validation skipped: ${e}`);
836
+ this.logWarn(pipelineLang, {
837
+ zh: `状态校验已跳过:${String(e)}`,
838
+ en: `State validation skipped: ${String(e)}`,
839
+ });
476
840
  }
477
841
  await writer.saveChapter(bookDir, persistenceOutput, gp.numericalSystem, pipelineLang);
478
- await writer.saveNewTruthFiles(bookDir, persistenceOutput);
842
+ await writer.saveNewTruthFiles(bookDir, persistenceOutput, pipelineLang);
843
+ await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, persistenceOutput);
844
+ this.logStage(stageLanguage, { zh: "同步记忆索引", en: "syncing memory indexes" });
845
+ await this.syncNarrativeMemoryIndex(bookId);
479
846
  // 5. Update chapter index
480
847
  const existingIndex = await this.state.loadChapterIndex(bookId);
481
848
  const now = new Date().toISOString();
@@ -487,9 +854,12 @@ export class PipelineRunner {
487
854
  createdAt: now,
488
855
  updatedAt: now,
489
856
  auditIssues: auditResult.issues.map((i) => `[${i.severity}] ${i.description}`),
857
+ lengthWarnings,
858
+ lengthTelemetry,
490
859
  tokenUsage: totalUsage,
491
860
  };
492
861
  await this.state.saveChapterIndex(bookId, [...existingIndex, newEntry]);
862
+ await this.markBookActiveIfNeeded(bookId);
493
863
  // 5.5 Audit drift correction — feed audit findings back into state
494
864
  // This prevents the writer from repeating mistakes in the next chapter
495
865
  const driftIssues = auditResult.issues.filter((i) => i.severity === "critical" || i.severity === "warning");
@@ -499,10 +869,16 @@ export class PipelineRunner {
499
869
  const statePath = join(storyDir, "current_state.md");
500
870
  const currentState = await readFile(statePath, "utf-8").catch(() => "");
501
871
  // Append drift correction section (or replace existing one)
502
- const correctionHeader = "## 审计纠偏(自动生成,下一章写作前参照)";
872
+ const correctionHeader = this.localize(stageLanguage, {
873
+ zh: "## 审计纠偏(自动生成,下一章写作前参照)",
874
+ en: "## Audit Drift Correction",
875
+ });
503
876
  const correctionBlock = [
504
877
  correctionHeader,
505
- `> 第${chapterNumber}章审计发现以下问题,下一章写作时必须避免:`,
878
+ this.localize(stageLanguage, {
879
+ zh: `> 第${chapterNumber}章审计发现以下问题,下一章写作时必须避免:`,
880
+ en: `> Chapter ${chapterNumber} audit found the following issues to avoid in the next chapter:`,
881
+ }),
506
882
  ...driftIssues.map((i) => `> - [${i.severity}] ${i.category}: ${i.description}`),
507
883
  "",
508
884
  ].join("\n");
@@ -518,14 +894,17 @@ export class PipelineRunner {
518
894
  }
519
895
  }
520
896
  // 5.6 Snapshot state for rollback support
897
+ this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" });
521
898
  await this.state.snapshotState(bookId, chapterNumber);
899
+ await this.syncCurrentStateFactHistory(bookId, chapterNumber);
522
900
  // 6. Send notification
523
901
  if (this.config.notifyChannels && this.config.notifyChannels.length > 0) {
524
902
  const statusEmoji = auditResult.passed ? "✅" : "⚠️";
903
+ const chapterLength = formatLengthCount(finalWordCount, lengthSpec.countingMode);
525
904
  await dispatchNotification(this.config.notifyChannels, {
526
905
  title: `${statusEmoji} ${book.title} 第${chapterNumber}章`,
527
906
  body: [
528
- `**${persistenceOutput.title}** | ${finalWordCount}字`,
907
+ `**${persistenceOutput.title}** | ${chapterLength}`,
529
908
  revised ? "📝 已自动修正" : "",
530
909
  `审稿: ${auditResult.passed ? "通过" : "需人工审核"}`,
531
910
  ...auditResult.issues
@@ -549,6 +928,8 @@ export class PipelineRunner {
549
928
  auditResult,
550
929
  revised,
551
930
  status: auditResult.passed ? "ready-for-review" : "audit-failed",
931
+ lengthWarnings,
932
+ lengthTelemetry,
552
933
  tokenUsage: totalUsage,
553
934
  };
554
935
  }
@@ -756,28 +1137,46 @@ ${matrix}`,
756
1137
  const book = await this.state.loadBookConfig(input.bookId);
757
1138
  const bookDir = this.state.bookDir(input.bookId);
758
1139
  const { profile: gp } = await this.loadGenreProfile(book.genre);
1140
+ const resolvedLanguage = book.language ?? gp.language;
759
1141
  const startFrom = input.resumeFrom ?? 1;
760
1142
  const log = this.config.logger?.child("import");
761
1143
  // Step 1: Generate foundation on first run (not on resume)
762
1144
  if (startFrom === 1) {
763
- log?.info(`Step 1: Generating foundation from ${input.chapters.length} chapters...`);
764
- const allText = input.chapters.map((c, i) => `第${i + 1} ${c.title}\n\n${c.content}`).join("\n\n---\n\n");
1145
+ log?.info(this.localize(resolvedLanguage, {
1146
+ zh: `步骤 1:从 ${input.chapters.length} 章生成基础设定...`,
1147
+ en: `Step 1: Generating foundation from ${input.chapters.length} chapters...`,
1148
+ }));
1149
+ const allText = input.chapters.map((c, i) => resolvedLanguage === "en"
1150
+ ? `Chapter ${i + 1}: ${c.title}\n\n${c.content}`
1151
+ : `第${i + 1}章 ${c.title}\n\n${c.content}`).join("\n\n---\n\n");
765
1152
  const architect = new ArchitectAgent(this.agentCtxFor("architect", input.bookId));
766
1153
  const foundation = await architect.generateFoundationFromImport(book, allText);
767
- await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem);
1154
+ await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, resolvedLanguage);
1155
+ await this.resetImportReplayTruthFiles(bookDir, resolvedLanguage);
768
1156
  await this.state.saveChapterIndex(input.bookId, []);
769
- log?.info("Foundation generated.");
1157
+ await this.state.snapshotState(input.bookId, 0);
1158
+ log?.info(this.localize(resolvedLanguage, {
1159
+ zh: "基础设定已生成。",
1160
+ en: "Foundation generated.",
1161
+ }));
770
1162
  }
771
1163
  // Step 2: Sequential replay
772
- log?.info(`Step 2: Sequential replay from chapter ${startFrom}...`);
1164
+ log?.info(this.localize(resolvedLanguage, {
1165
+ zh: `步骤 2:从第 ${startFrom} 章开始顺序回放...`,
1166
+ en: `Step 2: Sequential replay from chapter ${startFrom}...`,
1167
+ }));
773
1168
  const analyzer = new ChapterAnalyzerAgent(this.agentCtxFor("chapter-analyzer", input.bookId));
774
1169
  const writer = new WriterAgent(this.agentCtxFor("writer", input.bookId));
1170
+ const countingMode = resolveLengthCountingMode(book.language ?? gp.language);
775
1171
  let totalWords = 0;
776
1172
  let importedCount = 0;
777
1173
  for (let i = startFrom - 1; i < input.chapters.length; i++) {
778
1174
  const ch = input.chapters[i];
779
1175
  const chapterNumber = i + 1;
780
- log?.info(`Analyzing chapter ${chapterNumber}/${input.chapters.length}: ${ch.title}...`);
1176
+ log?.info(this.localize(resolvedLanguage, {
1177
+ zh: `分析章节 ${chapterNumber}/${input.chapters.length}:${ch.title}...`,
1178
+ en: `Analyzing chapter ${chapterNumber}/${input.chapters.length}: ${ch.title}...`,
1179
+ }));
781
1180
  // Analyze chapter to get truth file updates
782
1181
  const output = await analyzer.analyzeChapter({
783
1182
  book,
@@ -791,24 +1190,28 @@ ${matrix}`,
791
1190
  ...output,
792
1191
  postWriteErrors: [],
793
1192
  postWriteWarnings: [],
794
- }, gp.numericalSystem);
1193
+ }, gp.numericalSystem, resolvedLanguage);
795
1194
  // Save extended truth files (summaries, subplots, emotional arcs, character matrix)
796
1195
  await writer.saveNewTruthFiles(bookDir, {
797
1196
  ...output,
798
1197
  postWriteErrors: [],
799
1198
  postWriteWarnings: [],
800
- });
1199
+ }, resolvedLanguage);
1200
+ await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, output);
1201
+ await this.syncNarrativeMemoryIndex(input.bookId);
801
1202
  // Update chapter index
802
1203
  const existingIndex = await this.state.loadChapterIndex(input.bookId);
803
1204
  const now = new Date().toISOString();
1205
+ const chapterWordCount = countChapterLength(ch.content, countingMode);
804
1206
  const newEntry = {
805
1207
  number: chapterNumber,
806
1208
  title: output.title,
807
1209
  status: "imported",
808
- wordCount: ch.content.length,
1210
+ wordCount: chapterWordCount,
809
1211
  createdAt: now,
810
1212
  updatedAt: now,
811
1213
  auditIssues: [],
1214
+ lengthWarnings: [],
812
1215
  };
813
1216
  // Replace if exists (resume case), otherwise append
814
1217
  const existingIdx = existingIndex.findIndex((e) => e.number === chapterNumber);
@@ -819,10 +1222,17 @@ ${matrix}`,
819
1222
  // Snapshot state after each chapter for rollback + resume support
820
1223
  await this.state.snapshotState(input.bookId, chapterNumber);
821
1224
  importedCount++;
822
- totalWords += ch.content.length;
1225
+ totalWords += chapterWordCount;
1226
+ }
1227
+ if (input.chapters.length > 0) {
1228
+ await this.markBookActiveIfNeeded(input.bookId);
1229
+ await this.syncCurrentStateFactHistory(input.bookId, input.chapters.length);
823
1230
  }
824
1231
  const nextChapter = input.chapters.length + 1;
825
- log?.info(`Done. ${importedCount} chapters imported, ${totalWords} chars. Next chapter: ${nextChapter}`);
1232
+ log?.info(this.localize(resolvedLanguage, {
1233
+ zh: `完成。已导入 ${importedCount} 章,共 ${formatLengthCount(totalWords, countingMode)}。下一章:${nextChapter}`,
1234
+ en: `Done. ${importedCount} chapters imported, ${formatLengthCount(totalWords, countingMode)}. Next chapter: ${nextChapter}`,
1235
+ }));
826
1236
  return {
827
1237
  bookId: input.bookId,
828
1238
  importedCount,
@@ -859,12 +1269,558 @@ ${matrix}`,
859
1269
  ...analyzed,
860
1270
  postWriteErrors: [],
861
1271
  postWriteWarnings: [],
1272
+ hookHealthIssues: output.hookHealthIssues,
862
1273
  tokenUsage: output.tokenUsage,
863
1274
  };
864
1275
  }
865
1276
  // ---------------------------------------------------------------------------
866
1277
  // Helpers
867
1278
  // ---------------------------------------------------------------------------
1279
+ async prepareWriteInput(book, bookDir, chapterNumber, externalContext) {
1280
+ if ((this.config.inputGovernanceMode ?? "v2") === "legacy") {
1281
+ return { externalContext };
1282
+ }
1283
+ const { plan, composed } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, externalContext, { reuseExistingIntentWhenContextMissing: true });
1284
+ return {
1285
+ chapterIntent: plan.intentMarkdown,
1286
+ contextPackage: composed.contextPackage,
1287
+ ruleStack: composed.ruleStack,
1288
+ trace: composed.trace,
1289
+ };
1290
+ }
1291
+ async resetImportReplayTruthFiles(bookDir, language) {
1292
+ const storyDir = join(bookDir, "story");
1293
+ await Promise.all([
1294
+ writeFile(join(storyDir, "current_state.md"), this.buildImportReplayStateSeed(language), "utf-8"),
1295
+ writeFile(join(storyDir, "pending_hooks.md"), this.buildImportReplayHooksSeed(language), "utf-8"),
1296
+ rm(join(storyDir, "chapter_summaries.md"), { force: true }),
1297
+ rm(join(storyDir, "subplot_board.md"), { force: true }),
1298
+ rm(join(storyDir, "emotional_arcs.md"), { force: true }),
1299
+ rm(join(storyDir, "character_matrix.md"), { force: true }),
1300
+ rm(join(storyDir, "volume_summaries.md"), { force: true }),
1301
+ rm(join(storyDir, "particle_ledger.md"), { force: true }),
1302
+ rm(join(storyDir, "memory.db"), { force: true }),
1303
+ rm(join(storyDir, "memory.db-shm"), { force: true }),
1304
+ rm(join(storyDir, "memory.db-wal"), { force: true }),
1305
+ rm(join(storyDir, "state"), { recursive: true, force: true }),
1306
+ rm(join(storyDir, "snapshots"), { recursive: true, force: true }),
1307
+ ]);
1308
+ }
1309
+ buildImportReplayStateSeed(language) {
1310
+ if (language === "en") {
1311
+ return [
1312
+ "# Current State",
1313
+ "",
1314
+ "| Field | Value |",
1315
+ "| --- | --- |",
1316
+ "| Current Chapter | 0 |",
1317
+ "| Current Location | (not set) |",
1318
+ "| Protagonist State | (not set) |",
1319
+ "| Current Goal | (not set) |",
1320
+ "| Current Constraint | (not set) |",
1321
+ "| Current Alliances | (not set) |",
1322
+ "| Current Conflict | (not set) |",
1323
+ "",
1324
+ ].join("\n");
1325
+ }
1326
+ return [
1327
+ "# 当前状态",
1328
+ "",
1329
+ "| 字段 | 值 |",
1330
+ "| --- | --- |",
1331
+ "| 当前章节 | 0 |",
1332
+ "| 当前位置 | (未设定) |",
1333
+ "| 主角状态 | (未设定) |",
1334
+ "| 当前目标 | (未设定) |",
1335
+ "| 当前限制 | (未设定) |",
1336
+ "| 当前敌我 | (未设定) |",
1337
+ "| 当前冲突 | (未设定) |",
1338
+ "",
1339
+ ].join("\n");
1340
+ }
1341
+ buildImportReplayHooksSeed(language) {
1342
+ if (language === "en") {
1343
+ return [
1344
+ "# Pending Hooks",
1345
+ "",
1346
+ "| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | notes |",
1347
+ "| --- | --- | --- | --- | --- | --- | --- |",
1348
+ "",
1349
+ ].join("\n");
1350
+ }
1351
+ return [
1352
+ "# 伏笔池",
1353
+ "",
1354
+ "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |",
1355
+ "| --- | --- | --- | --- | --- | --- | --- |",
1356
+ "",
1357
+ ].join("\n");
1358
+ }
1359
+ async normalizeDraftLengthIfNeeded(params) {
1360
+ const writerCount = countChapterLength(params.chapterContent, params.lengthSpec.countingMode);
1361
+ if (!isOutsideSoftRange(writerCount, params.lengthSpec)) {
1362
+ return {
1363
+ content: params.chapterContent,
1364
+ wordCount: writerCount,
1365
+ applied: false,
1366
+ };
1367
+ }
1368
+ const normalizer = new LengthNormalizerAgent(this.agentCtxFor("length-normalizer", params.bookId));
1369
+ const normalized = await normalizer.normalizeChapter({
1370
+ chapterContent: params.chapterContent,
1371
+ lengthSpec: params.lengthSpec,
1372
+ chapterIntent: params.chapterIntent,
1373
+ });
1374
+ // Safety net: if normalizer output is less than 25% of original, it was too destructive.
1375
+ // Reject and keep original content.
1376
+ if (normalized.finalCount < writerCount * 0.25) {
1377
+ this.logWarn(this.languageFromLengthSpec(params.lengthSpec), {
1378
+ zh: `字数归一化被拒绝:第${params.chapterNumber}章 ${writerCount} -> ${normalized.finalCount}(砍了${Math.round((1 - normalized.finalCount / writerCount) * 100)}%,超过安全阈值)`,
1379
+ en: `Length normalization rejected for chapter ${params.chapterNumber}: ${writerCount} -> ${normalized.finalCount} (cut ${Math.round((1 - normalized.finalCount / writerCount) * 100)}%, exceeds safety threshold)`,
1380
+ });
1381
+ return {
1382
+ content: params.chapterContent,
1383
+ wordCount: writerCount,
1384
+ applied: false,
1385
+ };
1386
+ }
1387
+ this.logInfo(this.languageFromLengthSpec(params.lengthSpec), {
1388
+ zh: `审计前字数归一化:第${params.chapterNumber}章 ${writerCount} -> ${normalized.finalCount}`,
1389
+ en: `Length normalization before audit for chapter ${params.chapterNumber}: ${writerCount} -> ${normalized.finalCount}`,
1390
+ });
1391
+ return {
1392
+ content: normalized.normalizedContent,
1393
+ wordCount: normalized.finalCount,
1394
+ applied: normalized.applied,
1395
+ tokenUsage: normalized.tokenUsage,
1396
+ };
1397
+ }
1398
+ async syncCurrentStateFactHistory(bookId, uptoChapter) {
1399
+ const bookDir = this.state.bookDir(bookId);
1400
+ try {
1401
+ await this.rebuildCurrentStateFactHistory(bookDir, uptoChapter);
1402
+ }
1403
+ catch (error) {
1404
+ if (this.isMemoryIndexUnavailableError(error)) {
1405
+ if (this.canOpenMemoryIndex(bookDir)) {
1406
+ try {
1407
+ await this.rebuildCurrentStateFactHistory(bookDir, uptoChapter);
1408
+ return;
1409
+ }
1410
+ catch (retryError) {
1411
+ error = retryError;
1412
+ }
1413
+ }
1414
+ else {
1415
+ if (!this.memoryIndexFallbackWarned) {
1416
+ this.memoryIndexFallbackWarned = true;
1417
+ this.logWarn(await this.resolveBookLanguageById(bookId), {
1418
+ zh: "当前 Node 运行时不支持 SQLite 记忆索引,继续使用 Markdown 回退方案。",
1419
+ en: "SQLite memory index unavailable on this Node runtime; continuing with markdown fallback.",
1420
+ });
1421
+ await this.logMemoryIndexDebugInfo(bookId, error);
1422
+ }
1423
+ return;
1424
+ }
1425
+ }
1426
+ this.logWarn(await this.resolveBookLanguageById(bookId), {
1427
+ zh: `状态事实同步已跳过:${String(error)}`,
1428
+ en: `State fact sync skipped: ${String(error)}`,
1429
+ });
1430
+ }
1431
+ }
1432
+ async syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, output) {
1433
+ if (output?.runtimeStateDelta || output?.runtimeStateSnapshot) {
1434
+ return;
1435
+ }
1436
+ await rewriteStructuredStateFromMarkdown({
1437
+ bookDir,
1438
+ fallbackChapter: chapterNumber,
1439
+ });
1440
+ }
1441
+ async syncNarrativeMemoryIndex(bookId) {
1442
+ const bookDir = this.state.bookDir(bookId);
1443
+ try {
1444
+ await this.rebuildNarrativeMemoryIndex(bookDir);
1445
+ }
1446
+ catch (error) {
1447
+ if (this.isMemoryIndexUnavailableError(error)) {
1448
+ if (this.canOpenMemoryIndex(bookDir)) {
1449
+ try {
1450
+ await this.rebuildNarrativeMemoryIndex(bookDir);
1451
+ return;
1452
+ }
1453
+ catch (retryError) {
1454
+ error = retryError;
1455
+ }
1456
+ }
1457
+ else {
1458
+ if (!this.memoryIndexFallbackWarned) {
1459
+ this.memoryIndexFallbackWarned = true;
1460
+ this.logWarn(await this.resolveBookLanguageById(bookId), {
1461
+ zh: "当前 Node 运行时不支持 SQLite 记忆索引,继续使用 Markdown 回退方案。",
1462
+ en: "SQLite memory index unavailable on this Node runtime; continuing with markdown fallback.",
1463
+ });
1464
+ await this.logMemoryIndexDebugInfo(bookId, error);
1465
+ }
1466
+ return;
1467
+ }
1468
+ }
1469
+ this.logWarn(await this.resolveBookLanguageById(bookId), {
1470
+ zh: `叙事记忆同步已跳过:${String(error)}`,
1471
+ en: `Narrative memory sync skipped: ${String(error)}`,
1472
+ });
1473
+ }
1474
+ }
1475
+ async rebuildCurrentStateFactHistory(bookDir, uptoChapter) {
1476
+ const memoryDb = await this.withMemoryIndexRetry(async () => {
1477
+ const db = new MemoryDB(bookDir);
1478
+ try {
1479
+ db.resetFacts();
1480
+ const activeFacts = new Map();
1481
+ for (let chapter = 0; chapter <= uptoChapter; chapter++) {
1482
+ const snapshotFacts = await loadSnapshotCurrentStateFacts(bookDir, chapter);
1483
+ if (snapshotFacts.length === 0)
1484
+ continue;
1485
+ const nextFacts = new Map();
1486
+ for (const fact of snapshotFacts) {
1487
+ nextFacts.set(this.factKey(fact), {
1488
+ subject: fact.subject,
1489
+ predicate: fact.predicate,
1490
+ object: fact.object,
1491
+ validFromChapter: chapter,
1492
+ validUntilChapter: null,
1493
+ sourceChapter: chapter,
1494
+ });
1495
+ }
1496
+ for (const [key, previous] of activeFacts.entries()) {
1497
+ const next = nextFacts.get(key);
1498
+ if (!next || next.object !== previous.object) {
1499
+ db.invalidateFact(previous.id, chapter);
1500
+ activeFacts.delete(key);
1501
+ }
1502
+ }
1503
+ for (const [key, fact] of nextFacts.entries()) {
1504
+ if (activeFacts.has(key))
1505
+ continue;
1506
+ const id = db.addFact(fact);
1507
+ activeFacts.set(key, { id, object: fact.object });
1508
+ }
1509
+ }
1510
+ return db;
1511
+ }
1512
+ catch (error) {
1513
+ db.close();
1514
+ throw error;
1515
+ }
1516
+ });
1517
+ try {
1518
+ // No-op: keep the db open only for the duration of the rebuild.
1519
+ }
1520
+ finally {
1521
+ memoryDb.close();
1522
+ }
1523
+ }
1524
+ async rebuildNarrativeMemoryIndex(bookDir) {
1525
+ const memorySeed = await loadNarrativeMemorySeed(bookDir);
1526
+ const memoryDb = await this.withMemoryIndexRetry(() => {
1527
+ const db = new MemoryDB(bookDir);
1528
+ try {
1529
+ db.replaceSummaries(memorySeed.summaries);
1530
+ db.replaceHooks(memorySeed.hooks);
1531
+ return db;
1532
+ }
1533
+ catch (error) {
1534
+ db.close();
1535
+ throw error;
1536
+ }
1537
+ });
1538
+ try {
1539
+ // No-op: keep the db open only for the duration of the rebuild.
1540
+ }
1541
+ finally {
1542
+ memoryDb.close();
1543
+ }
1544
+ }
1545
+ canOpenMemoryIndex(bookDir) {
1546
+ let memoryDb = null;
1547
+ try {
1548
+ memoryDb = new MemoryDB(bookDir);
1549
+ return true;
1550
+ }
1551
+ catch {
1552
+ return false;
1553
+ }
1554
+ finally {
1555
+ memoryDb?.close();
1556
+ }
1557
+ }
1558
+ async logMemoryIndexDebugInfo(bookId, error) {
1559
+ if (process.env.INKOS_DEBUG_SQLITE_MEMORY !== "1") {
1560
+ return;
1561
+ }
1562
+ const code = typeof error === "object" && error !== null && "code" in error
1563
+ ? String(error.code ?? "")
1564
+ : "";
1565
+ const message = error instanceof Error
1566
+ ? error.message
1567
+ : String(error);
1568
+ this.logWarn(await this.resolveBookLanguageById(bookId), {
1569
+ zh: `SQLite 记忆索引调试:node=${process.version}; execArgv=${JSON.stringify(process.execArgv)}; code=${code || "(none)"}; message=${message}`,
1570
+ en: `SQLite memory debug: node=${process.version}; execArgv=${JSON.stringify(process.execArgv)}; code=${code || "(none)"}; message=${message}`,
1571
+ });
1572
+ }
1573
+ async withMemoryIndexRetry(operation) {
1574
+ const retryDelaysMs = [0, 25, 75];
1575
+ let lastError;
1576
+ for (let attempt = 0; attempt < retryDelaysMs.length; attempt += 1) {
1577
+ try {
1578
+ return await operation();
1579
+ }
1580
+ catch (error) {
1581
+ lastError = error;
1582
+ if (!this.isMemoryIndexBusyError(error) || attempt === retryDelaysMs.length - 1) {
1583
+ throw error;
1584
+ }
1585
+ await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt + 1]));
1586
+ }
1587
+ }
1588
+ throw lastError;
1589
+ }
1590
+ isMemoryIndexUnavailableError(error) {
1591
+ if (!error)
1592
+ return false;
1593
+ const code = typeof error === "object" && error !== null && "code" in error
1594
+ ? String(error.code ?? "")
1595
+ : "";
1596
+ const message = error instanceof Error
1597
+ ? error.message
1598
+ : String(error);
1599
+ const normalizedMessage = message.trim();
1600
+ return /^No such built-in module:\s*node:sqlite$/i.test(normalizedMessage)
1601
+ || /^Cannot find module ['"]node:sqlite['"]$/i.test(normalizedMessage)
1602
+ || (code === "ERR_UNKNOWN_BUILTIN_MODULE" && /\bnode:sqlite\b/i.test(normalizedMessage));
1603
+ }
1604
+ isMemoryIndexBusyError(error) {
1605
+ if (!error)
1606
+ return false;
1607
+ const code = typeof error === "object" && error !== null && "code" in error
1608
+ ? String(error.code ?? "")
1609
+ : "";
1610
+ const message = error instanceof Error
1611
+ ? error.message
1612
+ : String(error);
1613
+ return code === "SQLITE_BUSY"
1614
+ || code === "SQLITE_LOCKED"
1615
+ || /\bSQLITE_BUSY\b/i.test(message)
1616
+ || /\bSQLITE_LOCKED\b/i.test(message)
1617
+ || /database is locked/i.test(message)
1618
+ || /database is busy/i.test(message);
1619
+ }
1620
+ factKey(fact) {
1621
+ return `${fact.subject}::${fact.predicate}`;
1622
+ }
1623
+ buildLengthWarnings(chapterNumber, finalCount, lengthSpec) {
1624
+ if (!isOutsideHardRange(finalCount, lengthSpec)) {
1625
+ return [];
1626
+ }
1627
+ return [
1628
+ this.localize(this.languageFromLengthSpec(lengthSpec), {
1629
+ zh: `第${chapterNumber}章经过一次字数归一化后仍超出硬区间(${lengthSpec.hardMin}-${lengthSpec.hardMax},实际 ${finalCount})。`,
1630
+ en: `Chapter ${chapterNumber} remains outside hard range (${lengthSpec.hardMin}-${lengthSpec.hardMax}, actual ${finalCount}) after a single normalization pass.`,
1631
+ }),
1632
+ ];
1633
+ }
1634
+ buildLengthTelemetry(params) {
1635
+ return {
1636
+ target: params.lengthSpec.target,
1637
+ softMin: params.lengthSpec.softMin,
1638
+ softMax: params.lengthSpec.softMax,
1639
+ hardMin: params.lengthSpec.hardMin,
1640
+ hardMax: params.lengthSpec.hardMax,
1641
+ countingMode: params.lengthSpec.countingMode,
1642
+ writerCount: params.writerCount,
1643
+ postWriterNormalizeCount: params.postWriterNormalizeCount,
1644
+ postReviseCount: params.postReviseCount,
1645
+ finalCount: params.finalCount,
1646
+ normalizeApplied: params.normalizeApplied,
1647
+ lengthWarning: params.lengthWarning,
1648
+ };
1649
+ }
1650
+ logLengthWarnings(lengthWarnings) {
1651
+ for (const warning of lengthWarnings) {
1652
+ this.config.logger?.warn(warning);
1653
+ }
1654
+ }
1655
+ restoreLostAuditIssues(previous, next) {
1656
+ if (next.passed || next.issues.length > 0 || previous.issues.length === 0) {
1657
+ return next;
1658
+ }
1659
+ return {
1660
+ ...next,
1661
+ issues: previous.issues,
1662
+ summary: next.summary || previous.summary,
1663
+ };
1664
+ }
1665
+ restoreActionableAuditIfLost(previous, next) {
1666
+ const auditResult = this.restoreLostAuditIssues(previous.auditResult, next.auditResult);
1667
+ if (auditResult === next.auditResult) {
1668
+ return next;
1669
+ }
1670
+ return {
1671
+ ...next,
1672
+ auditResult,
1673
+ blockingCount: auditResult.issues.filter((issue) => issue.severity === "warning" || issue.severity === "critical").length,
1674
+ criticalCount: auditResult.issues.filter((issue) => issue.severity === "critical").length,
1675
+ };
1676
+ }
1677
+ async evaluateMergedAudit(params) {
1678
+ const llmAudit = await params.auditor.auditChapter(params.bookDir, params.chapterContent, params.chapterNumber, params.book.genre, params.auditOptions);
1679
+ const aiTells = analyzeAITells(params.chapterContent);
1680
+ const sensitiveResult = analyzeSensitiveWords(params.chapterContent);
1681
+ const longSpanFatigue = await analyzeLongSpanFatigue({
1682
+ bookDir: params.bookDir,
1683
+ chapterNumber: params.chapterNumber,
1684
+ chapterContent: params.chapterContent,
1685
+ language: params.language,
1686
+ });
1687
+ const hasBlockedWords = sensitiveResult.found.some((f) => f.severity === "block");
1688
+ const issues = [
1689
+ ...llmAudit.issues,
1690
+ ...aiTells.issues,
1691
+ ...sensitiveResult.issues,
1692
+ ...longSpanFatigue.issues,
1693
+ ];
1694
+ return {
1695
+ auditResult: {
1696
+ passed: hasBlockedWords ? false : llmAudit.passed,
1697
+ issues,
1698
+ summary: llmAudit.summary,
1699
+ tokenUsage: llmAudit.tokenUsage,
1700
+ },
1701
+ aiTellCount: aiTells.issues.length,
1702
+ blockingCount: issues.filter((issue) => issue.severity === "warning" || issue.severity === "critical").length,
1703
+ criticalCount: issues.filter((issue) => issue.severity === "critical").length,
1704
+ };
1705
+ }
1706
+ async markBookActiveIfNeeded(bookId) {
1707
+ const book = await this.state.loadBookConfig(bookId);
1708
+ if (book.status !== "outlining")
1709
+ return;
1710
+ await this.state.saveBookConfig(bookId, {
1711
+ ...book,
1712
+ status: "active",
1713
+ updatedAt: new Date().toISOString(),
1714
+ });
1715
+ }
1716
+ async createGovernedArtifacts(book, bookDir, chapterNumber, externalContext, options) {
1717
+ const plan = await this.resolveGovernedPlan(book, bookDir, chapterNumber, externalContext, options);
1718
+ const composer = new ComposerAgent(this.agentCtxFor("composer", book.id));
1719
+ const composed = await composer.composeChapter({
1720
+ book,
1721
+ bookDir,
1722
+ chapterNumber,
1723
+ plan,
1724
+ });
1725
+ return { plan, composed };
1726
+ }
1727
+ async resolveGovernedPlan(book, bookDir, chapterNumber, externalContext, options) {
1728
+ if (options?.reuseExistingIntentWhenContextMissing &&
1729
+ (!externalContext || externalContext.trim().length === 0)) {
1730
+ const persisted = await this.loadPersistedPlan(bookDir, chapterNumber);
1731
+ if (persisted)
1732
+ return persisted;
1733
+ }
1734
+ const planner = new PlannerAgent(this.agentCtxFor("planner", book.id));
1735
+ return planner.planChapter({
1736
+ book,
1737
+ bookDir,
1738
+ chapterNumber,
1739
+ externalContext,
1740
+ });
1741
+ }
1742
+ async loadPersistedPlan(bookDir, chapterNumber) {
1743
+ const runtimePath = join(bookDir, "story", "runtime", `chapter-${String(chapterNumber).padStart(4, "0")}.intent.md`);
1744
+ try {
1745
+ const intentMarkdown = await readFile(runtimePath, "utf-8");
1746
+ const sections = this.parseIntentSections(intentMarkdown);
1747
+ const goal = this.readIntentScalar(sections, "Goal");
1748
+ if (!goal || this.isInvalidPersistedIntentScalar(goal))
1749
+ return null;
1750
+ const outlineNode = this.readIntentScalar(sections, "Outline Node");
1751
+ if (outlineNode && outlineNode !== "(not found)" && this.isInvalidPersistedIntentScalar(outlineNode)) {
1752
+ return null;
1753
+ }
1754
+ const conflicts = this.readIntentList(sections, "Conflicts")
1755
+ .map((line) => {
1756
+ const separator = line.indexOf(":");
1757
+ if (separator < 0)
1758
+ return null;
1759
+ const type = line.slice(0, separator).trim();
1760
+ const resolution = line.slice(separator + 1).trim();
1761
+ if (!type || !resolution)
1762
+ return null;
1763
+ return { type, resolution };
1764
+ })
1765
+ .filter((conflict) => conflict !== null);
1766
+ return {
1767
+ intent: ChapterIntentSchema.parse({
1768
+ chapter: chapterNumber,
1769
+ goal,
1770
+ outlineNode: outlineNode && outlineNode !== "(not found)" ? outlineNode : undefined,
1771
+ mustKeep: this.readIntentList(sections, "Must Keep"),
1772
+ mustAvoid: this.readIntentList(sections, "Must Avoid"),
1773
+ styleEmphasis: this.readIntentList(sections, "Style Emphasis"),
1774
+ conflicts,
1775
+ }),
1776
+ intentMarkdown,
1777
+ plannerInputs: [runtimePath],
1778
+ runtimePath,
1779
+ };
1780
+ }
1781
+ catch {
1782
+ return null;
1783
+ }
1784
+ }
1785
+ parseIntentSections(markdown) {
1786
+ const sections = new Map();
1787
+ let current = null;
1788
+ for (const line of markdown.split("\n")) {
1789
+ if (line.startsWith("## ")) {
1790
+ current = line.slice(3).trim();
1791
+ sections.set(current, []);
1792
+ continue;
1793
+ }
1794
+ if (!current)
1795
+ continue;
1796
+ sections.get(current)?.push(line);
1797
+ }
1798
+ return sections;
1799
+ }
1800
+ readIntentScalar(sections, name) {
1801
+ const lines = sections.get(name) ?? [];
1802
+ const value = lines.map((line) => line.trim()).find((line) => line.length > 0);
1803
+ return value && value !== "- none" ? value : undefined;
1804
+ }
1805
+ readIntentList(sections, name) {
1806
+ return (sections.get(name) ?? [])
1807
+ .map((line) => line.trim())
1808
+ .filter((line) => line.startsWith("-") && line !== "- none")
1809
+ .map((line) => line.replace(/^-\s*/, ""));
1810
+ }
1811
+ isInvalidPersistedIntentScalar(value) {
1812
+ const normalized = value.trim();
1813
+ if (!normalized)
1814
+ return true;
1815
+ if (/^[*_`~::|.-]+$/.test(normalized))
1816
+ return true;
1817
+ return (/^\((describe|briefly describe|write)\b[\s\S]*\)$/i.test(normalized)
1818
+ || /^((?:在这里描述|描述|填写|写下)[\s\S]*)$/u.test(normalized));
1819
+ }
1820
+ relativeToBookDir(bookDir, absolutePath) {
1821
+ const prefix = `${bookDir}/`;
1822
+ return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
1823
+ }
868
1824
  async emitWebhook(event, bookId, chapterNumber, data) {
869
1825
  if (!this.config.notifyChannels || this.config.notifyChannels.length === 0)
870
1826
  return;