package intelligence import ( "path/filepath" "time" ) func buildRegressionReport(assets []loadedAsset, baselineDir string) RegressionReport { report := RegressionReport{ Version: "3.0", GeneratedAt: time.Now().UTC().Format(time.RFC3339), Status: "skipped", BaselineDir: baselineDir, Summary: map[string]interface{}{}, } if baselineDir == "" { report.Summary["reason"] = "baseline-dir not provided" return report } baseline, err := loadAssets(baselineDir) if err != nil { report.Summary["reason"] = err.Error() return report } bySKU := map[string]loadedAsset{} for _, item := range baseline { bySKU[item.Asset.SKU] = item } var drifted int for _, current := range assets { old, ok := bySKU[current.Asset.SKU] if !ok { continue } item := compareAssets(old, current) if item.Status != "stable" { drifted++ } report.Items = append(report.Items, item) report.Compared++ } report.Status = "complete" report.Summary["drifted"] = drifted report.Summary["stable"] = report.Compared - drifted return report } func compareAssets(old, current loadedAsset) RegressionItem { brightnessDelta := abs(current.Asset.Visual.Brightness - old.Asset.Visual.Brightness) histDistance := histogramDistance(old.Histogram.Values, current.Histogram.Values) embeddingDrift := 1 - cosineFromEmbeddingRecords(old.Embeddings, current.Embeddings) semanticChanged := old.Asset.Semantic.Description != current.Asset.Semantic.Description || old.Asset.Semantic.MaterialType != current.Asset.Semantic.MaterialType || old.Asset.Semantic.SurfaceFinish != current.Asset.Semantic.SurfaceFinish status := "stable" if brightnessDelta > 0.04 || histDistance > 0.10 || embeddingDrift > 0.08 || semanticChanged { status = "drift" } return RegressionItem{ SKU: current.Asset.SKU, BrightnessDelta: round6(brightnessDelta), HistogramDistance: round6(histDistance), EmbeddingDrift: round6(embeddingDrift), SemanticChanged: semanticChanged, Status: status, } } func histogramDistance(a, b []float64) float64 { n := len(a) if len(b) < n { n = len(b) } if n == 0 { return 0 } var sum float64 for i := 0; i < n; i++ { sum += abs(a[i] - b[i]) } return sum / 2 } func cosineFromEmbeddingRecords(a, b []EmbeddingRecord) float64 { if len(a) == 0 || len(b) == 0 { return 0 } av := make([]float64, len(a[0].Values)) bv := make([]float64, len(b[0].Values)) for i, v := range a[0].Values { av[i] = float64(v) } for i, v := range b[0].Values { bv[i] = float64(v) } return cosine(av, bv) } func abs(v float64) float64 { if v < 0 { return -v } return v } func regressionBaselineName(path string) string { if path == "" { return "" } return filepath.Base(path) }