package processing import ( "crypto/sha256" "encoding/hex" "encoding/json" "github.com/descrybe/descrybe-v2/apps/api/internal/company" ) // FieldEnhanceInputHash is stored on processed_products.field_sources so re-runs // can skip the LLM when enhance inputs are unchanged (mirrors feed content_hash). const FieldEnhanceInputHash = "enhance_input_hash" // enhanceInputHashVersion bumps when the enhance prompt/schema changes so prior // hashes are invalidated and products re-enhance once. const enhanceInputHashVersion = "3" // HashEnhanceInput returns a stable hex SHA-256 of the inputs that feed the // enhance LLM (same compaction as ProductEnhanceUser / CompactBrandPrompt). // Empty inputs still produce a deterministic hash. func HashEnhanceInput(category, name, description, brandPrompt, language, promptSystem, promptUser string, attrs map[string]any) string { langCode, err := company.ParseLanguage(language, true) if err != nil { langCode = company.DefaultLanguage } payload := map[string]any{ "v": enhanceInputHashVersion, "category": SanitizeText(category), "name": SanitizeText(truncateRunes(name, 200)), "description": SanitizeText(truncateRunes(description, MaxProductDescRunes)), "brand_prompt": CompactBrandPrompt(brandPrompt), "language": langCode, "prompt_system": SanitizeText(truncateRunes(promptSystem, 4000)), "prompt_user": SanitizeText(truncateRunes(promptUser, 4000)), "attrs": CompactAttrs(attrs, MaxAttrKeys), } b, err := json.Marshal(payload) if err != nil { // Unreachable for map[string]any of strings/scalars; fall back so callers // never skip LLM on a broken hash. sum := sha256.Sum256([]byte(category + "\x00" + name + "\x00" + description)) return hex.EncodeToString(sum[:]) } sum := sha256.Sum256(b) return hex.EncodeToString(sum[:]) } func enhanceHashFromMeta(raw any) string { m, ok := raw.(map[string]any) if !ok { return "" } h, _ := m["input_hash"].(string) return h } func enhanceStatusFromMeta(raw any) string { m, ok := raw.(map[string]any) if !ok { return "" } s, _ := m["status"].(string) return s }