fix: allow incomplete candidate fact cards

This commit is contained in:
Codex
2026-06-21 23:43:03 +08:00
parent fd7e92b026
commit adf844f752
2 changed files with 84 additions and 3 deletions
@@ -42,6 +42,62 @@ describe("domain validation", () => {
expect(parsed.is_ready_for_optimization).toBe(false); expect(parsed.is_ready_for_optimization).toBe(false);
}); });
it("normalizes near-valid LLM fact card field types", () => {
const emptyExperience = 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", "Brand teams"],
experience_years: "",
core_claims: ["GEO optimization experience"],
forbidden_claims: [],
image_topics: [],
uncertain_items: [],
});
const numericExperience = 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: ["GEO optimization experience"],
forbidden_claims: [],
image_topics: [],
uncertain_items: [],
});
expect(emptyExperience.target_audience).toBe("Marketing teams、Brand teams");
expect(emptyExperience.experience_years).toBeNull();
expect(numericExperience.experience_years).toBe(8);
});
it("keeps incomplete candidate fact cards editable and ready", () => {
const parsed = candidateFactCardSchema.parse({
company_full_name: "",
company_short_names: [],
brand_names: [],
product_names: [],
target_industry: "",
target_audience: "",
experience_years: "",
core_claims: [],
forbidden_claims: [],
image_topics: [],
uncertain_items: [],
});
expect(parsed.target_industry).toBe("");
expect(parsed.target_audience).toBe("");
expect(parsed.experience_years).toBeNull();
expect(parsed.uncertain_items).toEqual([]);
expect(parsed.is_ready_for_optimization).toBe(true);
});
it("rejects a confirmed fact card with an empty company full name", () => { it("rejects a confirmed fact card with an empty company full name", () => {
expect(() => expect(() =>
confirmedFactCardSchema.parse({ confirmedFactCardSchema.parse({
+28 -3
View File
@@ -52,14 +52,39 @@ export const articleInputSchema = z.object({
user_instructions: z.string().trim().default(""), user_instructions: z.string().trim().default(""),
}) satisfies z.ZodType<ArticleInput>; }) satisfies z.ZodType<ArticleInput>;
const stringOrStringArraySchema = z.preprocess((value) => {
if (Array.isArray(value)) {
return value
.map((item) => (typeof item === "string" ? item.trim() : ""))
.filter(Boolean)
.join("、");
}
return value;
}, z.string().trim());
const experienceYearsSchema = z.preprocess((value) => {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (
trimmed === "" ||
/^(?:unknown|none|null|n\/a|not\s+specified|不详|不明确|未知|无)$/i.test(
trimmed,
)
) {
return null;
}
const yearMatch = trimmed.match(/\d{1,3}/);
return yearMatch ? Number(yearMatch[0]) : value;
}, z.number().int().nonnegative().nullable().default(null));
const factCardBaseSchema = z.object({ const factCardBaseSchema = z.object({
company_full_name: z.string().trim(), company_full_name: z.string().trim(),
company_short_names: z.array(z.string().trim().min(1)).default([]), company_short_names: z.array(z.string().trim().min(1)).default([]),
brand_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([]), product_names: z.array(z.string().trim().min(1)).default([]),
target_industry: z.string().trim().min(1), target_industry: z.string().trim(),
target_audience: z.string().trim().min(1), target_audience: stringOrStringArraySchema,
experience_years: z.number().int().nonnegative().nullable().default(null), experience_years: experienceYearsSchema,
core_claims: z.array(z.string().trim().min(1)).default([]), core_claims: z.array(z.string().trim().min(1)).default([]),
forbidden_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([]), image_topics: z.array(z.string().trim().min(1)).default([]),