package pipeline import ( "sort" "strings" ) type ScheduledCandidate struct { Candidate Candidate `json:"candidate"` Priority float64 `json:"priority"` Tier string `json:"tier,omitempty"` Score *RefinementScore `json:"score,omitempty"` Breakdown *RefinementScoreDetails `json:"breakdown,omitempty"` } type RefinementScoreModel struct { SNRWeight float64 `json:"snr_weight"` BandwidthWeight float64 `json:"bandwidth_weight"` PeakWeight float64 `json:"peak_weight"` EvidenceWeight float64 `json:"evidence_weight"` } type RefinementScoreDetails struct { SNRScore float64 `json:"snr_score"` BandwidthScore float64 `json:"bandwidth_score"` PeakScore float64 `json:"peak_score"` PolicyBoost float64 `json:"policy_boost"` EvidenceScore float64 `json:"evidence_score"` EvidenceDetail *EvidenceScoreDetails `json:"evidence_detail,omitempty"` } type RefinementScore struct { Total float64 `json:"total"` Breakdown RefinementScoreDetails `json:"breakdown"` Weights *RefinementScoreModel `json:"weights,omitempty"` } type RefinementWorkItem struct { Candidate Candidate `json:"candidate"` Window RefinementWindow `json:"window,omitempty"` Execution *RefinementExecution `json:"execution,omitempty"` Priority float64 `json:"priority,omitempty"` Score *RefinementScore `json:"score,omitempty"` Breakdown *RefinementScoreDetails `json:"breakdown,omitempty"` Status string `json:"status,omitempty"` Reason string `json:"reason,omitempty"` Admission *PriorityAdmission `json:"admission,omitempty"` } type RefinementExecution struct { Stage string `json:"stage,omitempty"` SampleRate int `json:"sample_rate,omitempty"` FFTSize int `json:"fft_size,omitempty"` CenterHz float64 `json:"center_hz,omitempty"` SpanHz float64 `json:"span_hz,omitempty"` Source string `json:"source,omitempty"` } const ( RefinementStatusPlanned = "planned" RefinementStatusAdmitted = "admitted" RefinementStatusRunning = "running" RefinementStatusCompleted = "completed" RefinementStatusDropped = "dropped" RefinementStatusSkipped = "skipped" RefinementStatusDisplaced = "displaced" ) const ( RefinementReasonPlanned = "refinement:planned" RefinementReasonAdmitted = "refinement:admitted" RefinementReasonRunning = "refinement:running" RefinementReasonCompleted = "refinement:completed" RefinementReasonMonitorGate = "refinement:drop:monitor" RefinementReasonBelowSNR = "refinement:drop:snr" RefinementReasonBudget = "refinement:skip:budget" RefinementReasonDisabled = "refinement:drop:disabled" RefinementReasonUnclassified = "refinement:drop:unclassified" RefinementReasonDisplaced = "refinement:skip:displaced" ) // BuildRefinementPlan scores and ranks candidates for costly local refinement. // Admission/budget enforcement is handled by arbitration to keep refinement/record/decode consistent. // Current heuristic is intentionally simple and deterministic; later phases can add // richer scoring (novelty, persistence, profile-aware band priorities, decoder value). func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan { strategy, strategyReason := refinementStrategy(policy) budgetModel := BudgetModelFromPolicy(policy) budget := budgetModel.Refinement.Max holdPolicy := HoldPolicyFromPolicy(policy) plan := RefinementPlan{ TotalCandidates: len(candidates), MinCandidateSNRDb: policy.MinCandidateSNRDb, Budget: budget, BudgetSource: budgetModel.Refinement.Source, Strategy: strategy, StrategyReason: strategyReason, } if start, end, ok := monitorBounds(policy); ok { plan.MonitorStartHz = start plan.MonitorEndHz = end if end > start { plan.MonitorSpanHz = end - start } } if len(candidates) == 0 { return plan } snrWeight, bwWeight, peakWeight := refinementIntentWeights(policy.Intent) scoreModel := RefinementScoreModel{ SNRWeight: snrWeight, BandwidthWeight: bwWeight, PeakWeight: peakWeight, EvidenceWeight: 0.6, } scoreModel = applyStrategyWeights(strategy, scoreModel) plan.ScoreModel = scoreModel scored := make([]ScheduledCandidate, 0, len(candidates)) workItems := make([]RefinementWorkItem, 0, len(candidates)) for _, c := range candidates { candidate := c RefreshCandidateEvidenceState(&candidate) if !candidateInMonitor(policy, candidate) { plan.DroppedByMonitor++ workItems = append(workItems, RefinementWorkItem{ Candidate: candidate, Status: RefinementStatusDropped, Reason: RefinementReasonMonitorGate, Admission: &PriorityAdmission{ Tier: PriorityTierBackground, Class: AdmissionClassDrop, Basis: "refinement", Reason: admissionReason(RefinementReasonMonitorGate, policy, holdPolicy), }, }) continue } if candidate.SNRDb < policy.MinCandidateSNRDb { plan.DroppedBySNR++ workItems = append(workItems, RefinementWorkItem{ Candidate: candidate, Status: RefinementStatusDropped, Reason: RefinementReasonBelowSNR, Admission: &PriorityAdmission{ Tier: PriorityTierBackground, Class: AdmissionClassDrop, Basis: "refinement", Reason: admissionReason(RefinementReasonBelowSNR, policy, holdPolicy), }, }) continue } snrScore := candidate.SNRDb * scoreModel.SNRWeight bwScore := 0.0 peakScore := 0.0 policyBoost := CandidatePriorityBoost(policy, candidate.Hint) if candidate.BandwidthHz > 0 { bwScore = minFloat64(candidate.BandwidthHz/25000.0, 6) * scoreModel.BandwidthWeight } if candidate.PeakDb > 0 { peakScore = (candidate.PeakDb / 20.0) * scoreModel.PeakWeight } rawEvidenceScore, evidenceDetail := candidateEvidenceScore(candidate, strategy) evidenceDetail.Weight = scoreModel.EvidenceWeight evidenceDetail.RawScore = rawEvidenceScore evidenceDetail.WeightedScore = rawEvidenceScore * scoreModel.EvidenceWeight evidenceScore := evidenceDetail.WeightedScore priority := snrScore + bwScore + peakScore + policyBoost priority += evidenceScore score := &RefinementScore{ Total: priority, Breakdown: RefinementScoreDetails{ SNRScore: snrScore, BandwidthScore: bwScore, PeakScore: peakScore, PolicyBoost: policyBoost, EvidenceScore: evidenceScore, EvidenceDetail: &evidenceDetail, }, Weights: &scoreModel, } scored = append(scored, ScheduledCandidate{ Candidate: candidate, Priority: priority, Score: score, Breakdown: &score.Breakdown, }) workItems = append(workItems, RefinementWorkItem{ Candidate: candidate, Priority: priority, Score: score, Breakdown: &score.Breakdown, Status: RefinementStatusPlanned, Reason: RefinementReasonPlanned, Admission: &PriorityAdmission{ Class: AdmissionClassPlanned, Score: priority, Basis: "refinement", Reason: admissionReason(RefinementReasonPlanned, policy, holdPolicy), }, }) } sort.Slice(scored, func(i, j int) bool { if scored[i].Priority == scored[j].Priority { return scored[i].Candidate.CenterHz < scored[j].Candidate.CenterHz } return scored[i].Priority > scored[j].Priority }) if len(scored) > 0 { minPriority := scored[0].Priority maxPriority := scored[0].Priority sumPriority := 0.0 for _, s := range scored { if s.Priority < minPriority { minPriority = s.Priority } if s.Priority > maxPriority { maxPriority = s.Priority } sumPriority += s.Priority } plan.PriorityMin = minPriority plan.PriorityMax = maxPriority plan.PriorityAvg = sumPriority / float64(len(scored)) for i := range scored { scored[i].Tier = PriorityTierFromRange(scored[i].Priority, minPriority, maxPriority) } for i := range workItems { if workItems[i].Admission == nil { continue } if workItems[i].Status != RefinementStatusPlanned { continue } workItems[i].Admission.Tier = PriorityTierFromRange(workItems[i].Priority, minPriority, maxPriority) } } plan.Ranked = append(plan.Ranked, scored...) plan.WorkItems = workItems return plan } func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandidate { plan := BuildRefinementPlan(candidates, policy) if len(plan.Ranked) > 0 { return plan.Ranked } return plan.Selected } func refinementStrategy(policy Policy) (string, string) { intent := strings.ToLower(strings.TrimSpace(policy.Intent)) profile := strings.ToLower(strings.TrimSpace(policy.Profile)) switch { case strings.Contains(profile, "digital"): return "digital-hunting", "profile" case strings.Contains(profile, "archive"): return "archive-oriented", "profile" case strings.Contains(profile, "aggressive"): return "multi-resolution", "profile" case strings.Contains(intent, "digital") || strings.Contains(intent, "hunt") || strings.Contains(intent, "decode"): return "digital-hunting", "intent" case strings.Contains(intent, "archive") || strings.Contains(intent, "triage") || strings.Contains(policy.Mode, "archive"): return "archive-oriented", "intent" case strings.Contains(strings.ToLower(policy.SurveillanceStrategy), "multi"): return "multi-resolution", "surveillance-strategy" default: return "single-resolution", "default" } } func applyStrategyWeights(strategy string, model RefinementScoreModel) RefinementScoreModel { switch strings.ToLower(strings.TrimSpace(strategy)) { case "digital-hunting": model.SNRWeight *= 1.4 model.BandwidthWeight *= 0.75 model.PeakWeight *= 1.2 case "archive-oriented": model.SNRWeight *= 1.1 model.BandwidthWeight *= 1.6 model.PeakWeight *= 1.05 case "multi-resolution", "multi", "multi-res", "multi_res": model.SNRWeight *= 1.15 model.BandwidthWeight *= 1.1 model.PeakWeight *= 1.15 case "single-resolution": model.SNRWeight *= 1.1 model.BandwidthWeight *= 1.0 model.PeakWeight *= 1.0 } return model } func candidateEvidenceScore(candidate Candidate, strategy string) (float64, EvidenceScoreDetails) { state := CandidateEvidenceStateFor(candidate) details := EvidenceScoreDetails{ DetectionLevels: state.DetectionLevelCount, PrimaryLevels: state.PrimaryLevelCount, DerivedLevels: state.DerivedLevelCount, SupportLevels: state.SupportLevelCount, ProvenanceCount: len(state.Provenance), DerivedOnly: state.DerivedOnly, MultiLevelConfirmed: state.MultiLevelConfirmed, } score := 0.0 if state.MultiLevelConfirmed && state.DetectionLevelCount > 1 { bonus := 0.85 * float64(state.DetectionLevelCount-1) score += bonus details.MultiLevelBonus = bonus } if len(state.Provenance) > 1 { bonus := 0.15 * float64(len(state.Provenance)-1) score += bonus details.ProvenanceBonus = bonus } if state.DerivedOnly { penalty := 0.35 score -= penalty details.DerivedPenalty = -penalty } switch strings.ToLower(strings.TrimSpace(strategy)) { case "multi-resolution", "multi", "multi-res", "multi_res": if state.DerivedOnly { bias := 0.2 score += bias details.StrategyBias = bias } else if state.MultiLevelConfirmed { bias := 0.1 score += bias details.StrategyBias = bias } case "digital-hunting": if state.DerivedOnly { bias := -0.15 score += bias details.StrategyBias = bias } else if state.MultiLevelConfirmed { bias := 0.05 score += bias details.StrategyBias = bias } case "archive-oriented": if state.DerivedOnly { bias := -0.1 score += bias details.StrategyBias = bias } case "single-resolution": if state.MultiLevelConfirmed { bias := 0.05 score += bias details.StrategyBias = bias } } return score, details } func minFloat64(a, b float64) float64 { if a < b { return a } return b }