From 5a627bcaa8fce01f1e10700d8afcd1d826c4f74d Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 17 Jun 2026 11:09:45 +0800 Subject: [PATCH] Handle object-shaped changed sections --- src/lib/domain/__tests__/validation.test.ts | 20 ++++++++ src/lib/domain/validation.ts | 56 ++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/lib/domain/__tests__/validation.test.ts b/src/lib/domain/__tests__/validation.test.ts index 85fea9f..93fa23e 100644 --- a/src/lib/domain/__tests__/validation.test.ts +++ b/src/lib/domain/__tests__/validation.test.ts @@ -4,6 +4,7 @@ import { articleInputSchema, confirmedFactCardSchema, candidateFactCardSchema, + optimizedArticleSchema, qaReportSchema, } from "../validation"; @@ -144,4 +145,23 @@ describe("domain validation", () => { }), ).toThrow(); }); + + it("normalizes object-shaped changed sections from LLM output", () => { + const parsed = optimizedArticleSchema.parse({ + title: "Optimized article", + summary: "Summary constrained by the confirmed fact card.", + body_markdown: "## Body\nUpdated body.", + image_suggestions: [], + changed_sections: [ + { section: "title", change: "Improved keyword clarity." }, + { name: "body", reason: "Fixed sentence flow." }, + ], + requires_user_confirmation: [], + }); + + expect(parsed.changed_sections).toEqual([ + "title: Improved keyword clarity.", + "body: Fixed sentence flow.", + ]); + }); }); diff --git a/src/lib/domain/validation.ts b/src/lib/domain/validation.ts index d969637..64cd6a6 100644 --- a/src/lib/domain/validation.ts +++ b/src/lib/domain/validation.ts @@ -117,6 +117,60 @@ export const imageSuggestionSchema = z.object({ suggestion: z.string().trim().min(1), }); +function firstStringField(record: Record, keys: string[]) { + for (const key of keys) { + const value = record[key]; + if (typeof value === "string" && value.trim().length > 0) { + return value.trim(); + } + } + return null; +} + +function normalizeChangedSection(value: unknown) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return value; + } + + const record = value as Record; + const section = firstStringField(record, [ + "section", + "name", + "title", + "field", + "target", + "part", + ]); + const detail = firstStringField(record, [ + "change", + "changed", + "summary", + "reason", + "description", + "fix", + "after", + ]); + + if (section && detail && section !== detail) { + return `${section}: ${detail}`; + } + if (section || detail) { + return section ?? detail; + } + + const stringValues = Object.values(record) + .filter((entry): entry is string => typeof entry === "string") + .map((entry) => entry.trim()) + .filter(Boolean); + + return stringValues.join("; "); +} + +const changedSectionSchema = z.preprocess( + normalizeChangedSection, + z.string().trim().min(1), +); + export const optimizedArticleSchema = z.object({ job_id: z.string().trim().min(1).optional(), revision: z.number().int().positive().optional(), @@ -124,7 +178,7 @@ export const optimizedArticleSchema = z.object({ summary: z.string().trim().min(1), body_markdown: z.string().trim().min(1), image_suggestions: z.array(imageSuggestionSchema).default([]), - changed_sections: z.array(z.string().trim().min(1)).default([]), + changed_sections: z.array(changedSectionSchema).default([]), requires_user_confirmation: z.array(z.string().trim().min(1)).default([]), }) satisfies z.ZodType;