FloorMaterialAnalyzer/internal/intelligence/regression.go

109 lines
2.7 KiB
Go
Raw Permalink Normal View History

2026-07-27 03:03:51 +00:00
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)
}