diff --git a/internal/pipeline/monitor_rules.go b/internal/pipeline/monitor_rules.go new file mode 100644 index 0000000..da708ae --- /dev/null +++ b/internal/pipeline/monitor_rules.go @@ -0,0 +1,16 @@ +package pipeline + +func candidateInMonitor(policy Policy, candidate Candidate) bool { + start := policy.MonitorStartHz + end := policy.MonitorEndHz + if start == 0 || end == 0 || end <= start { + return true + } + left := candidate.CenterHz + right := candidate.CenterHz + if candidate.BandwidthHz > 0 { + left = candidate.CenterHz - candidate.BandwidthHz/2 + right = candidate.CenterHz + candidate.BandwidthHz/2 + } + return right >= start && left <= end +} diff --git a/internal/pipeline/phases.go b/internal/pipeline/phases.go index b9a2f0a..5da17b2 100644 --- a/internal/pipeline/phases.go +++ b/internal/pipeline/phases.go @@ -27,6 +27,7 @@ type RefinementPlan struct { TotalCandidates int `json:"total_candidates"` MinCandidateSNRDb float64 `json:"min_candidate_snr_db"` Budget int `json:"budget"` + DroppedByMonitor int `json:"dropped_by_monitor"` DroppedBySNR int `json:"dropped_by_snr"` DroppedByBudget int `json:"dropped_by_budget"` Selected []ScheduledCandidate `json:"selected,omitempty"` diff --git a/internal/pipeline/scheduler.go b/internal/pipeline/scheduler.go index 9fbee14..4b5c7aa 100644 --- a/internal/pipeline/scheduler.go +++ b/internal/pipeline/scheduler.go @@ -26,6 +26,10 @@ func BuildRefinementPlan(candidates []Candidate, policy Policy) RefinementPlan { snrWeight, bwWeight, peakWeight := refinementIntentWeights(policy.Intent) scored := make([]ScheduledCandidate, 0, len(candidates)) for _, c := range candidates { + if !candidateInMonitor(policy, c) { + plan.DroppedByMonitor++ + continue + } if c.SNRDb < policy.MinCandidateSNRDb { plan.DroppedBySNR++ continue diff --git a/internal/pipeline/scheduler_test.go b/internal/pipeline/scheduler_test.go index f98095b..497ad74 100644 --- a/internal/pipeline/scheduler_test.go +++ b/internal/pipeline/scheduler_test.go @@ -60,6 +60,23 @@ func TestBuildRefinementPlanRespectsMaxConcurrent(t *testing.T) { } } +func TestBuildRefinementPlanAppliesMonitorSpan(t *testing.T) { + policy := Policy{MaxRefinementJobs: 5, MinCandidateSNRDb: 0, MonitorStartHz: 150, MonitorEndHz: 350} + cands := []Candidate{ + {ID: 1, CenterHz: 100, BandwidthHz: 20}, + {ID: 2, CenterHz: 200, BandwidthHz: 50}, + {ID: 3, CenterHz: 300, BandwidthHz: 100}, + {ID: 4, CenterHz: 500, BandwidthHz: 50}, + } + plan := BuildRefinementPlan(cands, policy) + if plan.DroppedByMonitor != 2 { + t.Fatalf("expected 2 dropped by monitor, got %d", plan.DroppedByMonitor) + } + if len(plan.Selected) != 2 { + t.Fatalf("expected 2 selected within monitor, got %d", len(plan.Selected)) + } +} + func TestAutoSpanForHint(t *testing.T) { span, source := AutoSpanForHint("WFM_STEREO") if span < 150000 || source == "" {