diff --git a/apps/api/cmd/repair-category-prompts/main.go b/apps/api/cmd/repair-category-prompts/main.go index 55c7b93..8326be0 100644 --- a/apps/api/cmd/repair-category-prompts/main.go +++ b/apps/api/cmd/repair-category-prompts/main.go @@ -1,6 +1,6 @@ // Command repair-category-prompts rewrites A1 / Platform Demo categories.prompt -// values into aiprompts role-sectioned overlays (Title / Description / Meta / -// Attributes). Prefers wp_product_categories.sql (SEED_A1_WP_CATEGORIES / +// values into aiprompts role-sectioned overlays (Title / Description / Meta). +// Prefers wp_product_categories.sql (SEED_A1_WP_CATEGORIES / // scripts/seed) as source of truth, else a1-category-prompts.json. Legacy combined // Slovenian name+description blobs are split so naming rules land under Title // and HTML under Description; otherwise CategoryEnhanceUserTemplate is used. diff --git a/apps/api/internal/aiprompts/description_formula.go b/apps/api/internal/aiprompts/description_formula.go index bc76221..b43b68c 100644 --- a/apps/api/internal/aiprompts/description_formula.go +++ b/apps/api/internal/aiprompts/description_formula.go @@ -38,12 +38,22 @@ func DescriptionTemplateNeedsRepair(template any) bool { return strings.TrimSpace(f.MetaTitle) == "" && strings.TrimSpace(f.MetaDescription) == "" } +// DefaultLegacyMetaTitleRule fills description_template.metaTitle for legacy A1 +// prompts, which only carried a instruction. Mirrors the legacy +// name-first Slovenian instruction style with the SEO title length bound. +const DefaultLegacyMetaTitleRule = "Najprej napiši Novo ime izdelka in dodaj glavno prednost. Dolžina 50-60 znakov vključno s presledki." + // DeriveDescriptionFormulaFromLegacyParts builds description_template from split -// legacy DescriptionRules HTML + MetaRules. +// legacy DescriptionRules HTML + MetaRules. Legacy dumps never carried a meta +// title rule, so meta intent (MetaRules present) also seeds a default metaTitle +// instruction — the category UI then shows all four prompt areas filled. func DeriveDescriptionFormulaFromLegacyParts(parts LegacyEnhanceParts) DescriptionFormula { out := DescriptionFormula{ MetaDescription: strings.TrimSpace(parts.MetaRules), } + if out.MetaDescription != "" { + out.MetaTitle = DefaultLegacyMetaTitleRule + } body := strings.TrimSpace(parts.DescriptionRules) if body == "" { return out diff --git a/apps/api/internal/aiprompts/kinds.go b/apps/api/internal/aiprompts/kinds.go index 2ab40d4..edeeae0 100644 --- a/apps/api/internal/aiprompts/kinds.go +++ b/apps/api/internal/aiprompts/kinds.go @@ -50,17 +50,19 @@ type DefaultTemplate struct { } // CategoryEnhanceUserTemplate is the shared per-category (and built-in) enhance USER -// message, sectioned by role (title / description / meta / attributes) using the -// same "--- Section ---" markers as legacy enhance-product. Includes {{name}} +// message, sectioned by role (title / description / meta) using the same +// "--- Section ---" markers as legacy enhance-product. Includes {{name}} // {{description}} {{attrs}} {{category}} {{language}}. Compatible with enhance // JSON {"name","description","meta_title","meta_description","attrs"} — // description is formula HTML product body; meta_* are plain SEO fields (legacy -// A1 / cats.json metaTitle+metaDescription); attrs is the -// category-allowlisted attribute map. Title/description/meta formulas stay in +// A1 / cats.json metaTitle+metaDescription). Attribute +// extraction is pipeline-level (allowlist via AppendAttributeConstraints) and is +// deliberately NOT a category prompt section; Category/Attrs stay as plain +// product-context lines. Title/description/meta formulas stay in // title_template / description_template and are appended at render time (see -// processing.AppendFormulaConstraints / AppendAttributeConstraints). Categorize -// (unique_id) is RoleCategorize / ProductCategorize* — not this overlay. Used by -// local A1/Demo prompt repair and seed-a1 overlays. +// processing.AppendFormulaConstraints). Categorize (unique_id) is RoleCategorize +// / ProductCategorize* — not this overlay. Used by local A1/Demo prompt repair +// and seed-a1 overlays. const CategoryEnhanceUserTemplate = `Your reply is parsed as JSON {"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}} only (system schema). Write all string fields in {{language}} (do not hardcode a language). --- Title --- @@ -77,17 +79,17 @@ Description: {{description}} Role: meta. Build JSON "meta_title" and "meta_description" as plain SEO text (never HTML). meta_title: 50-60 chars; meta_description: 120-155 chars; follow any SEO meta formula that follows; never copy the full description HTML into meta_description. --- End Meta --- ---- Attributes --- -Role: attributes. Build JSON "attrs" as an object of attribute_key → value strings. Prefer Allowed attribute keys / Title formula attr slots that follow; remap near-miss labels onto those keys; fill missing keys only from Name/Description/Category/Attrs evidence; never invent specs; omit unknown keys; never invent dimensions. +Product context: Category: {{category}} -Attrs: {{attrs}} ---- End Attributes ---` +Attrs: {{attrs}}` // CategoryEnhancePromptNeedsRepair reports whether a stored categories.prompt value // should be rewritten into role-sectioned enhance overlay form. Empty prompts are // left alone. Canonical CategoryEnhanceUserTemplate and per-category overlays that -// already carry Title/Description/Meta/Attributes markers (+ {{attrs}}) are OK — -// equality with the shared template is not required (legacy splits keep Slovenian rules). +// already carry Title/Description/Meta markers are OK — equality with the shared +// template is not required (legacy splits keep Slovenian rules). Prompts still +// carrying a retired "--- Attributes ---" role section are rewritten so attribute +// prompting stays out of category prompts. func CategoryEnhancePromptNeedsRepair(prompt string) bool { p := strings.TrimSpace(prompt) if p == "" { @@ -96,10 +98,10 @@ func CategoryEnhancePromptNeedsRepair(prompt string) bool { if IsLegacyCombinedEnhancePrompt(p) { return true } - if CategoryEnhanceHasRoleSections(p) && strings.Contains(p, "{{attrs}}") { - return false + if !CategoryEnhanceHasRoleSections(p) { + return true } - return true + return strings.Contains(strings.ToLower(p), strings.ToLower(SectionAttributesStart)) } // BuiltInDefaults match the previous hardcoded system prompts, with structured user templates. diff --git a/apps/api/internal/aiprompts/kinds_test.go b/apps/api/internal/aiprompts/kinds_test.go index f8152c7..b6c71fa 100644 --- a/apps/api/internal/aiprompts/kinds_test.go +++ b/apps/api/internal/aiprompts/kinds_test.go @@ -25,11 +25,13 @@ func TestCategoryEnhanceUserTemplateHasRequiredVars(t *testing.T) { if !strings.Contains(CategoryEnhanceUserTemplate, `{"name":"string","description":"string","meta_title":"string","meta_description":"string","attrs":{}}`) { t.Fatal("template should reference enhance JSON schema including meta_* and attrs") } - if !strings.Contains(strings.ToLower(CategoryEnhanceUserTemplate), `build json "attrs"`) { - t.Fatal("Attributes role must ask for JSON attrs extraction/enhancement") + // Attribute prompting is retired from category prompts — extraction guidance + // is pipeline-level (AppendAttributeConstraints), never prompt-authored. + if strings.Contains(strings.ToLower(CategoryEnhanceUserTemplate), `build json "attrs"`) { + t.Fatal("template must not carry an attributes role instruction") } if !CategoryEnhanceHasRoleSections(CategoryEnhanceUserTemplate) { - t.Fatal("canonical template must be role-sectioned for title/description/meta/attributes") + t.Fatal("canonical template must be role-sectioned for title/description/meta") } if !strings.Contains(lower, "{{language}}") { t.Fatal("language must come from {{language}}") diff --git a/apps/api/internal/aiprompts/legacy_split.go b/apps/api/internal/aiprompts/legacy_split.go index f39ab7f..8c1ae42 100644 --- a/apps/api/internal/aiprompts/legacy_split.go +++ b/apps/api/internal/aiprompts/legacy_split.go @@ -126,11 +126,9 @@ func BuildCategoryEnhanceOverlay(parts LegacyEnhanceParts) string { b.WriteString(SectionMetaEnd) b.WriteString("\n\n") - b.WriteString(SectionAttributesStart) - b.WriteString("\nRole: attributes. Build JSON \"attrs\" as an object of attribute_key → value strings. Prefer Allowed attribute keys / Title formula attr slots that follow; remap near-miss labels onto those keys; fill missing keys only from Name/Description/Category/Attrs evidence; never invent specs; omit unknown keys; never invent dimensions.\n") + b.WriteString("Product context:\n") b.WriteString("Category: {{category}}\n") - b.WriteString("Attrs: {{attrs}}\n") - b.WriteString(SectionAttributesEnd) + b.WriteString("Attrs: {{attrs}}") return strings.TrimSpace(b.String()) } diff --git a/apps/api/internal/aiprompts/roles.go b/apps/api/internal/aiprompts/roles.go index 24c233e..d97296c 100644 --- a/apps/api/internal/aiprompts/roles.go +++ b/apps/api/internal/aiprompts/roles.go @@ -19,6 +19,9 @@ const ( // Section markers match legacy enhance-product "--- Section ---" / "--- Meta Title ---" // style so stored category prompts stay human-readable and machine-detectable. +// Attributes markers are retired for category prompts (attribute extraction is +// pipeline-level, not prompt-authored) but kept so repair can detect and strip +// old stored prompts that still carry the section. const ( SectionTitleStart = "--- Title ---" SectionTitleEnd = "--- End Title ---" @@ -40,12 +43,12 @@ var CategoryPromptRoles = []string{ } // CategoryEnhanceHasRoleSections reports whether a stored categories.prompt -// value uses the structured title/description/meta/attributes section markers -// from CategoryEnhanceUserTemplate (seed/repair canonical shape). +// value uses the structured title/description/meta section markers from +// CategoryEnhanceUserTemplate (seed/repair canonical shape). Attributes is not +// required — category prompts no longer carry an attributes role section. func CategoryEnhanceHasRoleSections(prompt string) bool { lower := strings.ToLower(prompt) return strings.Contains(lower, strings.ToLower(SectionTitleStart)) && strings.Contains(lower, strings.ToLower(SectionDescriptionStart)) && - strings.Contains(lower, strings.ToLower(SectionMetaStart)) && - strings.Contains(lower, strings.ToLower(SectionAttributesStart)) + strings.Contains(lower, strings.ToLower(SectionMetaStart)) } diff --git a/apps/api/internal/aiprompts/roles_test.go b/apps/api/internal/aiprompts/roles_test.go index f86bb40..6889b46 100644 --- a/apps/api/internal/aiprompts/roles_test.go +++ b/apps/api/internal/aiprompts/roles_test.go @@ -8,7 +8,7 @@ import ( func TestCategoryEnhanceHasRoleSections(t *testing.T) { t.Parallel() if !CategoryEnhanceHasRoleSections(CategoryEnhanceUserTemplate) { - t.Fatal("canonical CategoryEnhanceUserTemplate must have title/description/meta/attributes sections") + t.Fatal("canonical CategoryEnhanceUserTemplate must have title/description/meta sections") } if CategoryEnhanceHasRoleSections("legacy HTML prompt") { t.Fatal("unstructured prompt must not report role sections") @@ -16,6 +16,18 @@ func TestCategoryEnhanceHasRoleSections(t *testing.T) { if CategoryEnhanceHasRoleSections("--- Title ---\nonly title") { t.Fatal("partial sections must not pass") } + // Old stored prompts with an attributes section still count as sectioned + // (repair rewrites them via CategoryEnhancePromptNeedsRepair). + old := "--- Title ---\nx\n--- Description ---\ny\n--- Meta ---\nz\n--- Attributes ---\na\n--- End Attributes ---" + if !CategoryEnhanceHasRoleSections(old) { + t.Fatal("old attributes-bearing prompt must still report role sections") + } + if !CategoryEnhancePromptNeedsRepair(old) { + t.Fatal("old attributes-bearing prompt must need repair (attributes section retired)") + } + if CategoryEnhancePromptNeedsRepair(CategoryEnhanceUserTemplate) { + t.Fatal("canonical template must not need repair") + } } func TestCategoryPromptRolesOrder(t *testing.T) { @@ -38,14 +50,17 @@ func TestCategoryEnhanceUserTemplateRoleMarkers(t *testing.T) { SectionTitleStart, SectionTitleEnd, SectionDescriptionStart, SectionDescriptionEnd, SectionMetaStart, SectionMetaEnd, - SectionAttributesStart, SectionAttributesEnd, } { if !strings.Contains(tpl, marker) { t.Fatalf("template missing %q", marker) } } + // Attribute prompting is retired from category prompts (pipeline-level only). + if strings.Contains(tpl, SectionAttributesStart) || strings.Contains(strings.ToLower(tpl), "role: attributes") { + t.Fatal("template must not carry an attributes role section") + } lower := strings.ToLower(tpl) - for _, role := range []string{"role: title", "role: description", "role: meta", "role: attributes"} { + for _, role := range []string{"role: title", "role: description", "role: meta"} { if !strings.Contains(lower, role) { t.Fatalf("template missing %q", role) } diff --git a/apps/api/internal/catalog/prompt_repair.go b/apps/api/internal/catalog/prompt_repair.go index 303ca69..7779fdb 100644 --- a/apps/api/internal/catalog/prompt_repair.go +++ b/apps/api/internal/catalog/prompt_repair.go @@ -62,7 +62,7 @@ type RepairA1DemoOptions struct { // RepairA1DemoCategoryEnhancePrompts replaces non-sectioned / legacy combined // categories.prompt values for A1 Slovenija + Platform Demo with role-sectioned -// overlays (Title / Description / Meta / Attributes). Prefers wp_product_categories.sql +// overlays (Title / Description / Meta). Prefers wp_product_categories.sql // (SEED_A1_WP_CATEGORIES / scripts/seed) as source of truth, then a1-category-prompts.json, // splitting combined Name+Description prompts so naming rules land under Title and // HTML body under Description; otherwise fall back to CategoryEnhanceUserTemplate. diff --git a/apps/api/internal/httpapi/catalog_handlers_seo_test.go b/apps/api/internal/httpapi/catalog_handlers_seo_test.go index 825c106..33e6ccb 100644 --- a/apps/api/internal/httpapi/catalog_handlers_seo_test.go +++ b/apps/api/internal/httpapi/catalog_handlers_seo_test.go @@ -42,19 +42,19 @@ func TestStripCatalogSEOMetaIfOmitted(t *testing.T) { other := uuid.MustParse("11111111-1111-1111-1111-111111111111") item := catalogSEOItem() - stripCatalogSEOMetaIfOmitted(processing.CompanyOmitsSEOMetaLookup(other, "", "Acme", false), item) + stripCatalogSEOMetaIfOmitted(processing.CompanyOmitsSEOMetaLookup(other, "", "Acme"), item) if item["meta_title"] != "T" { t.Fatalf("ordinary company must keep meta: %v", item["meta_title"]) } - stripCatalogSEOMetaIfOmitted(processing.CompanyOmitsSEOMetaLookup(a1, "", "", false), item) + stripCatalogSEOMetaIfOmitted(processing.CompanyOmitsSEOMetaLookup(a1, "", ""), item) assertCatalogSEOOmitted(t, item) } func TestStripCatalogSEOMetaIfOmitted_demoByName(t *testing.T) { t.Parallel() other := uuid.MustParse("11111111-1111-1111-1111-111111111111") - omit := processing.CompanyOmitsSEOMetaLookup(other, "", " platform demo ", false) + omit := processing.CompanyOmitsSEOMetaLookup(other, "", " platform demo ") if !omit { t.Fatal("Platform Demo by name must omit SEO meta") } @@ -63,14 +63,12 @@ func TestStripCatalogSEOMetaIfOmitted_demoByName(t *testing.T) { assertCatalogSEOOmitted(t, item) } -func TestStripCatalogSEOMetaIfOmitted_a1PromptMarkers(t *testing.T) { +func TestStripCatalogSEOMetaIfOmitted_sectionedPromptsKeepMeta(t *testing.T) { t.Parallel() + // Role-sectioned category prompts are the canonical prompt-editor shape for + // every tenant now — using them must NOT put a company in the SEO-omit cohort. other := uuid.MustParse("11111111-1111-1111-1111-111111111111") - omit := processing.CompanyOmitsSEOMetaLookup(other, "", "Acme", true) - if !omit { - t.Fatal("A1-style prompt markers must omit SEO meta") + if processing.CompanyOmitsSEOMetaLookup(other, "", "Acme") { + t.Fatal("ordinary company must keep SEO meta regardless of prompt shape") } - item := catalogSEOItem() - stripCatalogSEOMetaIfOmitted(omit, item) - assertCatalogSEOOmitted(t, item) } diff --git a/apps/api/internal/processing/v1_legacy.go b/apps/api/internal/processing/v1_legacy.go index 12a6083..02d5d96 100644 --- a/apps/api/internal/processing/v1_legacy.go +++ b/apps/api/internal/processing/v1_legacy.go @@ -511,19 +511,17 @@ func CompanyOmitsSEOMetaID(companyID uuid.UUID) bool { // CompanyOmitsSEOMetaLookup is the shared A1/Demo SEO-omit decision used by V1 process // and catalog list/get/update. Matches hardcoded companies.id, A1 cohort (legacy id), -// Platform Demo by name, or A1-style --- Title --- / --- Description --- / --- Meta --- -// category prompts. -func CompanyOmitsSEOMetaLookup(companyID uuid.UUID, legacyID, name string, hasA1SectionPrompts bool) bool { +// or Platform Demo by name. Role-sectioned category prompts (--- Title --- / --- Meta ---) +// are the platform-wide canonical prompt shape now, so they no longer imply the A1 +// cohort — sniffing them here made any tenant using the prompt editor lose SEO meta. +func CompanyOmitsSEOMetaLookup(companyID uuid.UUID, legacyID, name string) bool { if CompanyOmitsSEOMetaID(companyID) { return true } if billing.IsA1CohortCompany(legacyID, "") { return true } - if strings.EqualFold(strings.TrimSpace(name), "Platform Demo") { - return true - } - return hasA1SectionPrompts + return strings.EqualFold(strings.TrimSpace(name), "Platform Demo") } // CompanyOmitsSEOMeta is true when catalog/V1 process must omit meta_title / @@ -543,23 +541,7 @@ func CompanyOmitsSEOMeta(ctx context.Context, pool *pgxpool.Pool, companyID uuid if err != nil { return false } - if CompanyOmitsSEOMetaLookup(companyID, legacy, name, false) { - return true - } - var hasA1Prompts bool - err = pool.QueryRow(ctx, ` - SELECT EXISTS ( - SELECT 1 FROM categories - WHERE company_id = $1 - AND prompt ILIKE '%--- Title ---%' - AND prompt ILIKE '%--- Description ---%' - AND prompt ILIKE '%--- Meta ---%' - LIMIT 1 - )`, companyID).Scan(&hasA1Prompts) - if err != nil { - return false - } - return hasA1Prompts + return CompanyOmitsSEOMetaLookup(companyID, legacy, name) } // StripV1SEOMeta removes meta_title / meta_description keys (omit, not null). diff --git a/apps/api/internal/processing/v1_legacy_test.go b/apps/api/internal/processing/v1_legacy_test.go index 620bccb..96360c5 100644 --- a/apps/api/internal/processing/v1_legacy_test.go +++ b/apps/api/internal/processing/v1_legacy_test.go @@ -259,20 +259,18 @@ func TestCompanyOmitsSEOMetaLookup(t *testing.T) { id uuid.UUID legacy string coName string - prompts bool wantOmit bool }{ {name: "ordinary", id: other, coName: "Acme", wantOmit: false}, {name: "a1_uuid", id: a1, wantOmit: true}, {name: "demo_by_name", id: other, coName: "Platform Demo", wantOmit: true}, {name: "demo_by_name_case", id: other, coName: " platform DEMO ", wantOmit: true}, - {name: "a1_prompt_markers", id: other, coName: "Acme", prompts: true, wantOmit: true}, {name: "a1_legacy_id", id: other, legacy: "97e1a309-3d23-4aa2-b518-8e8d7afdfec7", wantOmit: true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got := CompanyOmitsSEOMetaLookup(tc.id, tc.legacy, tc.coName, tc.prompts) + got := CompanyOmitsSEOMetaLookup(tc.id, tc.legacy, tc.coName) if got != tc.wantOmit { t.Fatalf("got %v want %v", got, tc.wantOmit) } diff --git a/apps/web/src/lib/categories/prompt-sections.ts b/apps/web/src/lib/categories/prompt-sections.ts index e76d855..fe46859 100644 --- a/apps/web/src/lib/categories/prompt-sections.ts +++ b/apps/web/src/lib/categories/prompt-sections.ts @@ -6,13 +6,12 @@ * enhance one-shot JSON parse (name, description, meta_*, attrs) keeps working. */ -export type EnhancePromptSectionId = "title" | "description" | "meta" | "attributes"; +export type EnhancePromptSectionId = "title" | "description" | "meta"; export const ENHANCE_PROMPT_SECTIONS: readonly EnhancePromptSectionId[] = [ "title", "description", - "meta", - "attributes" + "meta" ] as const; /** Markers must stay in sync with aiprompts.Section* constants. */ @@ -22,13 +21,22 @@ export const SECTION_MARKERS: Record< > = { title: { start: "--- Title ---", end: "--- End Title ---" }, description: { start: "--- Description ---", end: "--- End Description ---" }, - meta: { start: "--- Meta ---", end: "--- End Meta ---" }, - attributes: { start: "--- Attributes ---", end: "--- End Attributes ---" } + meta: { start: "--- Meta ---", end: "--- End Meta ---" } }; +/** + * Retired attributes section markers (attribute extraction is pipeline-level now). + * Old stored prompts may still carry this section; parse drops it and the next + * save writes the three-section shape. + */ +export const LEGACY_ATTRIBUTES_MARKERS = { + start: "--- Attributes ---", + end: "--- End Attributes ---" +} as const; + /** Canonical shared intro (same idea as CategoryEnhanceUserTemplate preamble). */ export const DEFAULT_ENHANCE_PREAMBLE = - "Write all product text in {{language}} (do not hardcode a language). Keep the title, description, SEO meta, and attributes consistent with the product evidence below."; + "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 = { @@ -36,9 +44,7 @@ export const DEFAULT_SECTION_BODIES: Record = { "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 (

  • ).\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.", - attributes: - "Fill product attributes as key/value pairs. Prefer allowed attribute keys and title-formula slots; remap near-miss labels onto those keys; fill missing keys only from name, description, category, and attributes evidence; never invent specs or dimensions; omit unknown keys.\nCategory: {{category}}\nAttrs: {{attrs}}" + 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 = { @@ -65,16 +71,11 @@ export const SECTION_SCHEMA_HINTS: Record { it("detects role section markers", () => { assert.equal(hasEnhanceRoleSections(sample), true); assert.equal(hasEnhanceRoleSections("plain marketing blob"), false); + // Old attributes-bearing prompts still count as structured. + assert.equal(hasEnhanceRoleSections(legacyWithAttributes), true); }); it("parses and round-trips structured prompts", () => { const parsed = parseEnhancePrompt(sample); assert.equal(parsed.hasRoleSections, true); - assert.match(parsed.preamble, /meta_title/); + assert.equal(parsed.preamble, DEFAULT_ENHANCE_PREAMBLE); assert.equal(parsed.sections.title, "Title body with {{name}}"); assert.equal(parsed.sections.description, "Desc body with {{description}}"); assert.equal(parsed.sections.meta, "Meta body"); - assert.equal(parsed.sections.attributes, "Attrs body {{attrs}}"); const again = composeEnhancePrompt(parsed.preamble, parsed.sections); assert.equal(hasEnhanceRoleSections(again), true); const reparsed = parseEnhancePrompt(again); assert.equal(reparsed.sections.title, parsed.sections.title); - assert.equal(reparsed.sections.attributes, parsed.sections.attributes); + assert.equal(reparsed.sections.meta, parsed.sections.meta); + }); + + it("drops retired attributes sections on re-save", () => { + const parsed = parseEnhancePrompt(legacyWithAttributes); + assert.equal(parsed.sections.title, "Title body with {{name}}"); + assert.equal(parsed.sections.meta, "Meta body"); + const again = composeEnhancePrompt(parsed.preamble, parsed.sections); + assert.equal(again.includes(LEGACY_ATTRIBUTES_MARKERS.start), false); + assert.equal(again.includes("Attrs body"), false); }); it("keeps unstructured prompts in description", () => { @@ -63,13 +79,12 @@ describe("category prompt sections", () => { const out = composeEnhancePrompt(DEFAULT_ENHANCE_PREAMBLE, { title: "Custom title rules", description: "", - meta: "", - attributes: "" + meta: "" }); assert.equal(hasEnhanceRoleSections(out), true); assert.match(out, /Custom title rules/); - assert.match(out, /Role: description/); - assert.match(out, /\{\{attrs\}\}/); + assert.match(out, /product description as HTML/); + assert.equal(out.includes(LEGACY_ATTRIBUTES_MARKERS.start), false); }); it("isEnhancePromptEmpty ignores default preamble", () => { @@ -79,4 +94,4 @@ describe("category prompt sections", () => { false ); }); -}); \ No newline at end of file +}); diff --git a/apps/web/src/routes/categories/[categoryId]/prompt/+page.svelte b/apps/web/src/routes/categories/[categoryId]/prompt/+page.svelte index ae0555b..347496e 100644 --- a/apps/web/src/routes/categories/[categoryId]/prompt/+page.svelte +++ b/apps/web/src/routes/categories/[categoryId]/prompt/+page.svelte @@ -35,7 +35,6 @@ Search, Share2, Sparkles, - Tags, Type, X } from "@lucide/svelte"; @@ -77,11 +76,6 @@ labelKey: "categories.promptSection.meta", helpKey: "categories.promptSection.metaHelp", formulaNoteKey: "categories.promptSection.metaFormulaNote" - }, - attributes: { - labelKey: "categories.promptSection.attributes", - helpKey: "categories.promptSection.attributesHelp", - formulaNoteKey: "categories.promptSection.attributesFormulaNote" } }; @@ -105,8 +99,7 @@ let sectionBodies = $state>({ title: "", description: "", - meta: "", - attributes: "" + meta: "" }); let unstructuredHint = $state(false); @@ -245,7 +238,7 @@ } function clearLanguageOverride() { - sectionBodies = { title: "", description: "", meta: "", attributes: "" }; + sectionBodies = { title: "", description: "", meta: "" }; unstructuredHint = false; const next = { ...promptsByLang }; delete next[selectedLang]; @@ -431,10 +424,8 @@ {:else if id === "description"} - {:else if id === "meta"} - {:else} - + {/if} {i18n.t(meta.labelKey)} {#if sectionFilled(id)}