package pipeline import ( "strings" "sdr-wideband-suite/internal/classifier" ) type SignalDecision struct { Candidate Candidate `json:"candidate"` Class string `json:"class,omitempty"` ShouldRecord bool `json:"should_record"` ShouldAutoDecode bool `json:"should_auto_decode"` Reason string `json:"reason,omitempty"` MonitorBias float64 `json:"monitor_bias,omitempty"` RecordBias float64 `json:"record_bias,omitempty"` DecodeBias float64 `json:"decode_bias,omitempty"` MonitorDetail *MonitorWindowMatch `json:"monitor_detail,omitempty"` MonitorWindow *MonitorWindowMatch `json:"monitor_window,omitempty"` RecordWindow *MonitorWindowMatch `json:"record_window,omitempty"` DecodeWindow *MonitorWindowMatch `json:"decode_window,omitempty"` RecordAdmission *PriorityAdmission `json:"record_admission,omitempty"` DecodeAdmission *PriorityAdmission `json:"decode_admission,omitempty"` } func DecideSignalAction(policy Policy, candidate Candidate, cls *classifier.Classification) SignalDecision { if len(policy.MonitorWindows) > 0 { _ = ApplyMonitorWindowMatches(policy, &candidate) } decision := SignalDecision{Candidate: candidate} classTag := "" hintTag := strings.TrimSpace(candidate.Hint) if cls != nil { decision.Class = string(cls.ModType) classTag = decision.Class } if classTag != "" && WantsClass(policy.AutoRecordClasses, classTag) { decision.ShouldRecord = true decision.Reason = DecisionReasonRecordClass } else if classTag == "" && hintTag != "" && WantsClass(policy.AutoRecordClasses, hintTag) { decision.ShouldRecord = true decision.Reason = DecisionReasonRecordHint } if classTag != "" && WantsClass(policy.AutoDecodeClasses, classTag) { decision.ShouldAutoDecode = true if decision.Reason == "" { decision.Reason = DecisionReasonDecodeClass } } else if classTag == "" && hintTag != "" && WantsClass(policy.AutoDecodeClasses, hintTag) { decision.ShouldAutoDecode = true if decision.Reason == "" { decision.Reason = DecisionReasonDecodeHint } } recordMatch := bestMonitorActionMatch(candidate.MonitorMatches, true, false) if recordMatch != nil { decision.RecordBias = recordMatch.RecordBias if decision.RecordWindow == nil { decision.RecordWindow = recordMatch } if !decision.ShouldRecord { decision.ShouldRecord = true if decision.Reason == "" { decision.Reason = DecisionReasonRecordWindow } } } decodeMatch := bestMonitorActionMatch(candidate.MonitorMatches, false, true) if decodeMatch != nil { decision.DecodeBias = decodeMatch.DecodeBias if decision.DecodeWindow == nil { decision.DecodeWindow = decodeMatch } if !decision.ShouldAutoDecode { decision.ShouldAutoDecode = true if decision.Reason == "" { decision.Reason = DecisionReasonDecodeWindow } } } if decision.Reason == "" && candidate.Hint != "" { decision.Reason = DecisionReasonHintOnly } monitorBias, monitorDetail := MonitorWindowBias(policy, candidate) if monitorDetail == nil { monitorDetail = selectMonitorDetail(recordMatch, decodeMatch) } if monitorBias != 0 { decision.MonitorBias = monitorBias } if monitorDetail != nil { decision.MonitorDetail = monitorDetail decision.MonitorWindow = monitorDetail } return decision } func bestMonitorActionMatch(matches []MonitorWindowMatch, wantRecord bool, wantDecode bool) *MonitorWindowMatch { if len(matches) == 0 || (!wantRecord && !wantDecode) { return nil } best := -1 for i := range matches { match := matches[i] if wantRecord && !match.AutoRecord { continue } if wantDecode && !match.AutoDecode { continue } if best == -1 || betterMonitorActionMatch(match, matches[best]) { best = i } } if best == -1 { return nil } return &matches[best] } func betterMonitorActionMatch(candidate MonitorWindowMatch, best MonitorWindowMatch) bool { if candidate.Coverage != best.Coverage { return candidate.Coverage > best.Coverage } if candidate.DistanceHz != best.DistanceHz { return candidate.DistanceHz < best.DistanceHz } if candidate.Bias != best.Bias { return candidate.Bias > best.Bias } return candidate.Index < best.Index } func selectMonitorDetail(recordMatch *MonitorWindowMatch, decodeMatch *MonitorWindowMatch) *MonitorWindowMatch { if recordMatch == nil { return decodeMatch } if decodeMatch == nil { return recordMatch } if betterMonitorActionMatch(*recordMatch, *decodeMatch) { return recordMatch } return decodeMatch }