Procházet zdrojové kódy

Consolidate arbitration core and normalize reasons

master
Jan Svabenik před 7 hodinami
rodič
revize
7c29f37ef4
18 změnil soubory, kde provedl 246 přidání a 132 odebrání
  1. +2
    -2
      cmd/sdrd/arbitration_snapshot.go
  2. +0
    -36
      cmd/sdrd/arbitrator.go
  3. +0
    -32
      cmd/sdrd/decision_budget_test.go
  4. +1
    -1
      cmd/sdrd/decision_summary.go
  5. +1
    -1
      cmd/sdrd/phase_state.go
  6. +1
    -1
      cmd/sdrd/phase_state_test.go
  7. +11
    -11
      cmd/sdrd/pipeline_runtime.go
  8. +6
    -13
      cmd/sdrd/types.go
  9. +32
    -0
      internal/pipeline/arbiter.go
  10. +21
    -7
      internal/pipeline/arbitration.go
  11. +26
    -0
      internal/pipeline/arbitration_reasons.go
  12. +17
    -0
      internal/pipeline/arbitration_state.go
  13. +26
    -1
      internal/pipeline/arbitration_test.go
  14. +10
    -12
      internal/pipeline/decision_queue.go
  15. +59
    -0
      internal/pipeline/decision_queue_test.go
  16. +5
    -5
      internal/pipeline/decisions.go
  17. +17
    -10
      internal/pipeline/scheduler.go
  18. +11
    -0
      internal/pipeline/scheduler_test.go

+ 2
- 2
cmd/sdrd/arbitration_snapshot.go Zobrazit soubor

@@ -2,12 +2,12 @@ package main

import "sdr-wideband-suite/internal/pipeline"

