mirror of
https://github.com/Narcooo/inkos.git
synced 2026-09-24 15:29:47 +08:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}字)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user