feat: define optimizer domain model

This commit is contained in:
Codex
2026-06-21 23:42:37 +08:00
parent a8403fc313
commit 8c24949bfa
3 changed files with 302 additions and 0 deletions
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import {
articleInputSchema,
confirmedFactCardSchema,
candidateFactCardSchema,
qaReportSchema,
} from "../validation";
describe("domain validation", () => {
it("accepts article input with title, body, images, platform, and instructions", () => {
const parsed = articleInputSchema.parse({
title: "How GEO Optimization Improves Brand Visibility",
body: "A practical overview of GEO optimization for marketing teams.",
images: [
{ type: "description", content: "Dashboard screenshot" },
{ type: "link", content: "https://example.com/image.png" },
],
platform: "official_site",
user_instructions: "Keep the article factual and concise.",
});
expect(parsed.images).toHaveLength(2);
expect(parsed.platform).toBe("official_site");
});
it("marks a fact card with unresolved uncertain items as not ready for optimization", () => {
const parsed = candidateFactCardSchema.parse({
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: [],
image_topics: ["Product dashboard"],
uncertain_items: ["Conflicting product names found"],
});
expect(parsed.is_ready_for_optimization).toBe(false);
});
it("rejects a confirmed fact card with an empty company full name", () => {
expect(() =>
confirmedFactCardSchema.parse({
company_full_name: "",
company_short_names: ["Example"],
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: [],
image_topics: [],
uncertain_items: [],
confirmed_by_user: true,
}),
).toThrow();
});
it("accepts QA reports only with pass, warn, or fail statuses", () => {
const valid = qaReportSchema.parse({
job_id: "job_123",
revision: 1,
overall_status: "warn",
checks: [
{
rule_id: "title_quality",
status: "pass",
evidence: "Title reads naturally.",
reason: "No grammar issue detected.",
suggested_fix: "",
target_agent: null,
},
],
});
expect(valid.checks[0]?.status).toBe("pass");
expect(() =>
qaReportSchema.parse({
job_id: "job_123",
revision: 1,
overall_status: "blocked",
checks: [],
}),
).toThrow();
});
});
+84
View File
@@ -0,0 +1,84 @@
export type PublishPlatform =
| "official_site"
| "media_article"
| "comparison_review"
| "recommendation_list";
export type CheckStatus = "pass" | "warn" | "fail";
export type 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 ImageInput {
type: "description" | "link";
content: string;
}
export interface ArticleInput {
title: string;
body: string;
images: ImageInput[];
platform: PublishPlatform;
user_instructions: string;
}
export interface CandidateFactCard {
company_full_name: string;
company_short_names: string[];
brand_names: string[];
product_names: string[];
target_industry: string;
target_audience: string;
experience_years: number | null;
core_claims: string[];
forbidden_claims: string[];
image_topics: string[];
uncertain_items: string[];
is_ready_for_optimization: boolean;
}
export interface ConfirmedFactCard extends CandidateFactCard {
confirmed_by_user: true;
is_ready_for_optimization: true;
}
export interface ImageSuggestion {
source: string;
suggestion: string;
}
export interface OptimizedArticle {
job_id?: string;
revision?: number;
title: string;
summary: string;
body_markdown: string;
image_suggestions: ImageSuggestion[];
changed_sections: string[];
requires_user_confirmation: string[];
}
export interface QaCheck {
rule_id: QualityRuleId;
status: CheckStatus;
evidence: string;
reason: string;
suggested_fix: string;
target_agent: string | null;
}
export interface QaReport {
job_id?: string;
revision?: number;
overall_status: CheckStatus;
checks: QaCheck[];
}
+127
View File
@@ -0,0 +1,127 @@
import { z } from "zod";
import type {
ArticleInput,
CandidateFactCard,
CheckStatus,
ConfirmedFactCard,
ImageInput,
OptimizedArticle,
PublishPlatform,
QaCheck,
QaReport,
QualityRuleId,
} from "./types";
export const publishPlatformSchema = z.enum([
"official_site",
"media_article",
"comparison_review",
"recommendation_list",
]) satisfies z.ZodType<PublishPlatform>;
export const checkStatusSchema = z.enum([
"pass",
"warn",
"fail",
]) satisfies z.ZodType<CheckStatus>;
export const qualityRuleIdSchema = z.enum([
"industry_alignment",
"image_text_match",
"voice_consistency",
"platform_fit",
"company_name_integrity",
"title_quality",
"body_quality",
"hallucination_risk",
"claim_consistency",
"context_sensitive_terms",
]) satisfies z.ZodType<QualityRuleId>;
export const imageInputSchema = z.object({
type: z.enum(["description", "link"]),
content: z.string().trim().min(1),
}) satisfies z.ZodType<ImageInput>;
export const articleInputSchema = z.object({
title: z.string().trim().min(1),
body: z.string().trim().min(1),
images: z.array(imageInputSchema).default([]),
platform: publishPlatformSchema,
user_instructions: z.string().trim().default(""),
}) satisfies z.ZodType<ArticleInput>;
const factCardBaseSchema = z.object({
company_full_name: z.string().trim(),
company_short_names: z.array(z.string().trim().min(1)).default([]),
brand_names: z.array(z.string().trim().min(1)).default([]),
product_names: z.array(z.string().trim().min(1)).default([]),
target_industry: z.string().trim().min(1),
target_audience: z.string().trim().min(1),
experience_years: z.number().int().nonnegative().nullable().default(null),
core_claims: z.array(z.string().trim().min(1)).default([]),
forbidden_claims: z.array(z.string().trim().min(1)).default([]),
image_topics: z.array(z.string().trim().min(1)).default([]),
uncertain_items: z.array(z.string().trim().min(1)).default([]),
});
export const candidateFactCardSchema = factCardBaseSchema
.extend({
is_ready_for_optimization: z.boolean().optional(),
})
.transform((card) => ({
...card,
is_ready_for_optimization: card.uncertain_items.length === 0,
})) satisfies z.ZodType<CandidateFactCard>;
export const confirmedFactCardSchema = candidateFactCardSchema
.pipe(
z.object({
company_full_name: z.string().trim().min(1),
company_short_names: z.array(z.string().trim().min(1)).default([]),
brand_names: z.array(z.string().trim().min(1)).default([]),
product_names: z.array(z.string().trim().min(1)).default([]),
target_industry: z.string().trim().min(1),
target_audience: z.string().trim().min(1),
experience_years: z.number().int().nonnegative().nullable().default(null),
core_claims: z.array(z.string().trim().min(1)).default([]),
forbidden_claims: z.array(z.string().trim().min(1)).default([]),
image_topics: z.array(z.string().trim().min(1)).default([]),
uncertain_items: z.array(z.never()).length(0),
is_ready_for_optimization: z.literal(true),
}),
)
.and(z.object({ confirmed_by_user: z.literal(true) })) satisfies z.ZodType<ConfirmedFactCard>;
export const imageSuggestionSchema = z.object({
source: z.string().trim().min(1),
suggestion: z.string().trim().min(1),
});
export const optimizedArticleSchema = z.object({
job_id: z.string().trim().min(1).optional(),
revision: z.number().int().positive().optional(),
title: z.string().trim().min(1),
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([]),
requires_user_confirmation: z.array(z.string().trim().min(1)).default([]),
}) satisfies z.ZodType<OptimizedArticle>;
export const qaCheckSchema = z.object({
rule_id: qualityRuleIdSchema,
status: checkStatusSchema,
evidence: z.string().trim().min(1),
reason: z.string().trim().min(1),
suggested_fix: z.string().trim().default(""),
target_agent: z.string().trim().min(1).nullable().default(null),
}) satisfies z.ZodType<QaCheck>;
export const qaReportSchema = z.object({
job_id: z.string().trim().min(1).optional(),
revision: z.number().int().positive().optional(),
overall_status: checkStatusSchema,
checks: z.array(qaCheckSchema).default([]),
}) satisfies z.ZodType<QaReport>;