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:
@@ -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 1–3 factual paragraphs with simple HTML (<h2><p><ul><li>).\nDescription: {{description}}",
|
||||
meta: "Write the SEO title (50–60 characters) and SEO description (120–155 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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user