feat: implement article optimization workflow

This commit is contained in:
Codex
2026-06-21 23:42:37 +08:00
parent cb56ff9ee7
commit 75bcab33c6
10 changed files with 834 additions and 1 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import type {
OptimizedArticle,
PublishPlatform,
QaReport,
} from "@/lib/domain/types";
} from "../domain/types";
import { createDatabase, getDefaultDatabasePath } from "./connection";
import { initializeSchema } from "./schema";
+112
View File
@@ -0,0 +1,112 @@
import OpenAI from "openai";
export interface GenerateInput {
system?: string;
prompt: string;
model?: string;
temperature?: number;
}
export interface LlmProviderStatus {
provider: "deepseek" | "openai";
configured: boolean;
model: string;
baseURL?: string;
reason?: string;
}
function getProvider() {
return (process.env.LLM_PROVIDER || "deepseek").toLowerCase();
}
export function getLlmProviderStatus(): LlmProviderStatus {
const provider = getProvider();
if (provider === "openai") {
return {
provider: "openai",
configured: Boolean(process.env.OPENAI_API_KEY),
model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
reason: process.env.OPENAI_API_KEY ? undefined : "OPENAI_API_KEY is missing",
};
}
return {
provider: "deepseek",
configured: Boolean(process.env.DEEPSEEK_API_KEY),
model: process.env.DEEPSEEK_MODEL || "deepseek-v4-pro",
baseURL: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com",
reason: process.env.DEEPSEEK_API_KEY
? undefined
: "DEEPSEEK_API_KEY is missing",
};
}
export function isLlmConfigured() {
return getLlmProviderStatus().configured;
}
function createClient() {
const status = getLlmProviderStatus();
if (!status.configured) {
throw new Error(status.reason ?? "LLM provider is not configured");
}
if (status.provider === "openai") {
return {
client: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
model: status.model,
};
}
return {
client: new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: status.baseURL,
}),
model: status.model,
};
}
export async function generateText(input: GenerateInput) {
try {
const { client, model } = createClient();
const response = await client.chat.completions.create({
model: input.model ?? model,
temperature: input.temperature ?? 0.2,
messages: [
...(input.system ? [{ role: "system" as const, content: input.system }] : []),
{ role: "user" as const, content: input.prompt },
],
});
return response.choices[0]?.message.content ?? "";
} catch (error) {
throw normalizeLlmError(error);
}
}
export async function generateJson<T>(input: GenerateInput): Promise<T> {
try {
const { client, model } = createClient();
const response = await client.chat.completions.create({
model: input.model ?? model,
temperature: input.temperature ?? 0.1,
response_format: { type: "json_object" },
messages: [
...(input.system ? [{ role: "system" as const, content: input.system }] : []),
{ role: "user" as const, content: input.prompt },
],
});
const content = response.choices[0]?.message.content ?? "{}";
return JSON.parse(content) as T;
} catch (error) {
throw normalizeLlmError(error);
}
}
function normalizeLlmError(error: unknown) {
const message = error instanceof Error ? error.message : "Unknown LLM error";
return new Error(`LLM provider error: ${message}`);
}
+14
View File
@@ -0,0 +1,14 @@
export const JSON_ONLY_PROMPT =
"Return valid JSON only. Do not include markdown fences or commentary.";
export const ARTICLE_OPTIMIZER_SYSTEM_PROMPT = [
"You optimize GEO-related articles under a confirmed fact card.",
"Never invent numbers, cases, qualifications, company names, products, or years.",
JSON_ONLY_PROMPT,
].join(" ");
export const QUALITY_INSPECTOR_SYSTEM_PROMPT = [
"Evaluate article quality against the confirmed fact card and target platform.",
"Return one structured check per required quality gate.",
JSON_ONLY_PROMPT,
].join(" ");
+193
View File
@@ -0,0 +1,193 @@
import { describe, expect, it } from "vitest";
import { optimizeArticle } from "../article-optimizer";
import { extractCandidateFactCard } from "../fact-extractor";
import { normalizeInput } from "../input-normalizer";
import { inspectQuality } from "../quality-inspector";
import { runOptimizationWorkflow } from "../orchestrator";
import { rewriteFailedSections } from "../targeted-rewriter";
const confirmedFactCard = {
company_full_name: "Example Technology Co., Ltd.",
company_short_names: ["Example Tech"],
brand_names: ["Example"],
product_names: ["Example GEO"],
target_industry: "GEO optimization",
target_audience: "Marketing teams",
experience_years: 8,
core_claims: ["Eight years of GEO optimization experience"],
forbidden_claims: ["industry first"],
image_topics: ["product dashboard"],
uncertain_items: [],
is_ready_for_optimization: true,
confirmed_by_user: true,
} as const;
describe("workflow nodes", () => {
it("normalizes input whitespace and image lines", () => {
const normalized = normalizeInput({
title: " A GEO Article ",
body: "\nFirst paragraph.\n\nSecond paragraph. ",
image_lines:
" Product dashboard screenshot \n https://example.com/image.png \n\n",
platform: "official_site",
user_instructions: " Keep factual. ",
});
expect(normalized.article_draft.title).toBe("A GEO Article");
expect(normalized.article_draft.body).toBe(
"First paragraph.\n\nSecond paragraph.",
);
expect(normalized.image_assets).toEqual([
{ type: "description", content: "Product dashboard screenshot" },
{ type: "link", content: "https://example.com/image.png" },
]);
});
it("places missing or conflicting company facts into uncertain items", async () => {
const card = await extractCandidateFactCard({
title: "Example announces GEO product",
body: "Example has 8 years of experience. Example has 12 years of service. The article discusses GEO optimization.",
images: [],
platform: "media_article",
user_instructions: "",
});
expect(card.company_full_name).toBe("");
expect(card.uncertain_items).toEqual(
expect.arrayContaining([
expect.stringContaining("company full name"),
expect.stringContaining("Conflicting experience years"),
]),
);
expect(card.is_ready_for_optimization).toBe(false);
});
it("does not add claims outside the confirmed fact card", async () => {
const optimized = await optimizeArticle({
input: {
title: "Example GEO article",
body: "Example GEO helps marketing teams improve content structure.",
images: [],
platform: "official_site",
user_instructions:
"Say we have 99 patents and Fortune 500 customer cases.",
},
factCard: confirmedFactCard,
});
expect(optimized.body_markdown).not.toContain("99 patents");
expect(optimized.body_markdown).not.toContain("Fortune 500");
expect(optimized.requires_user_confirmation).toEqual(
expect.arrayContaining([
expect.stringContaining("99 patents"),
expect.stringContaining("Fortune 500"),
]),
);
});
it("returns the 10 required quality checks", () => {
const report = inspectQuality({
article: {
title: "Example GEO Optimization Guide",
summary: "A factual guide for marketing teams.",
body_markdown:
"Example Technology Co., Ltd. has eight years of GEO optimization experience.",
image_suggestions: [],
changed_sections: [],
requires_user_confirmation: [],
},
factCard: confirmedFactCard,
platform: "official_site",
sourceImages: [],
});
expect(report.checks.map((check) => check.rule_id)).toEqual([
"industry_alignment",
"image_text_match",
"voice_consistency",
"platform_fit",
"company_name_integrity",
"title_quality",
"body_quality",
"hallucination_risk",
"claim_consistency",
"context_sensitive_terms",
]);
});
it("hard-fails incomplete company names, hallucinated numbers, industry drift, and conflicting years", () => {
const report = inspectQuality({
article: {
title: "Example Wins Finance Automation Market!!!",
summary: "A finance automation story.",
body_markdown:
"Example has 12 years of finance automation experience, 99 patents, and works in banking automation.",
image_suggestions: [],
changed_sections: [],
requires_user_confirmation: [],
},
factCard: confirmedFactCard,
platform: "official_site",
sourceImages: [],
});
const failures = report.checks.filter((check) => check.status === "fail");
expect(failures.map((check) => check.rule_id)).toEqual(
expect.arrayContaining([
"company_name_integrity",
"hallucination_risk",
"industry_alignment",
"claim_consistency",
]),
);
expect(report.overall_status).toBe("fail");
});
it("rewrites only the failing target area", () => {
const article = {
title: "Bad title!!!",
summary: "Original summary",
body_markdown: "Original body",
image_suggestions: [],
changed_sections: [],
requires_user_confirmation: [],
};
const rewritten = rewriteFailedSections({
article,
factCard: confirmedFactCard,
failedChecks: [
{
rule_id: "title_quality",
status: "fail",
evidence: "Bad title!!!",
reason: "Punctuation stuffing.",
suggested_fix: "Rewrite title.",
target_agent: "title",
},
],
});
expect(rewritten.title).not.toBe(article.title);
expect(rewritten.summary).toBe(article.summary);
expect(rewritten.body_markdown).toBe(article.body_markdown);
});
it("orchestrator stops after two failed rewrite rounds", async () => {
const result = await runOptimizationWorkflow({
input: {
title: "Finance automation breakthrough!!!",
body: "Example has 12 years in finance automation and 99 patents.",
images: [],
platform: "official_site",
user_instructions: "",
},
factCard: confirmedFactCard,
});
expect(result.rewrite_rounds).toBe(2);
expect(result.qaReport.overall_status).toBe("fail");
expect(result.stopped_after_max_rewrites).toBe(true);
});
});
+83
View File
@@ -0,0 +1,83 @@
import type {
ArticleInput,
ConfirmedFactCard,
OptimizedArticle,
} from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation";
export interface OptimizeArticleInput {
input: ArticleInput;
factCard: ConfirmedFactCard;
}
export async function optimizeArticle({
input,
factCard,
}: OptimizeArticleInput): Promise<OptimizedArticle> {
const unsupported = findUnsupportedInstructionClaims(
input.user_instructions,
factCard,
);
const title = `${factCard.brand_names[0] ?? factCard.company_short_names[0] ?? factCard.company_full_name} ${factCard.target_industry} Guide`;
const coreClaims =
factCard.core_claims.length > 0
? factCard.core_claims.map((claim) => `- ${claim}`).join("\n")
: "- Confirmed facts only; no extra claims added.";
const body = [
`## ${factCard.company_full_name}`,
cleanBody(input.body, factCard),
"",
"### Confirmed Facts",
coreClaims,
].join("\n");
return optimizedArticleSchema.parse({
title,
summary: `A ${input.platform.replace(/_/g, " ")} article for ${factCard.target_audience} about ${factCard.target_industry}.`,
body_markdown: body,
image_suggestions: factCard.image_topics.map((topic, index) => ({
source: `image_${index + 1}`,
suggestion: `Use image content related to ${topic}.`,
})),
changed_sections: ["title", "body structure", "summary"],
requires_user_confirmation: unsupported,
});
}
function cleanBody(body: string, factCard: ConfirmedFactCard) {
let cleaned = body.trim();
for (const forbidden of factCard.forbidden_claims) {
cleaned = cleaned.replace(new RegExp(escapeRegExp(forbidden), "gi"), "");
}
return cleaned;
}
function findUnsupportedInstructionClaims(
instructions: string,
factCard: ConfirmedFactCard,
) {
const unsupported: string[] = [];
const numbers = [...instructions.matchAll(/\b\d+\s*[A-Za-z]+\b/g)].map(
(match) => match[0],
);
const knownText = [
factCard.experience_years?.toString() ?? "",
...factCard.core_claims,
].join(" ");
for (const claim of numbers) {
if (!knownText.includes(claim.replace(/\D/g, ""))) {
unsupported.push(`Unsupported requested claim: ${claim}`);
}
}
if (/fortune\s*500/i.test(instructions)) {
unsupported.push("Unsupported requested claim: Fortune 500 customer cases");
}
return unsupported;
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+73
View File
@@ -0,0 +1,73 @@
import type { ArticleInput, CandidateFactCard } from "../domain/types";
import { candidateFactCardSchema } from "../domain/validation";
export async function extractCandidateFactCard(
input: ArticleInput,
): Promise<CandidateFactCard> {
const text = `${input.title}\n${input.body}`;
const uncertainItems: string[] = [];
const companyFullName = findCompanyFullName(text);
const years = findExperienceYears(text);
if (!companyFullName) {
uncertainItems.push("Missing company full name");
}
if (years.length > 1) {
uncertainItems.push(`Conflicting experience years: ${years.join(", ")}`);
}
if (input.images.length === 0) {
uncertainItems.push("Image description is missing");
}
const industry = inferIndustry(text);
return candidateFactCardSchema.parse({
company_full_name: companyFullName ?? "",
company_short_names: companyFullName ? [companyFullName.split(/\s+/)[0] ?? ""] : [],
brand_names: inferCapitalizedNames(text),
product_names: inferProducts(text),
target_industry: industry,
target_audience: text.toLowerCase().includes("marketing")
? "Marketing teams"
: "Business readers",
experience_years: years.length === 1 ? years[0] : null,
core_claims: years.length === 1 ? [`${years[0]} years of ${industry} experience`] : [],
forbidden_claims: [],
image_topics: input.images.map((image) => image.content),
uncertain_items: uncertainItems,
});
}
function findCompanyFullName(text: string) {
const match = text.match(
/([A-Z][A-Za-z0-9&.,\-\s]{2,}?(?:Co\.,?\s*Ltd\.?|Company|Inc\.?|LLC|Ltd\.))/,
);
return match?.[1].trim() ?? null;
}
function findExperienceYears(text: string) {
const matches = [...text.matchAll(/\b(\d{1,3})\s*(?:years?|年)\b/gi)];
return [...new Set(matches.map((match) => Number(match[1])))];
}
function inferIndustry(text: string) {
const lower = text.toLowerCase();
if (lower.includes("geo")) return "GEO optimization";
if (lower.includes("finance") || lower.includes("banking")) return "finance automation";
if (lower.includes("seo")) return "SEO";
return "General business";
}
function inferCapitalizedNames(text: string) {
const names = [...text.matchAll(/\b[A-Z][A-Za-z0-9]{2,}\b/g)]
.map((match) => match[0])
.filter((word) => !["The", "This", "And"].includes(word));
return [...new Set(names)].slice(0, 5);
}
function inferProducts(text: string) {
const productMatches = [...text.matchAll(/\b([A-Z][A-Za-z0-9]+\s+GEO)\b/g)].map(
(match) => match[1],
);
return [...new Set(productMatches)];
}
+57
View File
@@ -0,0 +1,57 @@
import type { ArticleInput, ImageInput, PublishPlatform } from "../domain/types";
import { articleInputSchema } from "../domain/validation";
export interface RawArticleInput {
title: string;
body: string;
image_lines?: string;
images?: ImageInput[];
platform: PublishPlatform;
user_instructions?: string;
}
export function normalizeInput(input: RawArticleInput) {
const images =
input.images ??
(input.image_lines ?? "")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map<ImageInput>((content) => ({
type: /^https?:\/\//i.test(content) ? "link" : "description",
content,
}));
const articleInput = articleInputSchema.parse({
title: input.title,
body: input.body,
images,
platform: input.platform,
user_instructions: input.user_instructions ?? "",
});
return {
article_draft: {
title: articleInput.title,
body: normalizeBody(articleInput.body),
},
image_assets: articleInput.images,
publish_context: {
platform: articleInput.platform,
user_instructions: articleInput.user_instructions,
},
articleInput: {
...articleInput,
body: normalizeBody(articleInput.body),
} satisfies ArticleInput,
};
}
function normalizeBody(body: string) {
return body
.split(/\r?\n/)
.map((line) => line.trim())
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
+44
View File
@@ -0,0 +1,44 @@
import type { ArticleInput, ConfirmedFactCard } from "../domain/types";
import { optimizeArticle } from "./article-optimizer";
import { inspectQuality } from "./quality-inspector";
import { rewriteFailedSections } from "./targeted-rewriter";
export interface RunOptimizationWorkflowInput {
input: ArticleInput;
factCard: ConfirmedFactCard;
}
export async function runOptimizationWorkflow({
input,
factCard,
}: RunOptimizationWorkflowInput) {
let article = await optimizeArticle({ input, factCard });
let qaReport = inspectQuality({
article,
factCard,
platform: input.platform,
sourceImages: input.images,
});
let rewriteRounds = 0;
while (qaReport.overall_status === "fail" && rewriteRounds < 2) {
const failedChecks = qaReport.checks.filter((check) => check.status === "fail");
article = rewriteFailedSections({ article, factCard, failedChecks });
rewriteRounds += 1;
qaReport = inspectQuality({
article,
factCard,
platform: input.platform,
sourceImages: input.images,
});
}
return {
article,
qaReport,
rewrite_rounds: rewriteRounds,
stopped_after_max_rewrites:
qaReport.overall_status === "fail" && rewriteRounds >= 2,
};
}
+209
View File
@@ -0,0 +1,209 @@
import type {
CheckStatus,
ConfirmedFactCard,
ImageInput,
OptimizedArticle,
PublishPlatform,
QaCheck,
QaReport,
QualityRuleId,
} from "../domain/types";
import { qaReportSchema } from "../domain/validation";
const REQUIRED_RULES: QualityRuleId[] = [
"industry_alignment",
"image_text_match",
"voice_consistency",
"platform_fit",
"company_name_integrity",
"title_quality",
"body_quality",
"hallucination_risk",
"claim_consistency",
"context_sensitive_terms",
];
export interface InspectQualityInput {
article: OptimizedArticle;
factCard: ConfirmedFactCard;
platform: PublishPlatform;
sourceImages: ImageInput[];
}
export function inspectQuality(input: InspectQualityInput): QaReport {
const checks = REQUIRED_RULES.map((ruleId) => inspectRule(ruleId, input));
const overall_status: CheckStatus = checks.some((check) => check.status === "fail")
? "fail"
: checks.some((check) => check.status === "warn")
? "warn"
: "pass";
return qaReportSchema.parse({ overall_status, checks });
}
function inspectRule(
ruleId: QualityRuleId,
{ article, factCard, platform, sourceImages }: InspectQualityInput,
): QaCheck {
const combined = `${article.title}\n${article.summary}\n${article.body_markdown}`;
const lower = combined.toLowerCase();
if (ruleId === "industry_alignment") {
const aligned = lower.includes(factCard.target_industry.toLowerCase());
return check(
ruleId,
aligned ? "pass" : "fail",
aligned ? factCard.target_industry : article.summary,
aligned
? "Article stays aligned with the confirmed industry."
: "Article drifts from the confirmed industry.",
"Rewrite affected paragraphs around the confirmed industry.",
aligned ? null : "body",
);
}
if (ruleId === "image_text_match") {
const hasImages = sourceImages.length > 0 || article.image_suggestions.length > 0;
return check(
ruleId,
hasImages ? "pass" : "warn",
hasImages ? "Image topics are available." : "No image descriptions supplied.",
hasImages
? "Image guidance can be compared with article sections."
: "Image-text confidence is low without image descriptions.",
"Add image descriptions or review image placement manually.",
null,
);
}
if (ruleId === "voice_consistency") {
const thirdPartyOfficial = platform === "official_site" && /\bthey\b|\btheir\b/i.test(combined);
return check(
ruleId,
thirdPartyOfficial ? "warn" : "pass",
thirdPartyOfficial ? "Third-party pronouns found." : "Voice matches platform.",
thirdPartyOfficial
? "Official-site content should avoid detached third-party voice."
: "No obvious voice mismatch detected.",
"Rewrite in official brand voice.",
thirdPartyOfficial ? "body" : null,
);
}
if (ruleId === "platform_fit") {
return check(
ruleId,
article.summary.toLowerCase().includes(platform.replace(/_/g, " "))
? "pass"
: "warn",
article.summary,
"Platform fit is based on the generated summary and structure.",
"Review platform-specific framing.",
null,
);
}
if (ruleId === "company_name_integrity") {
const hasFullName = combined.includes(factCard.company_full_name);
return check(
ruleId,
hasFullName ? "pass" : "fail",
hasFullName ? factCard.company_full_name : article.body_markdown,
hasFullName
? "Confirmed company full name is present."
: "The confirmed company full name is missing or shortened.",
"Use the confirmed company full name at first mention.",
hasFullName ? null : "body",
);
}
if (ruleId === "title_quality") {
const badTitle = /!!!|\?\?|keyword keyword/i.test(article.title);
return check(
ruleId,
badTitle ? "fail" : "pass",
article.title,
badTitle ? "Title appears awkward or over-punctuated." : "Title reads naturally.",
"Rewrite title for clarity and grammar.",
badTitle ? "title" : null,
);
}
if (ruleId === "body_quality") {
const longSentence = combined.split(/[.!?。]/).some((part) => part.length > 220);
return check(
ruleId,
longSentence ? "warn" : "pass",
longSentence ? "Long sentence detected." : "Body structure is readable.",
longSentence
? "Some sentences are too long for comfortable reading."
: "No severe body grammar issue detected.",
"Split long sentences and clarify references.",
longSentence ? "body" : null,
);
}
if (ruleId === "hallucination_risk") {
const unsupportedNumber = findUnsupportedNumbers(combined, factCard).length > 0;
return check(
ruleId,
unsupportedNumber || article.requires_user_confirmation.length > 0 ? "fail" : "pass",
unsupportedNumber
? findUnsupportedNumbers(combined, factCard).join(", ")
: "No unsupported numeric claims found.",
unsupportedNumber
? "Numeric claims are not traceable to the confirmed fact card."
: "Factual claims are traceable to the confirmed fact card.",
"Remove or confirm unsupported claims.",
unsupportedNumber ? "body" : null,
);
}
if (ruleId === "claim_consistency") {
const years = [...combined.matchAll(/\b(\d{1,3})\s*(?:years?|年)\b/gi)].map(
(match) => Number(match[1]),
);
const conflicts = years.filter((year) => year !== factCard.experience_years);
return check(
ruleId,
conflicts.length > 0 ? "fail" : "pass",
conflicts.length > 0 ? conflicts.join(", ") : "No conflicting claims found.",
conflicts.length > 0
? "Experience years conflict with the confirmed fact card."
: "Repeated claims are consistent.",
"Normalize years, products, and service claims to confirmed facts.",
conflicts.length > 0 ? "body" : null,
);
}
return check(
ruleId,
/\b(?:sensitive|forbidden)\b/i.test(combined) ? "warn" : "pass",
"Context-sensitive wording scan complete.",
"Sensitive terms need context-aware review when present.",
"Review wording manually instead of deleting terms mechanically.",
null,
);
}
function check(
rule_id: QualityRuleId,
status: CheckStatus,
evidence: string,
reason: string,
suggested_fix: string,
target_agent: string | null,
): QaCheck {
return { rule_id, status, evidence, reason, suggested_fix, target_agent };
}
function findUnsupportedNumbers(text: string, factCard: ConfirmedFactCard) {
const allowed = new Set(
[factCard.experience_years]
.filter((value): value is number => typeof value === "number")
.map(String),
);
return [...text.matchAll(/\b\d{1,4}\b/g)]
.map((match) => match[0])
.filter((number) => !allowed.has(number));
}
+48
View File
@@ -0,0 +1,48 @@
import type { ConfirmedFactCard, OptimizedArticle, QaCheck } from "../domain/types";
export interface RewriteFailedSectionsInput {
article: OptimizedArticle;
factCard: ConfirmedFactCard;
failedChecks: QaCheck[];
}
export function rewriteFailedSections({
article,
factCard,
failedChecks,
}: RewriteFailedSectionsInput): OptimizedArticle {
let rewritten = { ...article };
for (const check of failedChecks) {
if (check.target_agent === "title") {
rewritten = {
...rewritten,
title: `${factCard.brand_names[0] ?? factCard.company_short_names[0]} ${factCard.target_industry} Guide`,
changed_sections: [...new Set([...rewritten.changed_sections, "title"])],
};
}
if (check.target_agent === "body" && check.rule_id === "company_name_integrity") {
rewritten = {
...rewritten,
body_markdown: `${factCard.company_full_name}\n\n${rewritten.body_markdown}`,
changed_sections: [...new Set([...rewritten.changed_sections, "company name"])],
};
}
if (check.target_agent === "body" && check.rule_id === "claim_consistency") {
rewritten = {
...rewritten,
body_markdown: rewritten.body_markdown.replace(
/\b\d{1,3}\s*(?:years?|年)\b/gi,
factCard.experience_years === null
? "confirmed experience"
: `${factCard.experience_years} years`,
),
changed_sections: [...new Set([...rewritten.changed_sections, "claim consistency"])],
};
}
}
return rewritten;
}