2026-08-17 00:39:25 +02:00
package aiprompts
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
var (
reQuotedNameSlot = regexp . MustCompile ( `"([^"]+)"` )
2026-08-23 20:49:40 +02:00
// Slovenian legacy ("… po formuli: … Ne uporabljaj vejic.") and the English
// platform-default translation ("… using this formula: … Do not use commas.").
reNameFormulaTail = regexp . MustCompile ( `(?is)formuli\s*:\s*(.*?)(?:\.?\s*Ne uporabljaj|\.?$)` )
reNameFormulaTailEN = regexp . MustCompile ( `(?is)formula\s*:\s*(.*?)(?:\.?\s*Do not use|\.?$)` )
2026-08-17 00:39:25 +02:00
)
// TitleFormulaElement is one ordered slot in categories.title_template.
type TitleFormulaElement struct {
ID string `json:"id"`
Type string `json:"type"` // "text" | "variable"
Label string `json:"label,omitempty"`
Value string `json:"value"`
Description string `json:"description,omitempty"`
Example string `json:"example,omitempty"`
}
// TitleFormula is the JSON shape stored in categories.title_template.
type TitleFormula struct {
Separator string `json:"separator"`
Elements [] TitleFormulaElement `json:"elements"`
}
// DefaultRetailTitleFormula matches the dominant old A1/PHP <name> pattern:
// product type + brand + full model (never brand alone).
func DefaultRetailTitleFormula () TitleFormula {
return TitleFormula {
Separator : " " ,
Elements : [] TitleFormulaElement {
{ ID : "0-variable-product_type" , Type : "variable" , Label : "Product type" , Value : "product_type" , Description : "Specific product type (sentence case)" , Example : "Gaming monitor" },
{ ID : "1-variable-brand" , Type : "variable" , Label : "Brand" , Value : "brand" , Description : "Brand with correct capitalization" , Example : "Samsung" },
{ ID : "2-variable-product_model" , Type : "variable" , Label : "Model" , Value : "product_model" , Description : "Full product model / ID" , Example : "Odyssey G5" },
},
}
}
// TitleRoleInstruction is the shared Title-section body (CategoryEnhanceUserTemplate
// and BuildCategoryEnhanceOverlay). Drives enhance JSON "name"; never brand-only.
const TitleRoleInstruction = `Role: title. Build JSON "name": short retail title from Title formula + Attrs — never brand-only. Include product type and full model when evidence exists; follow any Title formula constraints that follow; use Attrs.`
// TitleTemplateIsBrandOnly reports stubs that only encode brand (the migrated A1
// default) so enhance collapses to brand-only names.
func TitleTemplateIsBrandOnly ( template any ) bool {
els , ok := titleFormulaElements ( template )
if ! ok || len ( els ) == 0 {
return false
}
vars := 0
brandOnly := true
for _ , el := range els {
switch strings . ToLower ( strings . TrimSpace ( el . Type )) {
case "variable" :
vars ++
v := strings . ToLower ( strings . TrimSpace ( el . Value ))
v = strings . ReplaceAll ( v , "-" , "_" )
if v != "brand" && v != "znamka" {
brandOnly = false
}
case "text" :
// Free-form naming-rule blobs are not brand-only stubs.
if strings . TrimSpace ( el . Value ) != "" {
brandOnly = false
}
}
}
return vars > 0 && brandOnly
}
// TitleTemplateNeedsRepair is true when template is missing, unparseable, or
// brand-only (insufficient vs old name/title rules).
func TitleTemplateNeedsRepair ( template any ) bool {
els , ok := titleFormulaElements ( template )
if ! ok || len ( els ) == 0 {
return true
}
return TitleTemplateIsBrandOnly ( template )
}
// DeriveTitleFormulaFromLegacyNameRules turns PHP/A1 <name> formula text
// (e.g. "tip izdelka …", "znamka …", "poln model …") into a structured
// title_template. Falls back to DefaultRetailTitleFormula when parsing yields
// fewer than two variable slots.
func DeriveTitleFormulaFromLegacyNameRules ( titleRules string ) TitleFormula {
slots := extractLegacyNameSlots ( titleRules )
out := TitleFormula { Separator : " " , Elements : make ([] TitleFormulaElement , 0 , len ( slots ))}
seen := map [ string ] struct {}{}
for _ , slot := range slots {
el , ok := mapLegacyNameSlot ( slot )
if ! ok {
continue
}
key := el . Type + ":" + strings . ToLower ( el . Value )
if _ , dup := seen [ key ]; dup {
continue
}
seen [ key ] = struct {}{}
el . ID = fmt . Sprintf ( "%d-%s-%s" , len ( out . Elements ), el . Type , sanitizeFormulaID ( el . Value ))
out . Elements = append ( out . Elements , el )
}
if countFormulaVariables ( out ) < 2 {
return ensureRetailTitleCoverage ( out )
}
return ensureRetailTitleCoverage ( out )
}
// DeriveTitleTemplateJSON is the JSON encoding used when repairing categories.title_template.
func DeriveTitleTemplateJSON ( titleRules string ) string {
f := DeriveTitleFormulaFromLegacyNameRules ( titleRules )
b , err := json . Marshal ( f )
if err != nil {
def := DefaultRetailTitleFormula ()
b , _ = json . Marshal ( def )
}
return string ( b )
}
func titleFormulaElements ( template any ) ([] TitleFormulaElement , bool ) {
if template == nil {
return nil , false
}
switch t := template .( type ) {
case TitleFormula :
return t . Elements , len ( t . Elements ) > 0
case * TitleFormula :
if t == nil {
return nil , false
}
return t . Elements , len ( t . Elements ) > 0
case [] byte :
if len ( t ) == 0 {
return nil , false
}
var f TitleFormula
if err := json . Unmarshal ( t , & f ); err != nil {
return nil , false
}
return f . Elements , len ( f . Elements ) > 0
case string :
s := strings . TrimSpace ( t )
if s == "" || s == "null" || s == "{}" {
return nil , false
}
var f TitleFormula
if err := json . Unmarshal ([] byte ( s ), & f ); err != nil {
return nil , false
}
return f . Elements , len ( f . Elements ) > 0
case map [ string ] any :
b , err := json . Marshal ( t )
if err != nil {
return nil , false
}
var f TitleFormula
if err := json . Unmarshal ( b , & f ); err != nil {
return nil , false
}
return f . Elements , len ( f . Elements ) > 0
default :
b , err := json . Marshal ( t )
if err != nil {
return nil , false
}
var f TitleFormula
if err := json . Unmarshal ( b , & f ); err != nil {
return nil , false
}
return f . Elements , len ( f . Elements ) > 0
}
}
func extractLegacyNameSlots ( rules string ) [] string {
rules = strings . TrimSpace ( rules )
if rules == "" {
return nil
}
body := rules
if m := reNameFormulaTail . FindStringSubmatch ( rules ); len ( m ) == 2 {
body = m [ 1 ]
2026-08-23 20:49:40 +02:00
} else if m := reNameFormulaTailEN . FindStringSubmatch ( rules ); len ( m ) == 2 {
body = m [ 1 ]
2026-08-17 00:39:25 +02:00
}
raw := reQuotedNameSlot . FindAllStringSubmatch ( body , - 1 )
out := make ([] string , 0 , len ( raw ))
for _ , m := range raw {
s := strings . TrimSpace ( m [ 1 ])
s = strings . Trim ( s , `",. ` )
if s == "" || s == "," {
continue
}
out = append ( out , s )
}
return out
}
func mapLegacyNameSlot ( slot string ) ( TitleFormulaElement , bool ) {
s := strings . TrimSpace ( slot )
if s == "" {
return TitleFormulaElement {}, false
}
low := strings . ToLower ( s )
2026-08-23 20:49:40 +02:00
// English platform-default slots (defaults/default-category-prompts.json) are
// matched before the Slovenian legacy vocabulary. Slovenian rules below are
// deliberately unchanged: A1's derived templates are already stored, and adding
// keywords there would churn them on every repair pass.
switch {
case strings . Contains ( low , "product model" ), strings . Contains ( low , "device model" ),
strings . Contains ( low , "model of the device" ):
return TitleFormulaElement { Type : "variable" , Label : "Model" , Value : "product_model" , Description : s , Example : "EHT6020" }, true
case strings . Contains ( low , "brand" ):
return TitleFormulaElement { Type : "variable" , Label : "Brand" , Value : "brand" , Description : s , Example : "Samsung" }, true
case strings . Contains ( low , "product type" ):
return TitleFormulaElement { Type : "variable" , Label : "Product type" , Value : "product_type" , Description : s , Example : "Bluetooth speaker" }, true
case strings . Contains ( low , "graphics card" ):
return TitleFormulaElement { Type : "variable" , Label : "GPU" , Value : "graficna_kartica" , Description : s }, true
case strings . Contains ( low , "processor" ):
return TitleFormulaElement { Type : "variable" , Label : "CPU" , Value : "procesor" , Description : s }, true
case strings . Contains ( low , "memory" ):
return TitleFormulaElement { Type : "variable" , Label : "Memory" , Value : "kapaciteta_ram_pomnilnika" , Description : s }, true
case strings . Contains ( low , "screen diagonal" ), strings . Contains ( low , "screen size" ):
return TitleFormulaElement { Type : "variable" , Label : "Diagonal" , Value : "diagonala_zaslona" , Description : s }, true
case strings . Contains ( low , "refresh rate" ):
return TitleFormulaElement { Type : "variable" , Label : "Refresh rate" , Value : "refresh_rate" , Description : s }, true
case strings . Contains ( low , "weight group" ):
return TitleFormulaElement { Type : "variable" , Label : "Weight group" , Value : "weight_group" , Description : s }, true
case strings . Contains ( low , "wash capacity" ), strings . Contains ( low , "capacity" ):
return TitleFormulaElement { Type : "variable" , Label : "Capacity" , Value : "kapaciteta" , Description : s }, true
case strings . Contains ( low , "connector type" ), strings . Contains ( low , "socket type" ),
strings . Contains ( low , "cable type" ):
return TitleFormulaElement { Type : "variable" , Label : "Connector" , Value : "connector" , Description : s }, true
case strings . Contains ( low , "colour" ), strings . Contains ( low , "color" ):
return TitleFormulaElement { Type : "variable" , Label : "Color" , Value : "barva" , Description : s }, true
case strings . Contains ( low , "dimensions" ):
return TitleFormulaElement { Type : "variable" , Label : "Dimensions" , Value : "dimensions" , Description : s }, true
}
2026-08-17 00:39:25 +02:00
switch {
case strings . Contains ( low , "znamka" ):
return TitleFormulaElement { Type : "variable" , Label : "Brand" , Value : "brand" , Description : s , Example : "Samsung" }, true
case strings . Contains ( low , "poln model" ), strings . Contains ( low , "model izdelka" ),
strings . HasPrefix ( low , "model " ), low == "model" , strings . Contains ( low , "model uppercase" ):
return TitleFormulaElement { Type : "variable" , Label : "Model" , Value : "product_model" , Description : s , Example : "EHT6020" }, true
case strings . Contains ( low , "tip izdelka" ):
return TitleFormulaElement { Type : "variable" , Label : "Product type" , Value : "product_type" , Description : s , Example : "Bluetooth speaker" }, true
case strings . HasPrefix ( low , "barva" ):
return TitleFormulaElement { Type : "variable" , Label : "Color" , Value : "barva" , Description : s }, true
case strings . Contains ( low , "dimenzij" ):
return TitleFormulaElement { Type : "variable" , Label : "Dimensions" , Value : "dimensions" , Description : s }, true
case strings . Contains ( low , "diagonala" ):
return TitleFormulaElement { Type : "variable" , Label : "Diagonal" , Value : "diagonala_zaslona" , Description : s }, true
case strings . Contains ( low , "pomnilnik" ):
return TitleFormulaElement { Type : "variable" , Label : "Memory" , Value : "kapaciteta_ram_pomnilnika" , Description : s }, true
case strings . Contains ( low , "procesor" ):
return TitleFormulaElement { Type : "variable" , Label : "CPU" , Value : "procesor" , Description : s }, true
case strings . Contains ( low , "grafi" ):
return TitleFormulaElement { Type : "variable" , Label : "GPU" , Value : "graficna_kartica" , Description : s }, true
case strings . Contains ( low , "kapaciteta" ):
return TitleFormulaElement { Type : "variable" , Label : "Capacity" , Value : "kapaciteta" , Description : s }, true
case strings . Contains ( low , "skupina po te" ) || strings . Contains ( low , "teži" ) || strings . Contains ( low , "tezi" ):
return TitleFormulaElement { Type : "variable" , Label : "Weight group" , Value : "weight_group" , Description : s }, true
case strings . Contains ( low , "priklju" ):
return TitleFormulaElement { Type : "variable" , Label : "Connector" , Value : "connector" , Description : s }, true
case strings . Contains ( low , "osvež" ) || strings . Contains ( low , "osvez" ) || strings . Contains ( low , "hz" ):
return TitleFormulaElement { Type : "variable" , Label : "Refresh rate" , Value : "refresh_rate" , Description : s }, true
case strings . Contains ( low , "lowercase" ) || strings . Contains ( low , "sentence case" ) || strings . Contains ( low , "uppercase" ):
// Category-specific type word ("cvrtnik lowercase", "monitor lowercase").
return TitleFormulaElement { Type : "variable" , Label : "Product type" , Value : "product_type" , Description : s }, true
default :
// Keep unrecognized instructional slots as text so order/intent survive.
if len ([] rune ( s )) > 80 {
s = string ([] rune ( s )[: 80 ])
}
return TitleFormulaElement { Type : "text" , Label : "Naming detail" , Value : s , Description : "Legacy name-formula slot" }, true
}
}
func countFormulaVariables ( f TitleFormula ) int {
n := 0
for _ , el := range f . Elements {
if el . Type == "variable" {
n ++
}
}
return n
}
func ensureRetailTitleCoverage ( f TitleFormula ) TitleFormula {
has := map [ string ] bool {}
for _ , el := range f . Elements {
if el . Type != "variable" {
continue
}
k := strings . ToLower ( strings . ReplaceAll ( strings . TrimSpace ( el . Value ), "-" , "_" ))
has [ k ] = true
}
def := DefaultRetailTitleFormula ()
for _ , el := range def . Elements {
k := strings . ToLower ( el . Value )
if has [ k ] {
continue
}
// Only inject core retail slots when missing.
if k == "brand" || k == "product_model" || ( k == "product_type" && countFormulaVariables ( f ) < 2 ) {
el . ID = fmt . Sprintf ( "%d-%s-%s" , len ( f . Elements ), el . Type , el . Value )
f . Elements = append ( f . Elements , el )
has [ k ] = true
}
}
if countFormulaVariables ( f ) < 2 {
return def
}
if f . Separator == "" {
f . Separator = " "
}
return f
}
func sanitizeFormulaID ( v string ) string {
v = strings . ToLower ( strings . TrimSpace ( v ))
v = strings . ReplaceAll ( v , " " , "_" )
v = strings . ReplaceAll ( v , "-" , "_" )
if v == "" {
return "slot"
}
if len ( v ) > 40 {
v = v [: 40 ]
}
return v
}