fix: deliver validated AI insight responses

This commit is contained in:
2026-08-22 15:47:38 +08:00
parent ca55c2fb3d
commit 3fba4261ee
68 changed files with 6870 additions and 493 deletions

View File

@@ -40,8 +40,13 @@ const (
type Event struct {
ID string `json:"id"`
OccasionKey string `json:"occasionKey,omitempty"`
Variant string `json:"variant,omitempty"`
TitleFA string `json:"titleFa"`
TitleEN string `json:"titleEn,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Themes []string `json:"themes,omitempty"`
CampaignTags []string `json:"campaignTags,omitempty"`
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
Recurrence Recurrence `json:"recurrence,omitempty"`
@@ -60,6 +65,7 @@ type Event struct {
LagDays int `json:"lagDays"`
RelevanceBPS int `json:"relevanceBps"`
ConfidenceBPS int `json:"confidenceBps"`
ImportanceBPS int `json:"importanceBps,omitempty"`
}
func ResolveRecurrence(rule Recurrence, year int, location *time.Location) (time.Time, error) {
@@ -79,6 +85,7 @@ func ResolveRecurrence(rule Recurrence, year int, location *time.Location) (time
func ValidateEvents(events []Event) error {
seen := make(map[string]struct{}, len(events))
canonical := make(map[string]struct{}, len(events))
for _, event := range events {
if strings.TrimSpace(event.ID) == "" || strings.TrimSpace(event.TitleFA) == "" || strings.TrimSpace(event.SourceURI) == "" || strings.TrimSpace(event.SourceVersion) == "" || strings.TrimSpace(event.InclusionRationale) == "" || strings.TrimSpace(event.GeographicScope) == "" {
return fmt.Errorf("event catalog: event %q is incomplete", event.ID)
@@ -87,6 +94,15 @@ func ValidateEvents(events []Event) error {
return fmt.Errorf("event catalog: duplicate event %q", event.ID)
}
seen[event.ID] = struct{}{}
if event.OccasionKey == "" {
event.OccasionKey = event.ID
}
if event.Variant == "" {
event.Variant = "PRIMARY"
}
if event.Variant != "PRIMARY" && event.Variant != "CAMPAIGN_WINDOW" && event.Variant != "OPERATIONAL_WINDOW" {
return fmt.Errorf("event catalog: event %q has invalid variant", event.ID)
}
start, err := time.Parse(time.DateOnly, event.StartDate)
if err != nil {
return fmt.Errorf("event catalog: event %q start date: %w", event.ID, err)
@@ -101,7 +117,7 @@ func ValidateEvents(events []Event) error {
if (event.Tone == ToneMourning || event.Tone == ToneMemorial) && event.CommercialPosture == PosturePromote {
return fmt.Errorf("event catalog: event %q cannot promote during mourning or memorial", event.ID)
}
if event.LeadDays < 0 || event.LagDays < 0 || event.RelevanceBPS < 0 || event.RelevanceBPS > 10_000 || event.ConfidenceBPS < 0 || event.ConfidenceBPS > 10_000 {
if event.LeadDays < 0 || event.LagDays < 0 || event.RelevanceBPS < 0 || event.RelevanceBPS > 10_000 || event.ConfidenceBPS < 0 || event.ConfidenceBPS > 10_000 || event.ImportanceBPS < 0 || event.ImportanceBPS > 10_000 {
return fmt.Errorf("event catalog: event %q has invalid impact metadata", event.ID)
}
if event.LeadDays > 90 || event.LagDays > 90 {
@@ -116,6 +132,11 @@ func ValidateEvents(events []Event) error {
return fmt.Errorf("event catalog: event %q does not match its resolved recurrence", event.ID)
}
}
canonicalKey := strings.Join([]string{event.OccasionKey, event.Variant, event.StartDate, event.EndDate}, "\x00")
if _, exists := canonical[canonicalKey]; exists {
return fmt.Errorf("event catalog: duplicate canonical occurrence %q", event.ID)
}
canonical[canonicalKey] = struct{}{}
}
return nil
}

View File

@@ -0,0 +1,44 @@
package eventcatalog
import (
"encoding/json"
"os"
"testing"
"time"
)
func TestImpactfulBusinessCatalogIsValidAndIndustryPersonalized(t *testing.T) {
data, err := os.ReadFile("../../catalogs/impactful-business-events-1405-v1.json")
if err != nil {
t.Fatal(err)
}
var input Import
if err := json.Unmarshal(data, &input); err != nil {
t.Fatal(err)
}
if err := ValidateImport(input, 1405); err != nil {
t.Fatal(err)
}
hasEducation, hasRetail := false, false
months := make(map[time.Month]bool)
for _, event := range input.Events {
if event.OfficialHoliday && event.RelevanceBPS < 7000 {
t.Fatalf("official holiday %s lacks evidenced business impact", event.ID)
}
for _, category := range event.MerchantCategories {
hasEducation = hasEducation || category == "مراکز آموزشی مجازی"
hasRetail = hasRetail || category == "کیف و کفش فروشی"
}
start, err := time.Parse(time.DateOnly, event.StartDate)
if err != nil {
t.Fatal(err)
}
months[start.Month()] = true
}
if !hasEducation || !hasRetail {
t.Fatal("catalog must include both education- and retail-specific windows")
}
if len(input.Events) < 12 || len(months) < 9 {
t.Fatalf("catalog must provide year-round candidate coverage; got %d events across %d months", len(input.Events), len(months))
}
}

View File

@@ -62,6 +62,12 @@ func (r *Repository) ImportDraft(ctx context.Context, input Import) error {
return fmt.Errorf("creating calendar catalog version: %w", err)
}
for _, event := range input.Events {
if event.OccasionKey == "" {
event.OccasionKey = event.ID
}
if event.Variant == "" {
event.Variant = "PRIMARY"
}
categories, err := json.Marshal(event.MerchantCategories)
if err != nil {
return fmt.Errorf("encoding calendar event categories: %w", err)
@@ -70,8 +76,20 @@ func (r *Repository) ImportDraft(ctx context.Context, input Import) error {
if err != nil {
return fmt.Errorf("encoding calendar event channels: %w", err)
}
_, err = tx.Exec(ctx, `INSERT INTO calendar_events(catalog_version_id,event_key,title_fa,title_en,start_date,end_date,recurrence_rule,event_type,tone,commercial_posture,official_holiday,source_uri,source_version,source_checksum,inclusion_rationale,geographic_scope,merchant_categories,behavioral_channels,lead_days,lag_days,relevance_bps,confidence_bps)
VALUES($1,$2,$3,nullif($4,''),$5,$6,nullif($7,''),$8,$9,$10,$11,$12,$13,nullif($14,''),$15,$16,$17,$18,$19,$20,$21,$22)`, versionID, event.ID, event.TitleFA, event.TitleEN, event.StartDate, event.EndDate, event.Recurrence, event.Type, event.Tone, event.CommercialPosture, event.OfficialHoliday, event.SourceURI, event.SourceVersion, event.SourceChecksum, event.InclusionRationale, event.GeographicScope, categories, channels, event.LeadDays, event.LagDays, event.RelevanceBPS, event.ConfidenceBPS)
aliases, err := json.Marshal(event.Aliases)
if err != nil {
return fmt.Errorf("encoding calendar event aliases: %w", err)
}
themes, err := json.Marshal(event.Themes)
if err != nil {
return fmt.Errorf("encoding calendar event themes: %w", err)
}
tags, err := json.Marshal(event.CampaignTags)
if err != nil {
return fmt.Errorf("encoding calendar event campaign tags: %w", err)
}
_, err = tx.Exec(ctx, `INSERT INTO calendar_events(catalog_version_id,event_key,occasion_key,event_variant,title_fa,title_en,aliases,themes,campaign_tags,start_date,end_date,recurrence_rule,event_type,tone,commercial_posture,official_holiday,source_uri,source_version,source_checksum,inclusion_rationale,geographic_scope,merchant_categories,behavioral_channels,lead_days,lag_days,relevance_bps,confidence_bps,importance_bps)
VALUES($1,$2,$3,$4,$5,nullif($6,''),$7,$8,$9,$10,$11,nullif($12,''),$13,$14,$15,$16,$17,$18,nullif($19,''),$20,$21,$22,$23,$24,$25,$26,$27,$28)`, versionID, event.ID, event.OccasionKey, event.Variant, event.TitleFA, event.TitleEN, aliases, themes, tags, event.StartDate, event.EndDate, event.Recurrence, event.Type, event.Tone, event.CommercialPosture, event.OfficialHoliday, event.SourceURI, event.SourceVersion, event.SourceChecksum, event.InclusionRationale, event.GeographicScope, categories, channels, event.LeadDays, event.LagDays, event.RelevanceBPS, event.ConfidenceBPS, event.ImportanceBPS)
if err != nil {
return fmt.Errorf("inserting calendar event %q: %w", event.ID, err)
}