fix: detect duplicate chapter titles in prompt and post-write validation

- Writer prompt: add "title must differ from existing chapters" constraint
- post-write-validator: add detectDuplicateTitle() for exact and near-duplicate matching
- runner: check title uniqueness against chapter index before persist

Addresses #94
This commit is contained in:
Ma
2026-03-28 20:38:21 +08:00
parent 7a4a486698
commit aeb6d2c171
3 changed files with 68 additions and 2 deletions
@@ -544,3 +544,47 @@ const ENGLISH_NAME_STOP_WORDS = new Set([
"Then",
"They",
]);
/**
* Detect duplicate or near-duplicate chapter titles.
* Compares the new title against existing chapter titles from index.
*/
export function detectDuplicateTitle(
newTitle: string,
existingTitles: ReadonlyArray<string>,
): ReadonlyArray<PostWriteViolation> {
if (!newTitle.trim()) return [];
const normalized = newTitle.trim().toLowerCase();
const violations: PostWriteViolation[] = [];
for (const existing of existingTitles) {
const existingNorm = existing.trim().toLowerCase();
if (!existingNorm) continue;
// Exact match
if (normalized === existingNorm) {
violations.push({
rule: "duplicate-title",
severity: "warning",
description: `章节标题"${newTitle}"与已有章节标题完全相同`,
suggestion: "更换一个不同的章节标题",
});
break;
}
// Near-duplicate: one is substring of the other, or only differs by punctuation/numbers
const stripPunct = (s: string) => s.replace(/[^\p{L}\p{N}]/gu, "");
if (stripPunct(normalized) === stripPunct(existingNorm)) {
violations.push({
rule: "near-duplicate-title",
severity: "warning",
description: `章节标题"${newTitle}"与已有标题"${existing}"高度相似`,
suggestion: "避免使用相似的章节标题",
});
break;
}
}
return violations;
}
+2 -2
View File
@@ -535,7 +535,7 @@ ${resourceRow}| 待回收伏笔 | Hook-A / Hook-B | 与伏笔池一致 |
${preWriteTable}
=== CHAPTER_TITLE ===
(章节标题,不含"第X章")
(章节标题,不含"第X章"。标题必须与已有章节标题不同,不要重复使用相同或相似的标题)
=== CHAPTER_CONTENT ===
(正文内容,目标${lengthSpec.target}字,允许区间${lengthSpec.softMin}-${lengthSpec.softMax}字)
@@ -588,7 +588,7 @@ ${resourceRow}| 待回收伏笔 | Hook-A / Hook-B | 与伏笔池一致 |
${preWriteTable}
=== CHAPTER_TITLE ===
(章节标题,不含"第X章")
(章节标题,不含"第X章"。标题必须与已有章节标题不同,不要重复使用相同或相似的标题)
=== CHAPTER_CONTENT ===
(正文内容,目标${lengthSpec.target}字,允许区间${lengthSpec.softMin}-${lengthSpec.softMax}字)
+22
View File
@@ -1204,6 +1204,28 @@ export class PipelineRunner {
}
}
// 4.3 Title dedup check
{
const { detectDuplicateTitle } = await import("../agents/post-write-validator.js");
const chapterIndex = await this.state.loadChapterIndex(bookId);
const existingTitles = chapterIndex.map((ch) => ch.title);
const titleIssues = detectDuplicateTitle(persistenceOutput.title, existingTitles);
if (titleIssues.length > 0) {
for (const issue of titleIssues) {
this.config.logger?.warn(`[title] ${issue.description}`);
}
auditResult = {
...auditResult,
issues: [...auditResult.issues, ...titleIssues.map((v) => ({
severity: v.severity as "warning",
category: "title-dedup",
description: v.description,
suggestion: v.suggestion,
}))],
};
}
}
await writer.saveChapter(bookDir, persistenceOutput, gp.numericalSystem, pipelineLang);
await writer.saveNewTruthFiles(bookDir, persistenceOutput, pipelineLang);
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, persistenceOutput);