@actalk/inkos-core 1.0.2 → 1.1.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.
- package/dist/agents/architect.d.ts +4 -2
- package/dist/agents/architect.d.ts.map +1 -1
- package/dist/agents/architect.js +59 -13
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/chapter-analyzer.js +2 -2
- package/dist/agents/composer.d.ts +8 -0
- package/dist/agents/composer.d.ts.map +1 -1
- package/dist/agents/composer.js +170 -5
- package/dist/agents/composer.js.map +1 -1
- package/dist/agents/continuity.js +8 -8
- package/dist/agents/continuity.js.map +1 -1
- package/dist/agents/fanfic-canon-importer.d.ts +1 -0
- package/dist/agents/fanfic-canon-importer.d.ts.map +1 -1
- package/dist/agents/fanfic-canon-importer.js +19 -1
- package/dist/agents/fanfic-canon-importer.js.map +1 -1
- package/dist/agents/foundation-reviewer.d.ts +29 -0
- package/dist/agents/foundation-reviewer.d.ts.map +1 -0
- package/dist/agents/foundation-reviewer.js +153 -0
- package/dist/agents/foundation-reviewer.js.map +1 -0
- package/dist/agents/planner.d.ts +15 -0
- package/dist/agents/planner.d.ts.map +1 -1
- package/dist/agents/planner.js +247 -36
- package/dist/agents/planner.js.map +1 -1
- package/dist/agents/post-write-validator.d.ts +3 -1
- package/dist/agents/post-write-validator.d.ts.map +1 -1
- package/dist/agents/post-write-validator.js +165 -12
- package/dist/agents/post-write-validator.js.map +1 -1
- package/dist/agents/settler-prompts.d.ts +1 -0
- package/dist/agents/settler-prompts.d.ts.map +1 -1
- package/dist/agents/settler-prompts.js +8 -1
- package/dist/agents/settler-prompts.js.map +1 -1
- package/dist/agents/writer-prompts.js +10 -4
- package/dist/agents/writer-prompts.js.map +1 -1
- package/dist/agents/writer.d.ts +15 -0
- package/dist/agents/writer.d.ts.map +1 -1
- package/dist/agents/writer.js +181 -15
- package/dist/agents/writer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +25 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/models/chapter.d.ts +4 -4
- package/dist/models/chapter.d.ts.map +1 -1
- package/dist/models/chapter.js +1 -0
- package/dist/models/chapter.js.map +1 -1
- package/dist/models/input-governance.d.ts +165 -0
- package/dist/models/input-governance.d.ts.map +1 -1
- package/dist/models/input-governance.js +34 -0
- package/dist/models/input-governance.js.map +1 -1
- package/dist/models/project.d.ts +8 -0
- package/dist/models/project.d.ts.map +1 -1
- package/dist/models/project.js +1 -0
- package/dist/models/project.js.map +1 -1
- package/dist/models/runtime-state.d.ts +30 -0
- package/dist/models/runtime-state.d.ts.map +1 -1
- package/dist/models/runtime-state.js +9 -0
- package/dist/models/runtime-state.js.map +1 -1
- package/dist/pipeline/chapter-persistence.d.ts +33 -0
- package/dist/pipeline/chapter-persistence.d.ts.map +1 -0
- package/dist/pipeline/chapter-persistence.js +35 -0
- package/dist/pipeline/chapter-persistence.js.map +1 -0
- package/dist/pipeline/chapter-review-cycle.d.ts +79 -0
- package/dist/pipeline/chapter-review-cycle.d.ts.map +1 -0
- package/dist/pipeline/chapter-review-cycle.js +97 -0
- package/dist/pipeline/chapter-review-cycle.js.map +1 -0
- package/dist/pipeline/chapter-state-recovery.d.ts +59 -0
- package/dist/pipeline/chapter-state-recovery.d.ts.map +1 -0
- package/dist/pipeline/chapter-state-recovery.js +132 -0
- package/dist/pipeline/chapter-state-recovery.js.map +1 -0
- package/dist/pipeline/chapter-truth-validation.d.ts +41 -0
- package/dist/pipeline/chapter-truth-validation.d.ts.map +1 -0
- package/dist/pipeline/chapter-truth-validation.js +67 -0
- package/dist/pipeline/chapter-truth-validation.js.map +1 -0
- package/dist/pipeline/persisted-governed-plan.d.ts +4 -0
- package/dist/pipeline/persisted-governed-plan.d.ts.map +1 -0
- package/dist/pipeline/persisted-governed-plan.js +85 -0
- package/dist/pipeline/persisted-governed-plan.js.map +1 -0
- package/dist/pipeline/runner.d.ts +10 -7
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +459 -283
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/state/manager.d.ts +8 -0
- package/dist/state/manager.d.ts.map +1 -1
- package/dist/state/manager.js +106 -9
- package/dist/state/manager.js.map +1 -1
- package/dist/state/memory-db.d.ts +2 -0
- package/dist/state/memory-db.d.ts.map +1 -1
- package/dist/state/memory-db.js +13 -2
- package/dist/state/memory-db.js.map +1 -1
- package/dist/state/runtime-state-store.d.ts.map +1 -1
- package/dist/state/runtime-state-store.js +1 -0
- package/dist/state/runtime-state-store.js.map +1 -1
- package/dist/state/state-bootstrap.d.ts +2 -5
- package/dist/state/state-bootstrap.d.ts.map +1 -1
- package/dist/state/state-bootstrap.js +61 -141
- package/dist/state/state-bootstrap.js.map +1 -1
- package/dist/state/state-projections.d.ts.map +1 -1
- package/dist/state/state-projections.js +6 -4
- package/dist/state/state-projections.js.map +1 -1
- package/dist/state/state-reducer.d.ts.map +1 -1
- package/dist/state/state-reducer.js +6 -0
- package/dist/state/state-reducer.js.map +1 -1
- package/dist/utils/cadence-policy.d.ts +36 -0
- package/dist/utils/cadence-policy.d.ts.map +1 -0
- package/dist/utils/cadence-policy.js +38 -0
- package/dist/utils/cadence-policy.js.map +1 -0
- package/dist/utils/chapter-cadence.d.ts +34 -0
- package/dist/utils/chapter-cadence.d.ts.map +1 -0
- package/dist/utils/chapter-cadence.js +142 -0
- package/dist/utils/chapter-cadence.js.map +1 -0
- package/dist/utils/context-filter.d.ts +2 -2
- package/dist/utils/context-filter.d.ts.map +1 -1
- package/dist/utils/context-filter.js +3 -2
- package/dist/utils/context-filter.js.map +1 -1
- package/dist/utils/governed-context.d.ts +4 -0
- package/dist/utils/governed-context.d.ts.map +1 -1
- package/dist/utils/governed-context.js +20 -0
- package/dist/utils/governed-context.js.map +1 -1
- package/dist/utils/governed-working-set.d.ts.map +1 -1
- package/dist/utils/governed-working-set.js +2 -1
- package/dist/utils/governed-working-set.js.map +1 -1
- package/dist/utils/hook-agenda.d.ts +21 -0
- package/dist/utils/hook-agenda.d.ts.map +1 -0
- package/dist/utils/hook-agenda.js +95 -0
- package/dist/utils/hook-agenda.js.map +1 -0
- package/dist/utils/hook-arbiter.d.ts.map +1 -1
- package/dist/utils/hook-arbiter.js +7 -0
- package/dist/utils/hook-arbiter.js.map +1 -1
- package/dist/utils/hook-governance.d.ts +2 -0
- package/dist/utils/hook-governance.d.ts.map +1 -1
- package/dist/utils/hook-governance.js +19 -3
- package/dist/utils/hook-governance.js.map +1 -1
- package/dist/utils/hook-health.d.ts +1 -0
- package/dist/utils/hook-health.d.ts.map +1 -1
- package/dist/utils/hook-health.js +85 -26
- package/dist/utils/hook-health.js.map +1 -1
- package/dist/utils/hook-lifecycle.d.ts +34 -0
- package/dist/utils/hook-lifecycle.d.ts.map +1 -0
- package/dist/utils/hook-lifecycle.js +125 -0
- package/dist/utils/hook-lifecycle.js.map +1 -0
- package/dist/utils/hook-policy.d.ts +74 -0
- package/dist/utils/hook-policy.d.ts.map +1 -0
- package/dist/utils/hook-policy.js +126 -0
- package/dist/utils/hook-policy.js.map +1 -0
- package/dist/utils/long-span-fatigue.d.ts.map +1 -1
- package/dist/utils/long-span-fatigue.js +75 -28
- package/dist/utils/long-span-fatigue.js.map +1 -1
- package/dist/utils/memory-retrieval.d.ts +2 -16
- package/dist/utils/memory-retrieval.d.ts.map +1 -1
- package/dist/utils/memory-retrieval.js +14 -269
- package/dist/utils/memory-retrieval.js.map +1 -1
- package/dist/utils/story-markdown.d.ts +13 -0
- package/dist/utils/story-markdown.d.ts.map +1 -0
- package/dist/utils/story-markdown.js +218 -0
- package/dist/utils/story-markdown.js.map +1 -0
- package/package.json +1 -1
package/dist/pipeline/runner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chatCompletion, createLLMClient } from "../llm/provider.js";
|
|
2
2
|
import { ArchitectAgent } from "../agents/architect.js";
|
|
3
|
+
import { FoundationReviewerAgent } from "../agents/foundation-reviewer.js";
|
|
3
4
|
import { PlannerAgent } from "../agents/planner.js";
|
|
4
5
|
import { ComposerAgent } from "../agents/composer.js";
|
|
5
6
|
import { WriterAgent } from "../agents/writer.js";
|
|
@@ -15,13 +16,28 @@ import { analyzeSensitiveWords } from "../agents/sensitive-words.js";
|
|
|
15
16
|
import { StateManager } from "../state/manager.js";
|
|
16
17
|
import { MemoryDB } from "../state/memory-db.js";
|
|
17
18
|
import { dispatchNotification, dispatchWebhookEvent } from "../notify/dispatcher.js";
|
|
18
|
-
import { ChapterIntentSchema } from "../models/input-governance.js";
|
|
19
19
|
import { buildLengthSpec, countChapterLength, formatLengthCount, isOutsideHardRange, isOutsideSoftRange, resolveLengthCountingMode } from "../utils/length-metrics.js";
|
|
20
20
|
import { analyzeLongSpanFatigue } from "../utils/long-span-fatigue.js";
|
|
21
21
|
import { loadNarrativeMemorySeed, loadSnapshotCurrentStateFacts } from "../state/runtime-state-store.js";
|
|
22
22
|
import { rewriteStructuredStateFromMarkdown } from "../state/state-bootstrap.js";
|
|
23
23
|
import { readFile, readdir, writeFile, mkdir, rename, rm, stat } from "node:fs/promises";
|
|
24
|
-
import { join
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { parseStateDegradedReviewNote, resolveStateDegradedBaseStatus, retrySettlementAfterValidationFailure, } from "./chapter-state-recovery.js";
|
|
26
|
+
import { persistChapterArtifacts } from "./chapter-persistence.js";
|
|
27
|
+
import { runChapterReviewCycle } from "./chapter-review-cycle.js";
|
|
28
|
+
import { validateChapterTruthPersistence } from "./chapter-truth-validation.js";
|
|
29
|
+
import { loadPersistedPlan, relativeToBookDir } from "./persisted-governed-plan.js";
|
|
30
|
+
const SEQUENCE_LEVEL_CATEGORIES = new Set([
|
|
31
|
+
"Pacing Monotony", "节奏单调",
|
|
32
|
+
"Mood Monotony", "情绪单调",
|
|
33
|
+
"Title Collapse", "标题重复",
|
|
34
|
+
"Title Clustering", "标题聚集",
|
|
35
|
+
"Opening Pattern Repetition", "开头同构",
|
|
36
|
+
"Ending Pattern Repetition", "结尾同构",
|
|
37
|
+
]);
|
|
38
|
+
function isSequenceLevelCategory(category) {
|
|
39
|
+
return SEQUENCE_LEVEL_CATEGORIES.has(category);
|
|
40
|
+
}
|
|
25
41
|
export class PipelineRunner {
|
|
26
42
|
state;
|
|
27
43
|
config;
|
|
@@ -67,6 +83,80 @@ export class PipelineRunner {
|
|
|
67
83
|
logWarn(language, message) {
|
|
68
84
|
this.config.logger?.warn(this.localize(language, message));
|
|
69
85
|
}
|
|
86
|
+
async tryGenerateStyleGuide(bookId, referenceText, sourceName, language) {
|
|
87
|
+
try {
|
|
88
|
+
await this.generateStyleGuide(bookId, referenceText, sourceName);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const resolvedLanguage = language ?? await this.resolveBookLanguageById(bookId);
|
|
92
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
93
|
+
this.logWarn(resolvedLanguage, {
|
|
94
|
+
zh: `风格指纹提取失败,已跳过:${detail}`,
|
|
95
|
+
en: `Style fingerprint extraction failed and was skipped: ${detail}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async generateAndReviewFoundation(params) {
|
|
100
|
+
const maxRetries = params.maxRetries ?? 2;
|
|
101
|
+
let foundation = await params.generate();
|
|
102
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
103
|
+
this.logStage(params.stageLanguage, {
|
|
104
|
+
zh: `审核基础设定(第${attempt + 1}轮)`,
|
|
105
|
+
en: `reviewing foundation (round ${attempt + 1})`,
|
|
106
|
+
});
|
|
107
|
+
const review = await params.reviewer.review({
|
|
108
|
+
foundation,
|
|
109
|
+
mode: params.mode,
|
|
110
|
+
sourceCanon: params.sourceCanon,
|
|
111
|
+
styleGuide: params.styleGuide,
|
|
112
|
+
language: params.language,
|
|
113
|
+
});
|
|
114
|
+
this.config.logger?.info(`Foundation review: ${review.totalScore}/100 ${review.passed ? "PASSED" : "REJECTED"}`);
|
|
115
|
+
for (const dim of review.dimensions) {
|
|
116
|
+
this.config.logger?.info(` [${dim.score}] ${dim.name.slice(0, 40)}`);
|
|
117
|
+
}
|
|
118
|
+
if (review.passed) {
|
|
119
|
+
return foundation;
|
|
120
|
+
}
|
|
121
|
+
this.logWarn(params.stageLanguage, {
|
|
122
|
+
zh: `基础设定未通过审核(${review.totalScore}分),正在重新生成...`,
|
|
123
|
+
en: `Foundation rejected (${review.totalScore}/100), regenerating...`,
|
|
124
|
+
});
|
|
125
|
+
foundation = await params.generate(this.buildFoundationReviewFeedback(review, params.language));
|
|
126
|
+
}
|
|
127
|
+
// Final review
|
|
128
|
+
const finalReview = await params.reviewer.review({
|
|
129
|
+
foundation,
|
|
130
|
+
mode: params.mode,
|
|
131
|
+
sourceCanon: params.sourceCanon,
|
|
132
|
+
styleGuide: params.styleGuide,
|
|
133
|
+
language: params.language,
|
|
134
|
+
});
|
|
135
|
+
this.config.logger?.info(`Foundation final review: ${finalReview.totalScore}/100 ${finalReview.passed ? "PASSED" : "ACCEPTED (max retries)"}`);
|
|
136
|
+
return foundation;
|
|
137
|
+
}
|
|
138
|
+
buildFoundationReviewFeedback(review, language) {
|
|
139
|
+
const dimensionLines = review.dimensions
|
|
140
|
+
.map((dimension) => (language === "en"
|
|
141
|
+
? `- ${dimension.name} [${dimension.score}]: ${dimension.feedback}`
|
|
142
|
+
: `- ${dimension.name}(${dimension.score}分):${dimension.feedback}`))
|
|
143
|
+
.join("\n");
|
|
144
|
+
return language === "en"
|
|
145
|
+
? [
|
|
146
|
+
"## Overall Feedback",
|
|
147
|
+
review.overallFeedback,
|
|
148
|
+
"",
|
|
149
|
+
"## Dimension Notes",
|
|
150
|
+
dimensionLines || "- none",
|
|
151
|
+
].join("\n")
|
|
152
|
+
: [
|
|
153
|
+
"## 总评",
|
|
154
|
+
review.overallFeedback,
|
|
155
|
+
"",
|
|
156
|
+
"## 分项问题",
|
|
157
|
+
dimensionLines || "- 无",
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
70
160
|
agentCtx(bookId) {
|
|
71
161
|
return {
|
|
72
162
|
client: this.config.client,
|
|
@@ -161,7 +251,15 @@ export class PipelineRunner {
|
|
|
161
251
|
const stageLanguage = await this.resolveBookLanguage(book);
|
|
162
252
|
this.logStage(stageLanguage, { zh: "生成基础设定", en: "generating foundation" });
|
|
163
253
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
164
|
-
const
|
|
254
|
+
const reviewer = new FoundationReviewerAgent(this.agentCtxFor("foundation-reviewer", book.id));
|
|
255
|
+
const resolvedLanguage = (book.language ?? gp.language) === "en" ? "en" : "zh";
|
|
256
|
+
const foundation = await this.generateAndReviewFoundation({
|
|
257
|
+
generate: (reviewFeedback) => architect.generateFoundation(book, this.config.externalContext, reviewFeedback),
|
|
258
|
+
reviewer,
|
|
259
|
+
mode: "original",
|
|
260
|
+
language: resolvedLanguage,
|
|
261
|
+
stageLanguage,
|
|
262
|
+
});
|
|
165
263
|
try {
|
|
166
264
|
this.logStage(stageLanguage, { zh: "保存书籍配置", en: "saving book config" });
|
|
167
265
|
await this.state.saveBookConfigAt(stagingBookDir, book);
|
|
@@ -205,16 +303,30 @@ export class PipelineRunner {
|
|
|
205
303
|
// Step 1: Import source material → fanfic_canon.md
|
|
206
304
|
this.logStage(stageLanguage, { zh: "导入同人正典", en: "importing fanfic canon" });
|
|
207
305
|
const fanficCanon = await this.importFanficCanon(book.id, sourceText, sourceName, fanficMode);
|
|
208
|
-
// Step 2: Generate foundation
|
|
306
|
+
// Step 2: Generate foundation with review loop
|
|
209
307
|
const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
|
|
308
|
+
const reviewer = new FoundationReviewerAgent(this.agentCtxFor("foundation-reviewer", book.id));
|
|
210
309
|
this.logStage(stageLanguage, { zh: "生成同人基础设定", en: "generating fanfic foundation" });
|
|
211
310
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
212
|
-
const
|
|
311
|
+
const resolvedLanguage = (book.language ?? gp.language) === "en" ? "en" : "zh";
|
|
312
|
+
const foundation = await this.generateAndReviewFoundation({
|
|
313
|
+
generate: (reviewFeedback) => architect.generateFanficFoundation(book, fanficCanon, fanficMode, reviewFeedback),
|
|
314
|
+
reviewer,
|
|
315
|
+
mode: "fanfic",
|
|
316
|
+
sourceCanon: fanficCanon,
|
|
317
|
+
language: resolvedLanguage,
|
|
318
|
+
stageLanguage,
|
|
319
|
+
});
|
|
213
320
|
this.logStage(stageLanguage, { zh: "写入基础设定文件", en: "writing foundation files" });
|
|
214
321
|
await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, book.language ?? gp.language);
|
|
215
322
|
this.logStage(stageLanguage, { zh: "初始化控制文档", en: "initializing control documents" });
|
|
216
323
|
await this.state.ensureControlDocuments(book.id, this.config.externalContext);
|
|
217
|
-
// Step 3:
|
|
324
|
+
// Step 3: Generate style guide from source material
|
|
325
|
+
if (sourceText.length >= 500) {
|
|
326
|
+
this.logStage(stageLanguage, { zh: "提取原作风格指纹", en: "extracting source style fingerprint" });
|
|
327
|
+
await this.tryGenerateStyleGuide(book.id, sourceText, sourceName, stageLanguage);
|
|
328
|
+
}
|
|
329
|
+
// Step 4: Initialize chapters directory + snapshot
|
|
218
330
|
this.logStage(stageLanguage, { zh: "创建初始快照", en: "creating initial snapshot" });
|
|
219
331
|
await mkdir(join(bookDir, "chapters"), { recursive: true });
|
|
220
332
|
await this.state.saveChapterIndex(book.id, []);
|
|
@@ -341,7 +453,7 @@ export class PipelineRunner {
|
|
|
341
453
|
return {
|
|
342
454
|
bookId,
|
|
343
455
|
chapterNumber,
|
|
344
|
-
intentPath:
|
|
456
|
+
intentPath: relativeToBookDir(bookDir, plan.runtimePath),
|
|
345
457
|
goal: plan.intent.goal,
|
|
346
458
|
conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
|
|
347
459
|
};
|
|
@@ -357,12 +469,12 @@ export class PipelineRunner {
|
|
|
357
469
|
return {
|
|
358
470
|
bookId,
|
|
359
471
|
chapterNumber,
|
|
360
|
-
intentPath:
|
|
472
|
+
intentPath: relativeToBookDir(bookDir, plan.runtimePath),
|
|
361
473
|
goal: plan.intent.goal,
|
|
362
474
|
conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
|
|
363
|
-
contextPath:
|
|
364
|
-
ruleStackPath:
|
|
365
|
-
tracePath:
|
|
475
|
+
contextPath: relativeToBookDir(bookDir, composed.contextPath),
|
|
476
|
+
ruleStackPath: relativeToBookDir(bookDir, composed.ruleStackPath),
|
|
477
|
+
tracePath: relativeToBookDir(bookDir, composed.tracePath),
|
|
366
478
|
};
|
|
367
479
|
}
|
|
368
480
|
/** Audit the latest (or specified) chapter. Read-only, no lock needed. */
|
|
@@ -401,6 +513,15 @@ export class PipelineRunner {
|
|
|
401
513
|
}
|
|
402
514
|
: ch);
|
|
403
515
|
await this.state.saveChapterIndex(bookId, updated);
|
|
516
|
+
const latestChapter = index.length > 0 ? Math.max(...index.map((chapter) => chapter.number)) : targetChapter;
|
|
517
|
+
if (targetChapter === latestChapter) {
|
|
518
|
+
await this.persistAuditDriftGuidance({
|
|
519
|
+
bookDir,
|
|
520
|
+
chapterNumber: targetChapter,
|
|
521
|
+
issues: result.issues.filter((issue) => issue.severity === "critical" || issue.severity === "warning"),
|
|
522
|
+
language,
|
|
523
|
+
}).catch(() => undefined);
|
|
524
|
+
}
|
|
404
525
|
await this.emitWebhook(result.passed ? "audit-passed" : "audit-failed", bookId, targetChapter, { summary: result.summary, issueCount: result.issues.length });
|
|
405
526
|
return { ...result, chapterNumber: targetChapter };
|
|
406
527
|
}
|
|
@@ -588,6 +709,15 @@ export class PipelineRunner {
|
|
|
588
709
|
}
|
|
589
710
|
: ch);
|
|
590
711
|
await this.state.saveChapterIndex(bookId, updatedIndex);
|
|
712
|
+
const latestChapter = index.length > 0 ? Math.max(...index.map((chapter) => chapter.number)) : targetChapter;
|
|
713
|
+
if (targetChapter === latestChapter) {
|
|
714
|
+
await this.persistAuditDriftGuidance({
|
|
715
|
+
bookDir,
|
|
716
|
+
chapterNumber: targetChapter,
|
|
717
|
+
issues: effectivePostRevision.auditResult.issues.filter((issue) => issue.severity === "critical" || issue.severity === "warning"),
|
|
718
|
+
language,
|
|
719
|
+
}).catch(() => undefined);
|
|
720
|
+
}
|
|
591
721
|
// Re-snapshot
|
|
592
722
|
this.logStage(stageLanguage, {
|
|
593
723
|
zh: `更新第${targetChapter}章索引与快照`,
|
|
@@ -666,10 +796,20 @@ export class PipelineRunner {
|
|
|
666
796
|
await releaseLock();
|
|
667
797
|
}
|
|
668
798
|
}
|
|
799
|
+
async repairChapterState(bookId, chapterNumber) {
|
|
800
|
+
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
801
|
+
try {
|
|
802
|
+
return await this._repairChapterStateLocked(bookId, chapterNumber);
|
|
803
|
+
}
|
|
804
|
+
finally {
|
|
805
|
+
await releaseLock();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
669
808
|
async _writeNextChapterLocked(bookId, wordCount, temperatureOverride) {
|
|
670
809
|
await this.state.ensureControlDocuments(bookId);
|
|
671
810
|
const book = await this.state.loadBookConfig(bookId);
|
|
672
811
|
const bookDir = this.state.bookDir(bookId);
|
|
812
|
+
await this.assertNoPendingStateRepair(bookId);
|
|
673
813
|
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
674
814
|
const stageLanguage = await this.resolveBookLanguage(book);
|
|
675
815
|
this.logStage(stageLanguage, { zh: "准备章节输入", en: "preparing chapter inputs" });
|
|
@@ -699,120 +839,49 @@ export class PipelineRunner {
|
|
|
699
839
|
const writerCount = countChapterLength(output.content, lengthSpec.countingMode);
|
|
700
840
|
// Token usage accumulator
|
|
701
841
|
let totalUsage = output.tokenUsage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
let finalWordCount = output.wordCount;
|
|
707
|
-
let revised = false;
|
|
708
|
-
if (output.postWriteErrors.length > 0) {
|
|
709
|
-
this.logWarn(pipelineLang, {
|
|
710
|
-
zh: `检测到 ${output.postWriteErrors.length} 个后写错误,审计前触发 spot-fix 修补`,
|
|
711
|
-
en: `${output.postWriteErrors.length} post-write errors detected, triggering spot-fix before audit`,
|
|
712
|
-
});
|
|
713
|
-
const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
|
|
714
|
-
const spotFixIssues = output.postWriteErrors.map((v) => ({
|
|
715
|
-
severity: "critical",
|
|
716
|
-
category: v.rule,
|
|
717
|
-
description: v.description,
|
|
718
|
-
suggestion: v.suggestion,
|
|
719
|
-
}));
|
|
720
|
-
const fixResult = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, spotFixIssues, "spot-fix", book.genre, {
|
|
721
|
-
...reducedControlInput,
|
|
722
|
-
lengthSpec,
|
|
723
|
-
});
|
|
724
|
-
totalUsage = PipelineRunner.addUsage(totalUsage, fixResult.tokenUsage);
|
|
725
|
-
if (fixResult.revisedContent.length > 0) {
|
|
726
|
-
finalContent = fixResult.revisedContent;
|
|
727
|
-
finalWordCount = fixResult.wordCount;
|
|
728
|
-
revised = true;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
const normalizedBeforeAudit = await this.normalizeDraftLengthIfNeeded({
|
|
732
|
-
bookId,
|
|
842
|
+
const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
|
|
843
|
+
const reviewResult = await runChapterReviewCycle({
|
|
844
|
+
book: { genre: book.genre },
|
|
845
|
+
bookDir,
|
|
733
846
|
chapterNumber,
|
|
734
|
-
|
|
847
|
+
initialOutput: output,
|
|
848
|
+
reducedControlInput,
|
|
735
849
|
lengthSpec,
|
|
736
|
-
|
|
850
|
+
initialUsage: totalUsage,
|
|
851
|
+
createReviser: () => new ReviserAgent(this.agentCtxFor("reviser", bookId)),
|
|
852
|
+
auditor,
|
|
853
|
+
normalizeDraftLengthIfNeeded: (chapterContent) => this.normalizeDraftLengthIfNeeded({
|
|
854
|
+
bookId,
|
|
855
|
+
chapterNumber,
|
|
856
|
+
chapterContent,
|
|
857
|
+
lengthSpec,
|
|
858
|
+
chapterIntent: writeInput.chapterIntent,
|
|
859
|
+
}),
|
|
860
|
+
assertChapterContentNotEmpty: (content, stage) => this.assertChapterContentNotEmpty(content, chapterNumber, stage),
|
|
861
|
+
addUsage: PipelineRunner.addUsage,
|
|
862
|
+
restoreLostAuditIssues: (previous, next) => this.restoreLostAuditIssues(previous, next),
|
|
863
|
+
analyzeAITells,
|
|
864
|
+
analyzeSensitiveWords,
|
|
865
|
+
logWarn: (message) => this.logWarn(pipelineLang, message),
|
|
866
|
+
logStage: (message) => this.logStage(stageLanguage, message),
|
|
737
867
|
});
|
|
738
|
-
totalUsage =
|
|
739
|
-
finalContent =
|
|
740
|
-
finalWordCount =
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
this.logStage(stageLanguage, { zh: "审计草稿", en: "auditing draft" });
|
|
746
|
-
const llmAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, reducedControlInput);
|
|
747
|
-
totalUsage = PipelineRunner.addUsage(totalUsage, llmAudit.tokenUsage);
|
|
748
|
-
const aiTellsResult = analyzeAITells(finalContent);
|
|
749
|
-
const sensitiveWriteResult = analyzeSensitiveWords(finalContent);
|
|
750
|
-
const hasBlockedWriteWords = sensitiveWriteResult.found.some((f) => f.severity === "block");
|
|
751
|
-
let auditResult = {
|
|
752
|
-
passed: hasBlockedWriteWords ? false : llmAudit.passed,
|
|
753
|
-
issues: [...llmAudit.issues, ...aiTellsResult.issues, ...sensitiveWriteResult.issues],
|
|
754
|
-
summary: llmAudit.summary,
|
|
755
|
-
};
|
|
756
|
-
// 3. If audit fails, try auto-revise once
|
|
757
|
-
if (!auditResult.passed) {
|
|
758
|
-
const criticalIssues = auditResult.issues.filter((i) => i.severity === "critical");
|
|
759
|
-
if (criticalIssues.length > 0) {
|
|
760
|
-
const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
|
|
761
|
-
this.logStage(stageLanguage, { zh: "自动修复关键问题", en: "auto-revising critical issues" });
|
|
762
|
-
const reviseOutput = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, auditResult.issues, "spot-fix", book.genre, {
|
|
763
|
-
...reducedControlInput,
|
|
764
|
-
lengthSpec,
|
|
765
|
-
});
|
|
766
|
-
totalUsage = PipelineRunner.addUsage(totalUsage, reviseOutput.tokenUsage);
|
|
767
|
-
if (reviseOutput.revisedContent.length > 0) {
|
|
768
|
-
const normalizedRevision = await this.normalizeDraftLengthIfNeeded({
|
|
769
|
-
bookId,
|
|
770
|
-
chapterNumber,
|
|
771
|
-
chapterContent: reviseOutput.revisedContent,
|
|
772
|
-
lengthSpec,
|
|
773
|
-
chapterIntent: writeInput.chapterIntent,
|
|
774
|
-
});
|
|
775
|
-
totalUsage = PipelineRunner.addUsage(totalUsage, normalizedRevision.tokenUsage);
|
|
776
|
-
postReviseCount = normalizedRevision.wordCount;
|
|
777
|
-
normalizeApplied = normalizeApplied || normalizedRevision.applied;
|
|
778
|
-
// Guard: reject revision if AI markers increased
|
|
779
|
-
const preMarkers = analyzeAITells(finalContent);
|
|
780
|
-
const postMarkers = analyzeAITells(normalizedRevision.content);
|
|
781
|
-
const preCount = preMarkers.issues.length;
|
|
782
|
-
const postCount = postMarkers.issues.length;
|
|
783
|
-
if (postCount > preCount) {
|
|
784
|
-
// Revision made text MORE AI-like — discard it, keep original
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
finalContent = normalizedRevision.content;
|
|
788
|
-
finalWordCount = normalizedRevision.wordCount;
|
|
789
|
-
revised = true;
|
|
790
|
-
this.assertChapterContentNotEmpty(finalContent, chapterNumber, "revision");
|
|
791
|
-
}
|
|
792
|
-
// Re-audit the (possibly revised) content
|
|
793
|
-
const reAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, { ...reducedControlInput, temperature: 0 });
|
|
794
|
-
totalUsage = PipelineRunner.addUsage(totalUsage, reAudit.tokenUsage);
|
|
795
|
-
const reAITells = analyzeAITells(finalContent);
|
|
796
|
-
const reSensitive = analyzeSensitiveWords(finalContent);
|
|
797
|
-
const reHasBlocked = reSensitive.found.some((f) => f.severity === "block");
|
|
798
|
-
auditResult = this.restoreLostAuditIssues(auditResult, {
|
|
799
|
-
passed: reHasBlocked ? false : reAudit.passed,
|
|
800
|
-
issues: [...reAudit.issues, ...reAITells.issues, ...reSensitive.issues],
|
|
801
|
-
summary: reAudit.summary,
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
868
|
+
totalUsage = reviewResult.totalUsage;
|
|
869
|
+
let finalContent = reviewResult.finalContent;
|
|
870
|
+
let finalWordCount = reviewResult.finalWordCount;
|
|
871
|
+
let revised = reviewResult.revised;
|
|
872
|
+
let auditResult = reviewResult.auditResult;
|
|
873
|
+
const postReviseCount = reviewResult.postReviseCount;
|
|
874
|
+
const normalizeApplied = reviewResult.normalizeApplied;
|
|
806
875
|
// 4. Save the final chapter and truth files from a single persistence source
|
|
807
876
|
this.logStage(stageLanguage, { zh: "落盘最终章节", en: "persisting final chapter" });
|
|
808
877
|
this.logStage(stageLanguage, { zh: "生成最终真相文件", en: "rebuilding final truth files" });
|
|
809
878
|
const chapterIndexBeforePersist = await this.state.loadChapterIndex(bookId);
|
|
810
879
|
const { resolveDuplicateTitle } = await import("../agents/post-write-validator.js");
|
|
811
|
-
const initialTitleResolution = resolveDuplicateTitle(output.title, chapterIndexBeforePersist.map((chapter) => chapter.title), pipelineLang);
|
|
880
|
+
const initialTitleResolution = resolveDuplicateTitle(output.title, chapterIndexBeforePersist.map((chapter) => chapter.title), pipelineLang, { content: finalContent });
|
|
812
881
|
let persistenceOutput = await this.buildPersistenceOutput(bookId, book, bookDir, chapterNumber, initialTitleResolution.title === output.title
|
|
813
882
|
? output
|
|
814
883
|
: { ...output, title: initialTitleResolution.title }, finalContent, lengthSpec.countingMode, reducedControlInput);
|
|
815
|
-
const finalTitleResolution = resolveDuplicateTitle(persistenceOutput.title, chapterIndexBeforePersist.map((chapter) => chapter.title), pipelineLang);
|
|
884
|
+
const finalTitleResolution = resolveDuplicateTitle(persistenceOutput.title, chapterIndexBeforePersist.map((chapter) => chapter.title), pipelineLang, { content: finalContent });
|
|
816
885
|
if (finalTitleResolution.title !== persistenceOutput.title) {
|
|
817
886
|
persistenceOutput = {
|
|
818
887
|
...persistenceOutput,
|
|
@@ -821,8 +890,8 @@ export class PipelineRunner {
|
|
|
821
890
|
}
|
|
822
891
|
if (persistenceOutput.title !== output.title) {
|
|
823
892
|
const description = pipelineLang === "en"
|
|
824
|
-
? `
|
|
825
|
-
: `章节标题"${output.title}"
|
|
893
|
+
? `Chapter title "${output.title}" was auto-adjusted to "${persistenceOutput.title}".`
|
|
894
|
+
: `章节标题"${output.title}"已自动调整为"${persistenceOutput.title}"。`;
|
|
826
895
|
this.config.logger?.warn(`[title] ${description}`);
|
|
827
896
|
auditResult = {
|
|
828
897
|
...auditResult,
|
|
@@ -856,7 +925,7 @@ export class PipelineRunner {
|
|
|
856
925
|
const lengthTelemetry = this.buildLengthTelemetry({
|
|
857
926
|
lengthSpec,
|
|
858
927
|
writerCount,
|
|
859
|
-
postWriterNormalizeCount:
|
|
928
|
+
postWriterNormalizeCount: reviewResult.preAuditNormalizedWordCount,
|
|
860
929
|
postReviseCount,
|
|
861
930
|
finalCount: finalWordCount,
|
|
862
931
|
normalizeApplied,
|
|
@@ -866,31 +935,36 @@ export class PipelineRunner {
|
|
|
866
935
|
// 4.1 Validate settler output before writing
|
|
867
936
|
this.logStage(stageLanguage, { zh: "校验真相文件变更", en: "validating truth file updates" });
|
|
868
937
|
const storyDir = join(bookDir, "story");
|
|
869
|
-
const [oldState, oldHooks] = await Promise.all([
|
|
938
|
+
const [oldState, oldHooks, oldLedger] = await Promise.all([
|
|
870
939
|
readFile(join(storyDir, "current_state.md"), "utf-8").catch(() => ""),
|
|
871
940
|
readFile(join(storyDir, "pending_hooks.md"), "utf-8").catch(() => ""),
|
|
941
|
+
readFile(join(storyDir, "particle_ledger.md"), "utf-8").catch(() => ""),
|
|
872
942
|
]);
|
|
873
943
|
const validator = new StateValidatorAgent(this.agentCtxFor("state-validator", bookId));
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
944
|
+
const truthValidation = await validateChapterTruthPersistence({
|
|
945
|
+
writer,
|
|
946
|
+
validator,
|
|
947
|
+
book,
|
|
948
|
+
bookDir,
|
|
949
|
+
chapterNumber,
|
|
950
|
+
title: persistenceOutput.title,
|
|
951
|
+
content: finalContent,
|
|
952
|
+
persistenceOutput,
|
|
953
|
+
auditResult,
|
|
954
|
+
previousTruth: {
|
|
955
|
+
oldState,
|
|
956
|
+
oldHooks,
|
|
957
|
+
oldLedger,
|
|
958
|
+
},
|
|
959
|
+
reducedControlInput,
|
|
960
|
+
language: pipelineLang,
|
|
961
|
+
logWarn: (message) => this.logWarn(pipelineLang, message),
|
|
962
|
+
logger: this.config.logger,
|
|
963
|
+
});
|
|
964
|
+
let chapterStatus = truthValidation.chapterStatus;
|
|
965
|
+
let degradedIssues = truthValidation.degradedIssues;
|
|
966
|
+
persistenceOutput = truthValidation.persistenceOutput;
|
|
967
|
+
auditResult = truthValidation.auditResult;
|
|
894
968
|
// 4.2 Final paragraph shape check on persisted content (post-normalize, post-revise)
|
|
895
969
|
{
|
|
896
970
|
const { detectParagraphLengthDrift, detectParagraphShapeWarnings, } = await import("../agents/post-write-validator.js");
|
|
@@ -919,75 +993,51 @@ export class PipelineRunner {
|
|
|
919
993
|
};
|
|
920
994
|
}
|
|
921
995
|
}
|
|
922
|
-
|
|
923
|
-
await
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const now = new Date().toISOString();
|
|
930
|
-
const newEntry = {
|
|
931
|
-
number: chapterNumber,
|
|
932
|
-
title: persistenceOutput.title,
|
|
933
|
-
status: auditResult.passed ? "ready-for-review" : "audit-failed",
|
|
934
|
-
wordCount: finalWordCount,
|
|
935
|
-
createdAt: now,
|
|
936
|
-
updatedAt: now,
|
|
937
|
-
auditIssues: auditResult.issues.map((i) => `[${i.severity}] ${i.description}`),
|
|
996
|
+
const resolvedStatus = chapterStatus ?? (auditResult.passed ? "ready-for-review" : "audit-failed");
|
|
997
|
+
await persistChapterArtifacts({
|
|
998
|
+
chapterNumber,
|
|
999
|
+
chapterTitle: persistenceOutput.title,
|
|
1000
|
+
status: resolvedStatus,
|
|
1001
|
+
auditResult,
|
|
1002
|
+
finalWordCount,
|
|
938
1003
|
lengthWarnings,
|
|
939
1004
|
lengthTelemetry,
|
|
1005
|
+
degradedIssues,
|
|
940
1006
|
tokenUsage: totalUsage,
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
en: `> Chapter ${chapterNumber} audit found the following issues to avoid in the next chapter:`,
|
|
962
|
-
}),
|
|
963
|
-
...driftIssues.map((i) => `> - [${i.severity}] ${i.category}: ${i.description}`),
|
|
964
|
-
"",
|
|
965
|
-
].join("\n");
|
|
966
|
-
// Replace existing correction block or append
|
|
967
|
-
const existingCorrectionIdx = currentState.indexOf(correctionHeader);
|
|
968
|
-
const updatedState = existingCorrectionIdx >= 0
|
|
969
|
-
? currentState.slice(0, existingCorrectionIdx) + correctionBlock
|
|
970
|
-
: currentState + "\n\n" + correctionBlock;
|
|
971
|
-
await writeFile(statePath, updatedState, "utf-8");
|
|
972
|
-
}
|
|
973
|
-
catch {
|
|
974
|
-
// Non-critical — don't block pipeline if drift correction fails
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
// 5.6 Snapshot state for rollback support
|
|
978
|
-
this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" });
|
|
979
|
-
await this.state.snapshotState(bookId, chapterNumber);
|
|
980
|
-
await this.syncCurrentStateFactHistory(bookId, chapterNumber);
|
|
1007
|
+
loadChapterIndex: () => this.state.loadChapterIndex(bookId),
|
|
1008
|
+
saveChapter: () => writer.saveChapter(bookDir, persistenceOutput, gp.numericalSystem, pipelineLang),
|
|
1009
|
+
saveTruthFiles: async () => {
|
|
1010
|
+
await writer.saveNewTruthFiles(bookDir, persistenceOutput, pipelineLang);
|
|
1011
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, persistenceOutput);
|
|
1012
|
+
this.logStage(stageLanguage, { zh: "同步记忆索引", en: "syncing memory indexes" });
|
|
1013
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
1014
|
+
},
|
|
1015
|
+
saveChapterIndex: (index) => this.state.saveChapterIndex(bookId, index),
|
|
1016
|
+
markBookActiveIfNeeded: () => this.markBookActiveIfNeeded(bookId),
|
|
1017
|
+
persistAuditDriftGuidance: (issues) => this.persistAuditDriftGuidance({
|
|
1018
|
+
bookDir,
|
|
1019
|
+
chapterNumber,
|
|
1020
|
+
issues,
|
|
1021
|
+
language: stageLanguage,
|
|
1022
|
+
}).catch(() => undefined),
|
|
1023
|
+
snapshotState: () => this.state.snapshotState(bookId, chapterNumber),
|
|
1024
|
+
syncCurrentStateFactHistory: () => this.syncCurrentStateFactHistory(bookId, chapterNumber),
|
|
1025
|
+
logSnapshotStage: () => this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" }),
|
|
1026
|
+
});
|
|
981
1027
|
// 6. Send notification
|
|
982
1028
|
if (this.config.notifyChannels && this.config.notifyChannels.length > 0) {
|
|
983
|
-
const statusEmoji =
|
|
1029
|
+
const statusEmoji = resolvedStatus === "state-degraded"
|
|
1030
|
+
? "🧯"
|
|
1031
|
+
: auditResult.passed ? "✅" : "⚠️";
|
|
984
1032
|
const chapterLength = formatLengthCount(finalWordCount, lengthSpec.countingMode);
|
|
985
1033
|
await dispatchNotification(this.config.notifyChannels, {
|
|
986
1034
|
title: `${statusEmoji} ${book.title} 第${chapterNumber}章`,
|
|
987
1035
|
body: [
|
|
988
1036
|
`**${persistenceOutput.title}** | ${chapterLength}`,
|
|
989
1037
|
revised ? "📝 已自动修正" : "",
|
|
990
|
-
|
|
1038
|
+
resolvedStatus === "state-degraded"
|
|
1039
|
+
? "状态结算: 已降级保存,需先修复 state 再继续"
|
|
1040
|
+
: `审稿: ${auditResult.passed ? "通过" : "需人工审核"}`,
|
|
991
1041
|
...auditResult.issues
|
|
992
1042
|
.filter((i) => i.severity !== "info")
|
|
993
1043
|
.map((i) => `- [${i.severity}] ${i.description}`),
|
|
@@ -1001,6 +1051,7 @@ export class PipelineRunner {
|
|
|
1001
1051
|
wordCount: finalWordCount,
|
|
1002
1052
|
passed: auditResult.passed,
|
|
1003
1053
|
revised,
|
|
1054
|
+
status: resolvedStatus,
|
|
1004
1055
|
});
|
|
1005
1056
|
return {
|
|
1006
1057
|
chapterNumber,
|
|
@@ -1008,12 +1059,112 @@ export class PipelineRunner {
|
|
|
1008
1059
|
wordCount: finalWordCount,
|
|
1009
1060
|
auditResult,
|
|
1010
1061
|
revised,
|
|
1011
|
-
status:
|
|
1062
|
+
status: resolvedStatus,
|
|
1012
1063
|
lengthWarnings,
|
|
1013
1064
|
lengthTelemetry,
|
|
1014
1065
|
tokenUsage: totalUsage,
|
|
1015
1066
|
};
|
|
1016
1067
|
}
|
|
1068
|
+
async _repairChapterStateLocked(bookId, chapterNumber) {
|
|
1069
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
1070
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1071
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
1072
|
+
const index = [...(await this.state.loadChapterIndex(bookId))];
|
|
1073
|
+
if (index.length === 0) {
|
|
1074
|
+
throw new Error(`Book "${bookId}" has no persisted chapters to repair.`);
|
|
1075
|
+
}
|
|
1076
|
+
const targetChapter = chapterNumber ?? index[index.length - 1].number;
|
|
1077
|
+
const targetIndex = index.findIndex((chapter) => chapter.number === targetChapter);
|
|
1078
|
+
if (targetIndex < 0) {
|
|
1079
|
+
throw new Error(`Chapter ${targetChapter} not found in "${bookId}".`);
|
|
1080
|
+
}
|
|
1081
|
+
const targetMeta = index[targetIndex];
|
|
1082
|
+
const latestChapter = Math.max(...index.map((chapter) => chapter.number));
|
|
1083
|
+
if (targetMeta.status !== "state-degraded") {
|
|
1084
|
+
throw new Error(`Chapter ${targetChapter} is not state-degraded.`);
|
|
1085
|
+
}
|
|
1086
|
+
if (targetChapter !== latestChapter) {
|
|
1087
|
+
throw new Error(`Only the latest state-degraded chapter can be repaired safely (latest is ${latestChapter}).`);
|
|
1088
|
+
}
|
|
1089
|
+
this.logStage(stageLanguage, { zh: "修复章节状态结算", en: "repairing chapter state settlement" });
|
|
1090
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
1091
|
+
const pipelineLang = book.language ?? gp.language;
|
|
1092
|
+
const content = await this.readChapterContent(bookDir, targetChapter);
|
|
1093
|
+
const storyDir = join(bookDir, "story");
|
|
1094
|
+
const [oldState, oldHooks] = await Promise.all([
|
|
1095
|
+
readFile(join(storyDir, "current_state.md"), "utf-8").catch(() => ""),
|
|
1096
|
+
readFile(join(storyDir, "pending_hooks.md"), "utf-8").catch(() => ""),
|
|
1097
|
+
]);
|
|
1098
|
+
const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
|
|
1099
|
+
let repairedOutput = await writer.settleChapterState({
|
|
1100
|
+
book,
|
|
1101
|
+
bookDir,
|
|
1102
|
+
chapterNumber: targetChapter,
|
|
1103
|
+
title: targetMeta.title,
|
|
1104
|
+
content,
|
|
1105
|
+
});
|
|
1106
|
+
const validator = new StateValidatorAgent(this.agentCtxFor("state-validator", bookId));
|
|
1107
|
+
let validation = await validator.validate(content, targetChapter, oldState, repairedOutput.updatedState, oldHooks, repairedOutput.updatedHooks, pipelineLang);
|
|
1108
|
+
if (!validation.passed) {
|
|
1109
|
+
const recovery = await retrySettlementAfterValidationFailure({
|
|
1110
|
+
writer,
|
|
1111
|
+
validator,
|
|
1112
|
+
book,
|
|
1113
|
+
bookDir,
|
|
1114
|
+
chapterNumber: targetChapter,
|
|
1115
|
+
title: targetMeta.title,
|
|
1116
|
+
content,
|
|
1117
|
+
oldState,
|
|
1118
|
+
oldHooks,
|
|
1119
|
+
originalValidation: validation,
|
|
1120
|
+
language: pipelineLang,
|
|
1121
|
+
logWarn: (message) => this.logWarn(pipelineLang, message),
|
|
1122
|
+
logger: this.config.logger,
|
|
1123
|
+
});
|
|
1124
|
+
if (recovery.kind !== "recovered") {
|
|
1125
|
+
throw new Error(recovery.issues[0]?.description
|
|
1126
|
+
?? `State repair still failed for chapter ${targetChapter}.`);
|
|
1127
|
+
}
|
|
1128
|
+
repairedOutput = recovery.output;
|
|
1129
|
+
validation = recovery.validation;
|
|
1130
|
+
}
|
|
1131
|
+
if (!validation.passed) {
|
|
1132
|
+
throw new Error(`State repair still failed for chapter ${targetChapter}.`);
|
|
1133
|
+
}
|
|
1134
|
+
await writer.saveChapter(bookDir, repairedOutput, gp.numericalSystem, pipelineLang);
|
|
1135
|
+
await writer.saveNewTruthFiles(bookDir, repairedOutput, pipelineLang);
|
|
1136
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, targetChapter, repairedOutput);
|
|
1137
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
1138
|
+
await this.state.snapshotState(bookId, targetChapter);
|
|
1139
|
+
await this.syncCurrentStateFactHistory(bookId, targetChapter);
|
|
1140
|
+
const baseStatus = resolveStateDegradedBaseStatus(targetMeta);
|
|
1141
|
+
const degradedMetadata = parseStateDegradedReviewNote(targetMeta.reviewNote);
|
|
1142
|
+
const injectedIssues = new Set(degradedMetadata?.injectedIssues ?? []);
|
|
1143
|
+
index[targetIndex] = {
|
|
1144
|
+
...targetMeta,
|
|
1145
|
+
status: baseStatus,
|
|
1146
|
+
updatedAt: new Date().toISOString(),
|
|
1147
|
+
auditIssues: targetMeta.auditIssues.filter((issue) => !injectedIssues.has(issue)),
|
|
1148
|
+
reviewNote: undefined,
|
|
1149
|
+
};
|
|
1150
|
+
await this.state.saveChapterIndex(bookId, index);
|
|
1151
|
+
const repairedPassesAudit = baseStatus !== "audit-failed";
|
|
1152
|
+
return {
|
|
1153
|
+
chapterNumber: targetChapter,
|
|
1154
|
+
title: targetMeta.title,
|
|
1155
|
+
wordCount: targetMeta.wordCount,
|
|
1156
|
+
auditResult: {
|
|
1157
|
+
passed: repairedPassesAudit,
|
|
1158
|
+
issues: [],
|
|
1159
|
+
summary: repairedPassesAudit ? "state repaired" : "state repaired but chapter still needs review",
|
|
1160
|
+
},
|
|
1161
|
+
revised: false,
|
|
1162
|
+
status: baseStatus,
|
|
1163
|
+
lengthWarnings: targetMeta.lengthWarnings,
|
|
1164
|
+
lengthTelemetry: targetMeta.lengthTelemetry,
|
|
1165
|
+
tokenUsage: targetMeta.tokenUsage,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1017
1168
|
// ---------------------------------------------------------------------------
|
|
1018
1169
|
// Import operations (style imitation + canon for spinoff)
|
|
1019
1170
|
// ---------------------------------------------------------------------------
|
|
@@ -1200,8 +1351,36 @@ ${matrix}`,
|
|
|
1200
1351
|
].join("\n");
|
|
1201
1352
|
const canon = response.content + metaBlock;
|
|
1202
1353
|
await writeFile(join(storyDir, "parent_canon.md"), canon, "utf-8");
|
|
1354
|
+
// Also generate style guide from parent's chapter text if available
|
|
1355
|
+
const parentChaptersDir = join(parentDir, "chapters");
|
|
1356
|
+
const parentChapterText = await this.readParentChapterSample(parentChaptersDir);
|
|
1357
|
+
if (parentChapterText.length >= 500) {
|
|
1358
|
+
await this.tryGenerateStyleGuide(targetBookId, parentChapterText, parentBook.title);
|
|
1359
|
+
}
|
|
1203
1360
|
return canon;
|
|
1204
1361
|
}
|
|
1362
|
+
async readParentChapterSample(chaptersDir) {
|
|
1363
|
+
try {
|
|
1364
|
+
const entries = await readdir(chaptersDir);
|
|
1365
|
+
const mdFiles = entries
|
|
1366
|
+
.filter((file) => file.endsWith(".md"))
|
|
1367
|
+
.sort()
|
|
1368
|
+
.slice(0, 5);
|
|
1369
|
+
const chunks = [];
|
|
1370
|
+
let totalLength = 0;
|
|
1371
|
+
for (const file of mdFiles) {
|
|
1372
|
+
if (totalLength >= 20000)
|
|
1373
|
+
break;
|
|
1374
|
+
const content = await readFile(join(chaptersDir, file), "utf-8");
|
|
1375
|
+
chunks.push(content);
|
|
1376
|
+
totalLength += content.length;
|
|
1377
|
+
}
|
|
1378
|
+
return chunks.join("\n\n---\n\n");
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
return "";
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1205
1384
|
// ---------------------------------------------------------------------------
|
|
1206
1385
|
// Chapter import (for continuation writing from existing chapters)
|
|
1207
1386
|
// ---------------------------------------------------------------------------
|
|
@@ -1236,6 +1415,14 @@ ${matrix}`,
|
|
|
1236
1415
|
await this.resetImportReplayTruthFiles(bookDir, resolvedLanguage);
|
|
1237
1416
|
await this.state.saveChapterIndex(input.bookId, []);
|
|
1238
1417
|
await this.state.snapshotState(input.bookId, 0);
|
|
1418
|
+
// Generate style guide from imported chapters
|
|
1419
|
+
if (allText.length >= 500) {
|
|
1420
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1421
|
+
zh: "提取原文风格指纹...",
|
|
1422
|
+
en: "Extracting source style fingerprint...",
|
|
1423
|
+
}));
|
|
1424
|
+
await this.tryGenerateStyleGuide(input.bookId, allText, book.title, resolvedLanguage);
|
|
1425
|
+
}
|
|
1239
1426
|
log?.info(this.localize(resolvedLanguage, {
|
|
1240
1427
|
zh: "基础设定已生成。",
|
|
1241
1428
|
en: "Foundation generated.",
|
|
@@ -1363,6 +1550,14 @@ ${matrix}`,
|
|
|
1363
1550
|
tokenUsage: output.tokenUsage,
|
|
1364
1551
|
};
|
|
1365
1552
|
}
|
|
1553
|
+
async assertNoPendingStateRepair(bookId) {
|
|
1554
|
+
const existingIndex = await this.state.loadChapterIndex(bookId);
|
|
1555
|
+
const latestChapter = [...existingIndex].sort((left, right) => right.number - left.number)[0];
|
|
1556
|
+
if (latestChapter?.status !== "state-degraded") {
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
throw new Error(`Latest chapter ${latestChapter.number} is state-degraded. Repair state or rewrite that chapter before continuing.`);
|
|
1560
|
+
}
|
|
1366
1561
|
// ---------------------------------------------------------------------------
|
|
1367
1562
|
// Helpers
|
|
1368
1563
|
// ---------------------------------------------------------------------------
|
|
@@ -1742,6 +1937,58 @@ ${matrix}`,
|
|
|
1742
1937
|
lengthWarning: params.lengthWarning,
|
|
1743
1938
|
};
|
|
1744
1939
|
}
|
|
1940
|
+
async persistAuditDriftGuidance(params) {
|
|
1941
|
+
const storyDir = join(params.bookDir, "story");
|
|
1942
|
+
const driftPath = join(storyDir, "audit_drift.md");
|
|
1943
|
+
const statePath = join(storyDir, "current_state.md");
|
|
1944
|
+
const currentState = await readFile(statePath, "utf-8").catch(() => "");
|
|
1945
|
+
const sanitizedState = this.stripAuditDriftCorrectionBlock(currentState).trimEnd();
|
|
1946
|
+
if (sanitizedState !== currentState) {
|
|
1947
|
+
await writeFile(statePath, sanitizedState, "utf-8");
|
|
1948
|
+
}
|
|
1949
|
+
if (params.issues.length === 0) {
|
|
1950
|
+
await rm(driftPath, { force: true }).catch(() => undefined);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const block = [
|
|
1954
|
+
this.localize(params.language, {
|
|
1955
|
+
zh: "# 审计纠偏",
|
|
1956
|
+
en: "# Audit Drift",
|
|
1957
|
+
}),
|
|
1958
|
+
"",
|
|
1959
|
+
this.localize(params.language, {
|
|
1960
|
+
zh: "## 审计纠偏(自动生成,下一章写作前参照)",
|
|
1961
|
+
en: "## Audit Drift Correction",
|
|
1962
|
+
}),
|
|
1963
|
+
"",
|
|
1964
|
+
this.localize(params.language, {
|
|
1965
|
+
zh: `> 第${params.chapterNumber}章审计发现以下问题,下一章写作时必须避免:`,
|
|
1966
|
+
en: `> Chapter ${params.chapterNumber} audit found the following issues to avoid in the next chapter:`,
|
|
1967
|
+
}),
|
|
1968
|
+
...params.issues.map((issue) => `> - [${issue.severity}] ${issue.category}: ${issue.description}`),
|
|
1969
|
+
"",
|
|
1970
|
+
].join("\n");
|
|
1971
|
+
await writeFile(driftPath, block, "utf-8");
|
|
1972
|
+
}
|
|
1973
|
+
stripAuditDriftCorrectionBlock(currentState) {
|
|
1974
|
+
const headers = [
|
|
1975
|
+
"## 审计纠偏(自动生成,下一章写作前参照)",
|
|
1976
|
+
"## Audit Drift Correction",
|
|
1977
|
+
"# 审计纠偏",
|
|
1978
|
+
"# Audit Drift",
|
|
1979
|
+
];
|
|
1980
|
+
let cutIndex = -1;
|
|
1981
|
+
for (const header of headers) {
|
|
1982
|
+
const index = currentState.indexOf(header);
|
|
1983
|
+
if (index >= 0 && (cutIndex < 0 || index < cutIndex)) {
|
|
1984
|
+
cutIndex = index;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
if (cutIndex < 0) {
|
|
1988
|
+
return currentState;
|
|
1989
|
+
}
|
|
1990
|
+
return currentState.slice(0, cutIndex).trimEnd();
|
|
1991
|
+
}
|
|
1745
1992
|
logLengthWarnings(lengthWarnings) {
|
|
1746
1993
|
for (const warning of lengthWarnings) {
|
|
1747
1994
|
this.config.logger?.warn(warning);
|
|
@@ -1765,8 +2012,9 @@ ${matrix}`,
|
|
|
1765
2012
|
return {
|
|
1766
2013
|
...next,
|
|
1767
2014
|
auditResult,
|
|
1768
|
-
|
|
1769
|
-
|
|
2015
|
+
revisionBlockingIssues: previous.revisionBlockingIssues,
|
|
2016
|
+
blockingCount: previous.blockingCount,
|
|
2017
|
+
criticalCount: previous.criticalCount,
|
|
1770
2018
|
};
|
|
1771
2019
|
}
|
|
1772
2020
|
async evaluateMergedAudit(params) {
|
|
@@ -1786,6 +2034,14 @@ ${matrix}`,
|
|
|
1786
2034
|
...sensitiveResult.issues,
|
|
1787
2035
|
...longSpanFatigue.issues,
|
|
1788
2036
|
];
|
|
2037
|
+
// revisionBlockingIssues excludes long-span-fatigue issues by
|
|
2038
|
+
// construction (not by category name) so that an LLM-reported issue
|
|
2039
|
+
// sharing a category label with a long-span issue is still counted.
|
|
2040
|
+
const revisionBlockingIssues = [
|
|
2041
|
+
...llmAudit.issues,
|
|
2042
|
+
...aiTells.issues,
|
|
2043
|
+
...sensitiveResult.issues,
|
|
2044
|
+
];
|
|
1789
2045
|
return {
|
|
1790
2046
|
auditResult: {
|
|
1791
2047
|
passed: hasBlockedWords ? false : llmAudit.passed,
|
|
@@ -1794,8 +2050,9 @@ ${matrix}`,
|
|
|
1794
2050
|
tokenUsage: llmAudit.tokenUsage,
|
|
1795
2051
|
},
|
|
1796
2052
|
aiTellCount: aiTells.issues.length,
|
|
1797
|
-
blockingCount:
|
|
1798
|
-
criticalCount:
|
|
2053
|
+
blockingCount: revisionBlockingIssues.filter((issue) => issue.severity === "warning" || issue.severity === "critical").length,
|
|
2054
|
+
criticalCount: revisionBlockingIssues.filter((issue) => issue.severity === "critical").length,
|
|
2055
|
+
revisionBlockingIssues,
|
|
1799
2056
|
};
|
|
1800
2057
|
}
|
|
1801
2058
|
async markBookActiveIfNeeded(bookId) {
|
|
@@ -1822,7 +2079,7 @@ ${matrix}`,
|
|
|
1822
2079
|
async resolveGovernedPlan(book, bookDir, chapterNumber, externalContext, options) {
|
|
1823
2080
|
if (options?.reuseExistingIntentWhenContextMissing &&
|
|
1824
2081
|
(!externalContext || externalContext.trim().length === 0)) {
|
|
1825
|
-
const persisted = await
|
|
2082
|
+
const persisted = await loadPersistedPlan(bookDir, chapterNumber);
|
|
1826
2083
|
if (persisted)
|
|
1827
2084
|
return persisted;
|
|
1828
2085
|
}
|
|
@@ -1834,87 +2091,6 @@ ${matrix}`,
|
|
|
1834
2091
|
externalContext,
|
|
1835
2092
|
});
|
|
1836
2093
|
}
|
|
1837
|
-
async loadPersistedPlan(bookDir, chapterNumber) {
|
|
1838
|
-
const runtimePath = join(bookDir, "story", "runtime", `chapter-${String(chapterNumber).padStart(4, "0")}.intent.md`);
|
|
1839
|
-
try {
|
|
1840
|
-
const intentMarkdown = await readFile(runtimePath, "utf-8");
|
|
1841
|
-
const sections = this.parseIntentSections(intentMarkdown);
|
|
1842
|
-
const goal = this.readIntentScalar(sections, "Goal");
|
|
1843
|
-
if (!goal || this.isInvalidPersistedIntentScalar(goal))
|
|
1844
|
-
return null;
|
|
1845
|
-
const outlineNode = this.readIntentScalar(sections, "Outline Node");
|
|
1846
|
-
if (outlineNode && outlineNode !== "(not found)" && this.isInvalidPersistedIntentScalar(outlineNode)) {
|
|
1847
|
-
return null;
|
|
1848
|
-
}
|
|
1849
|
-
const conflicts = this.readIntentList(sections, "Conflicts")
|
|
1850
|
-
.map((line) => {
|
|
1851
|
-
const separator = line.indexOf(":");
|
|
1852
|
-
if (separator < 0)
|
|
1853
|
-
return null;
|
|
1854
|
-
const type = line.slice(0, separator).trim();
|
|
1855
|
-
const resolution = line.slice(separator + 1).trim();
|
|
1856
|
-
if (!type || !resolution)
|
|
1857
|
-
return null;
|
|
1858
|
-
return { type, resolution };
|
|
1859
|
-
})
|
|
1860
|
-
.filter((conflict) => conflict !== null);
|
|
1861
|
-
return {
|
|
1862
|
-
intent: ChapterIntentSchema.parse({
|
|
1863
|
-
chapter: chapterNumber,
|
|
1864
|
-
goal,
|
|
1865
|
-
outlineNode: outlineNode && outlineNode !== "(not found)" ? outlineNode : undefined,
|
|
1866
|
-
mustKeep: this.readIntentList(sections, "Must Keep"),
|
|
1867
|
-
mustAvoid: this.readIntentList(sections, "Must Avoid"),
|
|
1868
|
-
styleEmphasis: this.readIntentList(sections, "Style Emphasis"),
|
|
1869
|
-
conflicts,
|
|
1870
|
-
}),
|
|
1871
|
-
intentMarkdown,
|
|
1872
|
-
plannerInputs: [runtimePath],
|
|
1873
|
-
runtimePath,
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
catch {
|
|
1877
|
-
return null;
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
parseIntentSections(markdown) {
|
|
1881
|
-
const sections = new Map();
|
|
1882
|
-
let current = null;
|
|
1883
|
-
for (const line of markdown.split("\n")) {
|
|
1884
|
-
if (line.startsWith("## ")) {
|
|
1885
|
-
current = line.slice(3).trim();
|
|
1886
|
-
sections.set(current, []);
|
|
1887
|
-
continue;
|
|
1888
|
-
}
|
|
1889
|
-
if (!current)
|
|
1890
|
-
continue;
|
|
1891
|
-
sections.get(current)?.push(line);
|
|
1892
|
-
}
|
|
1893
|
-
return sections;
|
|
1894
|
-
}
|
|
1895
|
-
readIntentScalar(sections, name) {
|
|
1896
|
-
const lines = sections.get(name) ?? [];
|
|
1897
|
-
const value = lines.map((line) => line.trim()).find((line) => line.length > 0);
|
|
1898
|
-
return value && value !== "- none" ? value : undefined;
|
|
1899
|
-
}
|
|
1900
|
-
readIntentList(sections, name) {
|
|
1901
|
-
return (sections.get(name) ?? [])
|
|
1902
|
-
.map((line) => line.trim())
|
|
1903
|
-
.filter((line) => line.startsWith("-") && line !== "- none")
|
|
1904
|
-
.map((line) => line.replace(/^-\s*/, ""));
|
|
1905
|
-
}
|
|
1906
|
-
isInvalidPersistedIntentScalar(value) {
|
|
1907
|
-
const normalized = value.trim();
|
|
1908
|
-
if (!normalized)
|
|
1909
|
-
return true;
|
|
1910
|
-
if (/^[*_`~::|.-]+$/.test(normalized))
|
|
1911
|
-
return true;
|
|
1912
|
-
return (/^\((describe|briefly describe|write)\b[\s\S]*\)$/i.test(normalized)
|
|
1913
|
-
|| /^((?:在这里描述|描述|填写|写下)[\s\S]*)$/u.test(normalized));
|
|
1914
|
-
}
|
|
1915
|
-
relativeToBookDir(bookDir, absolutePath) {
|
|
1916
|
-
return relative(bookDir, absolutePath).replaceAll("\\", "/");
|
|
1917
|
-
}
|
|
1918
2094
|
async emitWebhook(event, bookId, chapterNumber, data) {
|
|
1919
2095
|
if (!this.config.notifyChannels || this.config.notifyChannels.length === 0)
|
|
1920
2096
|
return;
|