Files
descrybe/apps/api/internal/aiprompts/description_formula.go
T
greeneclipseandClaude Fable 5 b595398389 Split A1 category prompts canonically; retire attributes prompting
- Category enhance prompts (Sync A1 / seed-a1 / repair) now use the same
  three role sections as the rest of the platform: Title / Description /
  Meta. The retired "--- Attributes ---" role section is removed from the
  canonical template, the legacy-split overlay, and the web prompt editor;
  Category/Attrs stay as plain product-context lines (attribute extraction
  remains pipeline-level via AppendAttributeConstraints).
- Stored prompts still carrying an attributes section are detected by
  CategoryEnhancePromptNeedsRepair and rewritten on the next Sync.
- Legacy wp_product_categories.sql splits now also seed a default Slovenian
  metaTitle rule (dumps only carried <metaDescription>), so every A1
  category gets all four prompt areas: title formula, description
  sections, meta title, meta description.
- Role-sectioned prompts no longer imply the A1 SEO-omit cohort
  (CompanyOmitsSEOMeta): sectioned prompts are the canonical prompt-editor
  output for every tenant, and the sniff silently disabled SEO meta for
  any company that saved a category prompt.
- Verified locally: repair-category-prompts -apply updated 238 A1 +
  Platform Demo categories from scripts/seed/wp_product_categories.sql
  (216 legacy splits), idempotent on re-run, zero attributes sections
  left in DB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 00:13:44 +02:00

163 lines
5.2 KiB
Go

package aiprompts
import (
"encoding/json"
"regexp"
"strings"
)
var (
reLegacyHTMLBlock = regexp.MustCompile(`(?is)<\s*(h[1-4]|p|ul|ol|li|b|strong)\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*(?:h[1-4]|p|ul|ol|li|b|strong)\s*>`)
reLegacyBoldUL = regexp.MustCompile(`(?is)<\s*b\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*b\s*>\s*<\s*ul\s*>\s*<\s*li\s*>\s*\{?\s*(.*?)\s*\}?\s*<\s*/\s*li\s*>\s*<\s*/\s*ul\s*>`)
)
// DescriptionFormulaSection is one ordered block in categories.description_template.
type DescriptionFormulaSection struct {
Type string `json:"type"`
Instructions string `json:"instructions"`
}
// DescriptionFormula is the JSON shape stored in categories.description_template
// (sections + optional SEO meta instruction strings).
type DescriptionFormula struct {
Sections []DescriptionFormulaSection `json:"sections,omitempty"`
MetaTitle string `json:"metaTitle,omitempty"`
MetaDescription string `json:"metaDescription,omitempty"`
}
// DescriptionTemplateNeedsRepair is true when template is missing or has no sections
// (and no usable meta instructions).
func DescriptionTemplateNeedsRepair(template any) bool {
f, ok := parseDescriptionFormula(template)
if !ok {
return true
}
if len(f.Sections) > 0 {
return false
}
return strings.TrimSpace(f.MetaTitle) == "" && strings.TrimSpace(f.MetaDescription) == ""
}
// DefaultLegacyMetaTitleRule fills description_template.metaTitle for legacy A1
// prompts, which only carried a <metaDescription> 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 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
}
// Walk left-to-right so section order matches the legacy HTML template.
remaining := body
for remaining != "" {
boldUL := reLegacyBoldUL.FindStringSubmatchIndex(remaining)
block := reLegacyHTMLBlock.FindStringSubmatchIndex(remaining)
switch {
case boldUL != nil && (block == nil || boldUL[0] <= block[0]):
m := reLegacyBoldUL.FindStringSubmatch(remaining[boldUL[0]:boldUL[1]])
heading, instr := "", ""
if len(m) >= 3 {
heading = cleanLegacyInstruction(m[1])
instr = cleanLegacyInstruction(m[2])
}
if heading != "" && instr != "" && !strings.Contains(strings.ToLower(instr), strings.ToLower(heading)) {
instr = heading + ": " + instr
} else if instr == "" {
instr = heading
}
if strings.TrimSpace(instr) != "" {
out.Sections = append(out.Sections, DescriptionFormulaSection{Type: "ul", Instructions: instr})
}
remaining = remaining[boldUL[1]:]
case block != nil:
m := reLegacyHTMLBlock.FindStringSubmatch(remaining[block[0]:block[1]])
if len(m) >= 3 {
typ := strings.ToLower(strings.TrimSpace(m[1]))
instr := cleanLegacyInstruction(m[2])
if instr != "" {
switch typ {
case "b", "strong":
typ = "h2"
case "li", "ol":
typ = "ul"
}
out.Sections = append(out.Sections, DescriptionFormulaSection{Type: typ, Instructions: instr})
}
}
remaining = remaining[block[1]:]
default:
return out
}
}
return out
}
// DeriveDescriptionTemplateJSON encodes DeriveDescriptionFormulaFromLegacyParts for DB write.
func DeriveDescriptionTemplateJSON(parts LegacyEnhanceParts) string {
f := DeriveDescriptionFormulaFromLegacyParts(parts)
if len(f.Sections) == 0 && strings.TrimSpace(f.MetaTitle) == "" && strings.TrimSpace(f.MetaDescription) == "" {
return ""
}
b, err := json.Marshal(f)
if err != nil {
return ""
}
return string(b)
}
func parseDescriptionFormula(template any) (DescriptionFormula, bool) {
if template == nil {
return DescriptionFormula{}, false
}
switch t := template.(type) {
case DescriptionFormula:
return t, true
case *DescriptionFormula:
if t == nil {
return DescriptionFormula{}, false
}
return *t, true
case []byte:
if len(t) == 0 {
return DescriptionFormula{}, false
}
var f DescriptionFormula
if err := json.Unmarshal(t, &f); err != nil {
return DescriptionFormula{}, false
}
return f, true
case string:
s := strings.TrimSpace(t)
if s == "" || s == "null" || s == "{}" {
return DescriptionFormula{}, false
}
var f DescriptionFormula
if err := json.Unmarshal([]byte(s), &f); err != nil {
return DescriptionFormula{}, false
}
return f, true
default:
b, err := json.Marshal(t)
if err != nil {
return DescriptionFormula{}, false
}
var f DescriptionFormula
if err := json.Unmarshal(b, &f); err != nil {
return DescriptionFormula{}, false
}
return f, true
}
}