Procházet zdrojové kódy

feat: post-write validator errors now trigger auto spot-fix

Previously post-write violations were logged to stderr but
chapters were saved regardless (advisory-only). Now error-severity
violations trigger an automatic spot-fix revision pass before
the LLM audit phase, making the validator a real quality gate.

Writer output now includes postWriteErrors and postWriteWarnings
for downstream inspection.
majx_mac před 8 měsíci
rodič
revize
63421596dc

+ 9 - 6
packages/core/src/agents/writer.ts

@@ -4,7 +4,7 @@ import type { GenreProfile } from "../models/genre-profile.js";
 import type { BookRules } from "../models/book-rules.js";
 import { buildWriterSystemPrompt } from "./writer-prompts.js";
 import { readGenreProfile, readBookRules } from "./rules-reader.js";
-import { validatePostWrite } from "./post-write-validator.js";
+import { validatePostWrite, type PostWriteViolation } from "./post-write-validator.js";
 import { analyzeAITells } from "./ai-tells.js";
 import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
 import { join } from "node:path";
@@ -32,6 +32,8 @@ export interface WriteChapterOutput {
   readonly updatedSubplots: string;
   readonly updatedEmotionalArcs: string;
   readonly updatedCharacterMatrix: string;
+  readonly postWriteErrors: ReadonlyArray<PostWriteViolation>;
+  readonly postWriteWarnings: ReadonlyArray<PostWriteViolation>;
 }
 
 export class WriterAgent extends BaseAgent {
@@ -117,11 +119,12 @@ export class WriterAgent extends BaseAgent {
     const ruleViolations = validatePostWrite(output.content, genreProfile, bookRules);
     const aiTellIssues = analyzeAITells(output.content).issues;
 
+    const postWriteErrors = ruleViolations.filter(v => v.severity === "error");
+    const postWriteWarnings = ruleViolations.filter(v => v.severity === "warning");
+
     if (ruleViolations.length > 0) {
-      const errors = ruleViolations.filter(v => v.severity === "error");
-      const warnings = ruleViolations.filter(v => v.severity === "warning");
       process.stderr.write(
-        `[writer] Post-write: ${errors.length} errors, ${warnings.length} warnings in chapter ${chapterNumber}\n`,
+        `[writer] Post-write: ${postWriteErrors.length} errors, ${postWriteWarnings.length} warnings in chapter ${chapterNumber}\n`,
       );
       for (const v of ruleViolations) {
         process.stderr.write(`  [${v.severity}] ${v.rule}: ${v.description}\n`);
@@ -136,7 +139,7 @@ export class WriterAgent extends BaseAgent {
       }
     }
 
-    return output;
+    return { ...output, postWriteErrors, postWriteWarnings };
   }
 
   async saveChapter(
@@ -290,7 +293,7 @@ ${params.volumeOutline}
     chapterNumber: number,
     content: string,
     genreProfile: GenreProfile,
-  ): WriteChapterOutput {
+  ): Omit<WriteChapterOutput, "postWriteErrors" | "postWriteWarnings"> {
     const extract = (tag: string): string => {
       const regex = new RegExp(
         `=== ${tag} ===\\s*([\\s\\S]*?)(?==== [A-Z_]+ ===|$)`,

+ 35 - 8
packages/core/src/pipeline/runner.ts

@@ -415,16 +415,47 @@ export class PipelineRunner {
       ...(temperatureOverride ? { temperatureOverride } : {}),
     });
 
-    // 2. Audit chapter
+    // 2a. Post-write error gate: if deterministic rules found errors, auto-fix before LLM audit
+    let finalContent = output.content;
+    let finalWordCount = output.wordCount;
+    let revised = false;
+
+    if (output.postWriteErrors.length > 0) {
+      process.stderr.write(
+        `[pipeline] ${output.postWriteErrors.length} post-write errors detected, triggering spot-fix before audit\n`,
+      );
+      const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
+      const spotFixIssues = output.postWriteErrors.map((v) => ({
+        severity: "critical" as const,
+        category: v.rule,
+        description: v.description,
+        suggestion: v.suggestion,
+      }));
+      const fixResult = await reviser.reviseChapter(
+        bookDir,
+        finalContent,
+        chapterNumber,
+        spotFixIssues,
+        "spot-fix",
+        book.genre,
+      );
+      if (fixResult.revisedContent.length > 0) {
+        finalContent = fixResult.revisedContent;
+        finalWordCount = fixResult.wordCount;
+        revised = true;
+      }
+    }
+
+    // 2b. LLM audit
     const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
     const llmAudit = await auditor.auditChapter(
       bookDir,
-      output.content,
+      finalContent,
       chapterNumber,
       book.genre,
     );
-    const aiTellsResult = analyzeAITells(output.content);
-    const sensitiveWriteResult = analyzeSensitiveWords(output.content);
+    const aiTellsResult = analyzeAITells(finalContent);
+    const sensitiveWriteResult = analyzeSensitiveWords(finalContent);
     const hasBlockedWriteWords = sensitiveWriteResult.found.some((f) => f.severity === "block");
     let auditResult: AuditResult = {
       passed: hasBlockedWriteWords ? false : llmAudit.passed,
@@ -432,10 +463,6 @@ export class PipelineRunner {
       summary: llmAudit.summary,
     };
 
-    let finalContent = output.content;
-    let finalWordCount = output.wordCount;
-    let revised = false;
-
     // 3. If audit fails, try auto-revise once
     if (!auditResult.passed) {
       const criticalIssues = auditResult.issues.filter(