func buildArbitrationSnapshot(step pipeline.RefinementStep, arb arbitrationState) *ArbitrationSnapshot {
func buildArbitrationSnapshot(step pipeline.RefinementStep, arb pipeline.ArbitrationState) *ArbitrationSnapshot {
return &ArbitrationSnapshot{
Budgets: &arb.Budgets,
HoldPolicy: &arb.HoldPolicy,
RefinementPlan: &step.Input.Plan,
RefinementAdmission: &step.Input.Admission,
RefinementAdmission: &arb.Refinement,
Queue: arb.Queue,
DecisionSummary: summarizeDecisions(step.Result.Decisions),
DecisionItems: compactDecisions(step.Result.Decisions),


+ 0
- 36
cmd/sdrd/arbitrator.go Zobrazit soubor

@@ -1,36 +0,0 @@
package main

import (
"time"

"sdr-wideband-suite/internal/pipeline"
)

type arbitrator struct {
refinementHold *pipeline.RefinementHold
queues *decisionQueues
}

func newArbitrator() *arbitrator {
return &arbitrator{
refinementHold: &pipeline.RefinementHold{Active: map[int64]time.Time{}},
queues: newDecisionQueues(),
}
}

func (a *arbitrator) AdmitRefinement(plan pipeline.RefinementPlan, policy pipeline.Policy, now time.Time) pipeline.RefinementAdmissionResult {
if a == nil {
return pipeline.AdmitRefinementPlan(plan, policy, now, nil)
}
if a.refinementHold == nil {
a.refinementHold = &pipeline.RefinementHold{Active: map[int64]time.Time{}}
}
return pipeline.AdmitRefinementPlan(plan, policy, now, a.refinementHold)
}

func (a *arbitrator) ApplyDecisions(decisions []pipeline.SignalDecision, budget pipeline.BudgetModel, now time.Time, policy pipeline.Policy) decisionQueueStats {
if a == nil || a.queues == nil {
return decisionQueueStats{}
}
return a.queues.Apply(decisions, budget, now, policy)
}

+ 0
- 32
cmd/sdrd/decision_budget_test.go Zobrazit soubor

@@ -1,32 +0,0 @@
package main

import (
"testing"
"time"

"sdr-wideband-suite/internal/pipeline"
)

func TestEnforceDecisionBudgets(t *testing.T) {
decisions := []pipeline.SignalDecision{
{Candidate: pipeline.Candidate{ID: 1, SNRDb: 5}, ShouldRecord: true, ShouldAutoDecode: true},
{Candidate: pipeline.Candidate{ID: 2, SNRDb: 15}, ShouldRecord: true, ShouldAutoDecode: true},
{Candidate: pipeline.Candidate{ID: 3, SNRDb: 10}, ShouldRecord: true, ShouldAutoDecode: false},
}
q := newDecisionQueues()
policy := pipeline.Policy{SignalPriorities: []string{"digital"}, MaxRecordingStreams: 1, MaxDecodeJobs: 1}
budget := pipeline.BudgetModelFromPolicy(policy)
stats := q.Apply(decisions, budget, time.Now(), policy)
if stats.RecordSelected != 1 || stats.DecodeSelected != 1 {
t.Fatalf("unexpected counts: record=%d decode=%d", stats.RecordSelected, stats.DecodeSelected)
}
if !decisions[1].ShouldRecord || !decisions[1].ShouldAutoDecode {
t.Fatalf("expected highest SNR decision to remain allowed")
}
if decisions[0].ShouldRecord || decisions[0].ShouldAutoDecode {
t.Fatalf("expected lowest SNR decision to be budgeted off")
}
if decisions[2].ShouldRecord {
t.Fatalf("expected mid SNR decision to be budgeted off by record budget")
}
}

+ 1
- 1
cmd/sdrd/decision_summary.go Zobrazit soubor

@@ -21,7 +21,7 @@ func summarizeDecisions(decisions []pipeline.SignalDecision) decisionSummary {
}
reason := d.Reason
if reason == "" {
reason = "unspecified"
reason = pipeline.DecisionReasonUnspecified
}
summary.Reasons[reason]++
}


+ 1
- 1
cmd/sdrd/phase_state.go Zobrazit soubor

@@ -5,6 +5,6 @@ import "sdr-wideband-suite/internal/pipeline"
type phaseState struct {
surveillance pipeline.SurveillanceResult
refinement pipeline.RefinementStep
arbitration arbitrationState
arbitration pipeline.ArbitrationState
presentation pipeline.AnalysisLevel
}

+ 1
- 1
cmd/sdrd/phase_state_test.go Zobrazit soubor

@@ -13,7 +13,7 @@ func TestPhaseStateCarriesPhaseResults(t *testing.T) {
Input: pipeline.RefinementInput{Scheduled: []pipeline.ScheduledCandidate{{Candidate: pipeline.Candidate{ID: 1}, Priority: 5}}, SampleRate: 2048000, FFTSize: 2048, CenterHz: 7.1e6},
Result: pipeline.RefinementResult{Level: pipeline.AnalysisLevel{Name: "refinement"}, Decisions: []pipeline.SignalDecision{{ShouldRecord: true}}, Candidates: []pipeline.Candidate{{ID: 1}}},
},
arbitration: arbitrationState{Queue: decisionQueueStats{RecordQueued: 1}},
arbitration: pipeline.ArbitrationState{Queue: pipeline.DecisionQueueStats{RecordQueued: 1}},
presentation: pipeline.AnalysisLevel{Name: "presentation"},
}
if ps.surveillance.NoiseFloor != -90 || len(ps.surveillance.Scheduled) != 1 {


+ 11
- 11
cmd/sdrd/pipeline_runtime.go Zobrazit soubor

@@ -42,8 +42,8 @@ type dspRuntime struct {
rdsMap map[int64]*rdsState
streamPhaseState map[int64]*streamExtractState
streamOverlap *streamIQOverlap
arbiter *arbitrator
arbitration arbitrationState
arbiter *pipeline.Arbiter
arbitration pipeline.ArbitrationState
gotSamples bool
}

@@ -79,7 +79,7 @@ func newDSPRuntime(cfg config.Config, det *detector.Detector, window []float64,
rdsMap: map[int64]*rdsState{},
streamPhaseState: map[int64]*streamExtractState{},
streamOverlap: &streamIQOverlap{},
arbiter: newArbitrator(),
arbiter: pipeline.NewArbiter(),
}
if rt.useGPU && gpuState != nil {
snap := gpuState.snapshot()
@@ -426,17 +426,15 @@ func (rt *dspRuntime) buildRefinementInput(surv pipeline.SurveillanceResult, now
item.Reason = pipeline.RefinementReasonDisabled
}
input.Scheduled = nil
input.Request.Reason = pipeline.RefinementReasonDisabled
input.Admission.Reason = pipeline.RefinementReasonDisabled
input.Request.Reason = pipeline.ReasonAdmissionDisabled
input.Admission.Reason = pipeline.ReasonAdmissionDisabled
input.Admission.Admitted = 0
input.Admission.Skipped = 0
input.Admission.Displaced = 0
input.Plan.Selected = nil
input.Plan.DroppedByBudget = 0
}
rt.arbitration.Budgets = input.Budgets
rt.arbitration.Refinement = input.Admission
rt.arbitration.HoldPolicy = pipeline.HoldPolicyFromPolicy(policy)
rt.setArbitration(policy, input.Budgets, input.Admission, rt.arbitration.Queue)
return input
}

@@ -508,9 +506,7 @@ func (rt *dspRuntime) refineSignals(art *spectrumArtifacts, input pipeline.Refin
}
budget := pipeline.BudgetModelFromPolicy(policy)
queueStats := rt.arbiter.ApplyDecisions(decisions, budget, art.now, policy)
rt.arbitration.Budgets = budget
rt.arbitration.HoldPolicy = pipeline.HoldPolicyFromPolicy(policy)
rt.arbitration.Queue = queueStats
rt.setArbitration(policy, budget, input.Admission, queueStats)
summary := summarizeDecisions(decisions)
if rec != nil {
if summary.RecordEnabled > 0 {
@@ -705,3 +701,7 @@ func markWorkItemsCompleted(items []pipeline.RefinementWorkItem, candidates []pi
items[i].Reason = pipeline.RefinementReasonCompleted
}
}

func (rt *dspRuntime) setArbitration(policy pipeline.Policy, budget pipeline.BudgetModel, admission pipeline.RefinementAdmission, queue pipeline.DecisionQueueStats) {
rt.arbitration = pipeline.BuildArbitrationState(policy, budget, admission, queue)
}

+ 6
- 13
cmd/sdrd/types.go Zobrazit soubor

@@ -45,20 +45,13 @@ type DecisionDebug struct {
}

type ArbitrationSnapshot struct {
Budgets *pipeline.BudgetModel `json:"budgets,omitempty"`
HoldPolicy *pipeline.HoldPolicy `json:"hold_policy,omitempty"`
RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"`
Budgets *pipeline.BudgetModel `json:"budgets,omitempty"`
HoldPolicy *pipeline.HoldPolicy `json:"hold_policy,omitempty"`
RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"`
RefinementAdmission *pipeline.RefinementAdmission `json:"refinement_admission,omitempty"`
Queue decisionQueueStats `json:"queue,omitempty"`
DecisionSummary decisionSummary `json:"decision_summary,omitempty"`
DecisionItems []compactDecision `json:"decision_items,omitempty"`
}

type arbitrationState struct {
Budgets pipeline.BudgetModel
HoldPolicy pipeline.HoldPolicy
Refinement pipeline.RefinementAdmission
Queue decisionQueueStats
Queue pipeline.DecisionQueueStats `json:"queue,omitempty"`
DecisionSummary decisionSummary `json:"decision_summary,omitempty"`
DecisionItems []compactDecision `json:"decision_items,omitempty"`
}

type SpectrumFrame struct {


+ 32
- 0
internal/pipeline/arbiter.go Zobrazit soubor

@@ -0,0 +1,32 @@
package pipeline

import "time"

type Arbiter struct {
refinementHold *RefinementHold
queues *decisionQueues
}

func NewArbiter() *Arbiter {
return &Arbiter{
refinementHold: &RefinementHold{Active: map[int64]time.Time{}},
queues: newDecisionQueues(),
}
}

func (a *Arbiter) AdmitRefinement(plan RefinementPlan, policy Policy, now time.Time) RefinementAdmissionResult {
if a == nil {
return AdmitRefinementPlan(plan, policy, now, nil)
}
if a.refinementHold == nil {
a.refinementHold = &RefinementHold{Active: map[int64]time.Time{}}
}
return AdmitRefinementPlan(plan, policy, now, a.refinementHold)
}

func (a *Arbiter) ApplyDecisions(decisions []SignalDecision, budget BudgetModel, now time.Time, policy Policy) DecisionQueueStats {
if a == nil || a.queues == nil {
return DecisionQueueStats{}
}
return a.queues.Apply(decisions, budget, now, policy)
}

+ 21
- 7
internal/pipeline/arbitration.go Zobrazit soubor

@@ -52,25 +52,39 @@ func HoldPolicyFromPolicy(policy Policy) HoldPolicy {
profile := strings.ToLower(strings.TrimSpace(policy.Profile))
strategy := strings.ToLower(strings.TrimSpace(policy.RefinementStrategy))

if profileContains(profile, "archive") || strategyContains(strategy, "archive") {
archiveProfile := profileContains(profile, "archive")
archiveStrategy := strategyContains(strategy, "archive")
if archiveProfile || archiveStrategy {
recMult *= 1.5
decMult *= 1.1
refMult *= 1.2
reasons = append(reasons, "archive")
if archiveProfile {
reasons = append(reasons, HoldReasonProfileArchive)
}
if archiveStrategy {
reasons = append(reasons, HoldReasonStrategyArchive)
}
}
if profileContains(profile, "digital") || strategyContains(strategy, "digital") {
digitalProfile := profileContains(profile, "digital")
digitalStrategy := strategyContains(strategy, "digital")
if digitalProfile || digitalStrategy {
decMult *= 1.6
recMult *= 0.85
refMult *= 1.1
reasons = append(reasons, "digital")
if digitalProfile {
reasons = append(reasons, HoldReasonProfileDigital)
}
if digitalStrategy {
reasons = append(reasons, HoldReasonStrategyDigital)
}
}
if profileContains(profile, "aggressive") {
refMult *= 1.15
reasons = append(reasons, "aggressive")
reasons = append(reasons, HoldReasonProfileAggressive)
}
if strategyContains(strings.ToLower(strings.TrimSpace(policy.SurveillanceStrategy)), "multi") {
refMult *= 1.1
reasons = append(reasons, "multi-resolution")
reasons = append(reasons, HoldReasonStrategyMultiRes)
}

return HoldPolicy{
@@ -95,7 +109,7 @@ func AdmitRefinementPlan(plan RefinementPlan, policy Policy, now time.Time, hold
BudgetSource: plan.BudgetSource,
}
if len(ranked) == 0 {
admission.Reason = "no-candidates"
admission.Reason = ReasonAdmissionNoCandidates
return RefinementAdmissionResult{Plan: plan, WorkItems: workItems, Admission: admission}
}



+ 26
- 0
internal/pipeline/arbitration_reasons.go Zobrazit soubor

@@ -0,0 +1,26 @@
package pipeline

const (
ReasonAdmissionNoCandidates = "admission:none:candidates"
ReasonAdmissionDisabled = "admission:disabled"
)

const (
DecisionReasonRecordClass = "decision:record:class"
DecisionReasonRecordHint = "decision:record:hint"
DecisionReasonDecodeClass = "decision:decode:class"
DecisionReasonDecodeHint = "decision:decode:hint"
DecisionReasonHintOnly = "decision:hint"
DecisionReasonQueueRecord = "queue:record:budget"
DecisionReasonQueueDecode = "queue:decode:budget"
DecisionReasonUnspecified = "decision:unspecified"
)

const (
HoldReasonProfileArchive = "profile:archive"
HoldReasonProfileDigital = "profile:digital"
HoldReasonProfileAggressive = "profile:aggressive"
HoldReasonStrategyArchive = "strategy:archive"
HoldReasonStrategyDigital = "strategy:digital"
HoldReasonStrategyMultiRes = "strategy:multi-resolution"
)

+ 17
- 0
internal/pipeline/arbitration_state.go Zobrazit soubor

@@ -0,0 +1,17 @@
package pipeline

type ArbitrationState struct {
Budgets BudgetModel `json:"budgets,omitempty"`
HoldPolicy HoldPolicy `json:"hold_policy,omitempty"`
Refinement RefinementAdmission `json:"refinement,omitempty"`
Queue DecisionQueueStats `json:"queue,omitempty"`
}

func BuildArbitrationState(policy Policy, budget BudgetModel, admission RefinementAdmission, queue DecisionQueueStats) ArbitrationState {
return ArbitrationState{
Budgets: budget,
HoldPolicy: HoldPolicyFromPolicy(policy),
Refinement: admission,
Queue: queue,
}
}

+ 26
- 1
internal/pipeline/arbitration_test.go Zobrazit soubor

@@ -1,6 +1,9 @@
package pipeline

import "testing"
import (
"testing"
"time"
)

func TestHoldPolicyArchiveBiasesRecord(t *testing.T) {
policy := Policy{DecisionHoldMs: 1000, Profile: "archive", RefinementStrategy: "archive-oriented"}
@@ -11,6 +14,9 @@ func TestHoldPolicyArchiveBiasesRecord(t *testing.T) {
if hold.RefinementMs <= hold.BaseMs {
t.Fatalf("expected archive profile to extend refinement hold, got %d vs %d", hold.RefinementMs, hold.BaseMs)
}
if !containsReason(hold.Reasons, HoldReasonProfileArchive) {
t.Fatalf("expected profile archive reason, got %+v", hold.Reasons)
}
}

func TestHoldPolicyDigitalBiasesDecode(t *testing.T) {
@@ -19,4 +25,23 @@ func TestHoldPolicyDigitalBiasesDecode(t *testing.T) {
if hold.DecodeMs <= hold.RecordMs {
t.Fatalf("expected digital profile to favor decode hold, got decode=%d record=%d", hold.DecodeMs, hold.RecordMs)
}
if !containsReason(hold.Reasons, HoldReasonProfileDigital) {
t.Fatalf("expected profile digital reason, got %+v", hold.Reasons)
}
}

func TestAdmitRefinementPlanNoCandidatesReason(t *testing.T) {
res := AdmitRefinementPlan(RefinementPlan{}, Policy{}, time.Now(), &RefinementHold{Active: map[int64]time.Time{}})
if res.Admission.Reason != ReasonAdmissionNoCandidates {
t.Fatalf("expected no-candidates reason, got %s", res.Admission.Reason)
}
}

func containsReason(reasons []string, target string) bool {
for _, r := range reasons {
if r == target {
return true
}
}
return false
}

cmd/sdrd/decision_budget.go → internal/pipeline/decision_queue.go Zobrazit soubor

@@ -1,13 +1,11 @@
package main
package pipeline

import (
"sort"
"time"

"sdr-wideband-suite/internal/pipeline"
)

type decisionQueueStats struct {
type DecisionQueueStats struct {
RecordQueued int `json:"record_queued"`
DecodeQueued int `json:"decode_queued"`
RecordSelected int `json:"record_selected"`
@@ -50,11 +48,11 @@ func newDecisionQueues() *decisionQueues {
}
}

func (dq *decisionQueues) Apply(decisions []pipeline.SignalDecision, budget pipeline.BudgetModel, now time.Time, policy pipeline.Policy) decisionQueueStats {
func (dq *decisionQueues) Apply(decisions []SignalDecision, budget BudgetModel, now time.Time, policy Policy) DecisionQueueStats {
if dq == nil {
return decisionQueueStats{}
return DecisionQueueStats{}
}
holdPolicy := pipeline.HoldPolicyFromPolicy(policy)
holdPolicy := HoldPolicyFromPolicy(policy)
recordHold := time.Duration(holdPolicy.RecordMs) * time.Millisecond
decodeHold := time.Duration(holdPolicy.DecodeMs) * time.Millisecond
recSeen := map[int64]bool{}
@@ -106,7 +104,7 @@ func (dq *decisionQueues) Apply(decisions []pipeline.SignalDecision, budget pipe
recSelected := selectQueued("record", dq.record, dq.recordHold, budget.Record.Max, recordHold, now, policy)
decSelected := selectQueued("decode", dq.decode, dq.decodeHold, budget.Decode.Max, decodeHold, now, policy)

stats := decisionQueueStats{
stats := DecisionQueueStats{
RecordQueued: len(dq.record),
DecodeQueued: len(dq.decode),
RecordSelected: len(recSelected),
@@ -127,7 +125,7 @@ func (dq *decisionQueues) Apply(decisions []pipeline.SignalDecision, budget pipe
if decisions[i].ShouldRecord {
if _, ok := recSelected[id]; !ok {
decisions[i].ShouldRecord = false
decisions[i].Reason = "queued: record budget"
decisions[i].Reason = DecisionReasonQueueRecord
stats.RecordDropped++
}
}
@@ -135,7 +133,7 @@ func (dq *decisionQueues) Apply(decisions []pipeline.SignalDecision, budget pipe
if _, ok := decSelected[id]; !ok {
decisions[i].ShouldAutoDecode = false
if decisions[i].Reason == "" {
decisions[i].Reason = "queued: decode budget"
decisions[i].Reason = DecisionReasonQueueDecode
}
stats.DecodeDropped++
}
@@ -144,7 +142,7 @@ func (dq *decisionQueues) Apply(decisions []pipeline.SignalDecision, budget pipe
return stats
}

func selectQueued(queueName string, queue map[int64]*queuedDecision, hold map[int64]time.Time, max int, holdDur time.Duration, now time.Time, policy pipeline.Policy) map[int64]struct{} {
func selectQueued(queueName string, queue map[int64]*queuedDecision, hold map[int64]time.Time, max int, holdDur time.Duration, now time.Time, policy Policy) map[int64]struct{} {
selected := map[int64]struct{}{}
if len(queue) == 0 {
return selected
@@ -164,7 +162,7 @@ func selectQueued(queueName string, queue map[int64]*queuedDecision, hold map[in
if hint == "" {
hint = qd.Class
}
policyBoost := pipeline.DecisionPriorityBoost(policy, hint, qd.Class, queueName)
policyBoost := DecisionPriorityBoost(policy, hint, qd.Class, queueName)
scoredList = append(scoredList, scored{id: id, score: qd.SNRDb + boost + policyBoost})
}
sort.Slice(scoredList, func(i, j int) bool {

+ 59
- 0
internal/pipeline/decision_queue_test.go Zobrazit soubor

@@ -0,0 +1,59 @@
package pipeline

import (
"testing"
"time"
)

func TestDecisionQueueDropsByBudget(t *testing.T) {
arbiter := NewArbiter()
decisions := []SignalDecision{
{Candidate: Candidate{ID: 1, SNRDb: 12}, ShouldRecord: true, ShouldAutoDecode: true},
{Candidate: Candidate{ID: 2, SNRDb: 10}, ShouldRecord: true, ShouldAutoDecode: true},
}
budget := BudgetModel{
Record: BudgetQueue{Max: 1},
Decode: BudgetQueue{Max: 1},
}
stats := arbiter.ApplyDecisions(decisions, budget, time.Now(), Policy{DecisionHoldMs: 250})
if stats.RecordDropped == 0 || stats.DecodeDropped == 0 {
t.Fatalf("expected drops by budget, got %+v", stats)
}
allowed := 0
for _, d := range decisions {
if d.ShouldRecord || d.ShouldAutoDecode {
allowed++
continue
}
if d.Reason != DecisionReasonQueueRecord && d.Reason != DecisionReasonQueueDecode {
t.Fatalf("unexpected decision reason: %s", d.Reason)
}
}
if allowed != 1 {
t.Fatalf("expected 1 decision allowed, got %d", allowed)
}
}

func TestDecisionQueueEnforcesBudgets(t *testing.T) {
decisions := []SignalDecision{
{Candidate: Candidate{ID: 1, SNRDb: 5}, ShouldRecord: true, ShouldAutoDecode: true},
{Candidate: Candidate{ID: 2, SNRDb: 15}, ShouldRecord: true, ShouldAutoDecode: true},
{Candidate: Candidate{ID: 3, SNRDb: 10}, ShouldRecord: true, ShouldAutoDecode: false},
}
arbiter := NewArbiter()
policy := Policy{SignalPriorities: []string{"digital"}, MaxRecordingStreams: 1, MaxDecodeJobs: 1}
budget := BudgetModelFromPolicy(policy)
stats := arbiter.ApplyDecisions(decisions, budget, time.Now(), policy)
if stats.RecordSelected != 1 || stats.DecodeSelected != 1 {
t.Fatalf("unexpected counts: record=%d decode=%d", stats.RecordSelected, stats.DecodeSelected)
}
if !decisions[1].ShouldRecord || !decisions[1].ShouldAutoDecode {
t.Fatalf("expected highest SNR decision to remain allowed")
}
if decisions[0].ShouldRecord || decisions[0].ShouldAutoDecode {
t.Fatalf("expected lowest SNR decision to be budgeted off")
}
if decisions[2].ShouldRecord {
t.Fatalf("expected mid SNR decision to be budgeted off by record budget")
}
}

+ 5
- 5
internal/pipeline/decisions.go Zobrazit soubor

@@ -24,24 +24,24 @@ func DecideSignalAction(policy Policy, candidate Candidate, cls *classifier.Clas
}
if classTag != "" && WantsClass(policy.AutoRecordClasses, classTag) {
decision.ShouldRecord = true
decision.Reason = "matched auto_record_classes"
decision.Reason = DecisionReasonRecordClass
} else if classTag == "" && hintTag != "" && WantsClass(policy.AutoRecordClasses, hintTag) {
decision.ShouldRecord = true
decision.Reason = "matched auto_record_classes (hint)"
decision.Reason = DecisionReasonRecordHint
}
if classTag != "" && WantsClass(policy.AutoDecodeClasses, classTag) {
decision.ShouldAutoDecode = true
if decision.Reason == "" {
decision.Reason = "matched auto_decode_classes"
decision.Reason = DecisionReasonDecodeClass
}
} else if classTag == "" && hintTag != "" && WantsClass(policy.AutoDecodeClasses, hintTag) {
decision.ShouldAutoDecode = true
if decision.Reason == "" {
decision.Reason = "matched auto_decode_classes (hint)"
decision.Reason = DecisionReasonDecodeHint
}
}
if decision.Reason == "" && candidate.Hint != "" {
decision.Reason = "policy evaluated candidate hint"
decision.Reason = DecisionReasonHintOnly
}
return decision
}

+ 17
- 10
internal/pipeline/scheduler.go Zobrazit soubor

@@ -62,16 +62,16 @@ const (
)

const (
RefinementReasonPlanned = "planned"
RefinementReasonAdmitted = "admitted"
RefinementReasonRunning = "running"
RefinementReasonCompleted = "completed"
RefinementReasonMonitorGate = "dropped:monitor"
RefinementReasonBelowSNR = "dropped:snr"
RefinementReasonBudget = "skipped:budget"
RefinementReasonDisabled = "dropped:disabled"
RefinementReasonUnclassified = "dropped:unclassified"
RefinementReasonDisplaced = "skipped:displaced"
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.
@@ -203,7 +203,14 @@ func ScheduleCandidates(candidates []Candidate, policy Policy) []ScheduledCandid

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"):


+ 11
- 0
internal/pipeline/scheduler_test.go Zobrazit soubor

@@ -226,6 +226,17 @@ func TestAdmitRefinementPlanDisplacedByHold(t *testing.T) {
}
}

func TestRefinementStrategyUsesProfile(t *testing.T) {
strategy, reason := refinementStrategy(Policy{Profile: "digital-hunting"})
if strategy != "digital-hunting" || reason != "profile" {
t.Fatalf("expected digital profile to set strategy, got %s (%s)", strategy, reason)
}
strategy, reason = refinementStrategy(Policy{Profile: "archive"})
if strategy != "archive-oriented" || reason != "profile" {
t.Fatalf("expected archive profile to set strategy, got %s (%s)", strategy, reason)
}
}

func findWorkItem(items []RefinementWorkItem, id int64) *RefinementWorkItem {
for i := range items {
if items[i].Candidate.ID == id {


Načítá se…
Zrušit
Uložit