Store category prompts as EXACT split legacy text, no boilerplate

The per-category prompt editor showed English machine boilerplate
("Role: description. Build JSON …", schema intro, {{var}} context lines)
because seed/sync baked the render framing into categories.prompt. Now the
stored overlay is exactly the legacy wp_product_categories.sql content,
split into the three sections a user would type themselves:

  --- Title ---        Slovenian naming formula
  --- Description ---  legacy <H2>/<p>/<ul> body structure
  --- Meta ---         legacy metaDescription instruction

Schema, role framing, and Name/Description/Category/Attrs product context
stay render-time only (system template + ensureCategoryEnhanceUserContext),
where they already existed.

- SplitLegacyCombinedEnhancePrompt: non-legacy input now returns "" —
  categories without seed/legacy content get their override CLEARED
  (company default) instead of being stuffed with the canonical template
  (e.g. parent categories like "Bela tehnika" absent from the SQL).
- CategoryEnhancePromptNeedsRepair flags stored boilerplate ("parsed as
  JSON", "Build JSON", retired Attributes section) so Sync rewrites old
  data to the clean shape; repair supports clearing (prompt = '{}').
- seed-a1 apply-category-prompts skips instead of writing template text.
- Web editor compose stores only the user's section text: empty sections
  keep bare markers, default bodies and the default preamble are never
  persisted (DEFAULT_SECTION_BODIES removed).
- Verified locally: apply rewrote 238 A1+Demo categories (216 exact
  splits, 22 cleared), zero boilerplate matches in DB, idempotent re-run
  (would_update=0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 00:53:26 +02:00
co-authored by Claude Fable 5
parent 0eef202f56
commit 9a839d6d13
13 changed files with 182 additions and 157 deletions
+20 -16
View File
@@ -34,19 +34,14 @@ export const LEGACY_ATTRIBUTES_MARKERS = {
end: "--- End Attributes ---"
} as const;
/** Canonical shared intro (same idea as CategoryEnhanceUserTemplate preamble). */
/**
* UI-only default intro shown in the editor for context. NEVER stored: the
* stored prompt is exactly the user's section text — schema/role framing and
* product context are supplied by the pipeline at render time.
*/
export const DEFAULT_ENHANCE_PREAMBLE =
"Write all product text in {{language}} (do not hardcode a language). Keep the title, description, and SEO meta consistent with the product evidence below.";
/** Minimal role bodies used when a section is empty on compose (keeps markers valid). */
export const DEFAULT_SECTION_BODIES: Record<EnhancePromptSectionId, string> = {
title:
"Write a short retail product title from the title formula and attributes — never brand-only. Include product type and full model when evidence exists; follow any title formula constraints that follow.\nName: {{name}}",
description:
"Write the product description as HTML (not SEO meta). When a description formula follows, cover each section in order as one HTML string; otherwise prefer 13 factual paragraphs with simple HTML (<h2><p><ul><li>).\nDescription: {{description}}",
meta: "Write the SEO title (5060 characters) and SEO description (120155 characters) as plain text, never HTML. Follow any SEO meta formula that follows; do not copy the full description into the SEO description."
};
export type SectionSchemaHint = {
/** JSON keys this section is responsible for in the one-shot enhance reply. */
jsonKeys: string[];
@@ -170,20 +165,29 @@ export function parseEnhancePrompt(prompt: string): ParsedEnhancePrompt {
}
/**
* Compose preamble + section bodies back into the stored enhance USER template shape.
* Empty section bodies fall back to DEFAULT_SECTION_BODIES so markers stay valid.
* Compose section bodies back into the stored enhance USER template shape.
* Stores ONLY the user's text inside the markers — empty sections keep bare
* markers, and the default preamble is never persisted (a custom, non-default
* preamble from an existing prompt is kept).
*/
export function composeEnhancePrompt(
preamble: string,
sections: Partial<Record<EnhancePromptSectionId, string>>
): string {
const intro = (preamble.trim() || DEFAULT_ENHANCE_PREAMBLE).trim();
const parts: string[] = [intro, ""];
const intro = preamble.trim();
const parts: string[] = [];
if (intro && intro !== DEFAULT_ENHANCE_PREAMBLE.trim()) {
parts.push(intro, "");
}
for (const id of ENHANCE_PROMPT_SECTIONS) {
const { start, end } = SECTION_MARKERS[id];
const body = (sections[id] ?? "").trim() || DEFAULT_SECTION_BODIES[id];
parts.push(start, body, end, "");
const body = (sections[id] ?? "").trim();
if (body) {
parts.push(start, body, end, "");
} else {
parts.push(start, end, "");
}
}
return parts.join("\n").trim();
@@ -75,16 +75,22 @@ describe("category prompt sections", () => {
assert.equal(parsed.sections.title, "");
});
it("compose fills empty sections with defaults so markers remain", () => {
it("compose stores ONLY the user's text — no boilerplate, no preamble", () => {
const out = composeEnhancePrompt(DEFAULT_ENHANCE_PREAMBLE, {
title: "Custom title rules",
title: "Napiši novo ime izdelka po formuli",
description: "",
meta: ""
});
assert.equal(hasEnhanceRoleSections(out), true);
assert.match(out, /Custom title rules/);
assert.match(out, /product description as HTML/);
assert.match(out, /Napiši novo ime izdelka po formuli/);
// Empty sections keep bare markers; nothing is injected.
assert.equal(out.includes(DEFAULT_ENHANCE_PREAMBLE), false);
assert.equal(out.includes(LEGACY_ATTRIBUTES_MARKERS.start), false);
assert.equal(out.toLowerCase().includes("write the"), false);
const reparsed = parseEnhancePrompt(out);
assert.equal(reparsed.sections.title, "Napiši novo ime izdelka po formuli");
assert.equal(reparsed.sections.description, "");
assert.equal(reparsed.sections.meta, "");
});
it("isEnhancePromptEmpty ignores default preamble", () => {
+4 -8
View File
@@ -8,10 +8,7 @@ import { describe, it } from "node:test";
import { UI_LOCALES } from "./i18n/locales.ts";
import { en } from "./i18n/messages/en.ts";
import { loadAllMessages, messagesFor } from "./i18n/messages/catalog.ts";
import {
DEFAULT_ENHANCE_PREAMBLE,
DEFAULT_SECTION_BODIES
} from "./categories/prompt-sections.ts";
import { DEFAULT_ENHANCE_PREAMBLE } from "./categories/prompt-sections.ts";
const CUSTOMER_DEMO_EMPTY_KEYS = ["dashboard.demoEmptyHint", "dashboard.demoEmptyMessage"] as const;
const CUSTOMER_ETL_KEYS = [
@@ -72,9 +69,8 @@ describe("customer-visible copy leaks", () => {
});
it("category prompt defaults hide JSON schema jargon from customer text", () => {
const blob = [DEFAULT_ENHANCE_PREAMBLE, ...Object.values(DEFAULT_SECTION_BODIES)].join("\n");
assert.doesNotMatch(blob, /system schema/i);
assert.doesNotMatch(blob, /your reply is parsed as json/i);
assert.doesNotMatch(blob, /build json/i);
assert.doesNotMatch(DEFAULT_ENHANCE_PREAMBLE, /system schema/i);
assert.doesNotMatch(DEFAULT_ENHANCE_PREAMBLE, /your reply is parsed as json/i);
assert.doesNotMatch(DEFAULT_ENHANCE_PREAMBLE, /build json/i);
});
});