Просмотр исходного кода

Clarify derived detection policy and evidence summaries

master
Jan Svabenik 4 часов назад
Родитель
Сommit
9798fd7cba
6 измененных файлов: 168 добавлений и 11 удалений
  1. +45
    -3
      cmd/sdrd/level_summary.go
  2. +12
    -0
      cmd/sdrd/pipeline_runtime.go
  3. +10
    -4
      internal/pipeline/evidence.go
  4. +49
    -0
      internal/pipeline/evidence_test.go
  5. +20
    -4
      internal/pipeline/surveillance_policy.go
  6. +32
    -0
      internal/pipeline/surveillance_policy_test.go

+ 45
- 3
cmd/sdrd/level_summary.go Просмотреть файл

@@ -2,6 +2,7 @@ package main

import (
"sort"
"strings"

"sdr-wideband-suite/internal/pipeline"
)
@@ -10,6 +11,7 @@ type SurveillanceLevelSummary struct {
Name string `json:"name"`
Role string `json:"role,omitempty"`
Truth string `json:"truth,omitempty"`
Kind string `json:"kind,omitempty"`
SampleRate int `json:"sample_rate,omitempty"`
FFTSize int `json:"fft_size,omitempty"`
BinHz float64 `json:"bin_hz,omitempty"`
@@ -22,6 +24,8 @@ type SurveillanceLevelSummary struct {

type CandidateEvidenceSummary struct {
Level string `json:"level"`
Role string `json:"role,omitempty"`
Kind string `json:"kind,omitempty"`
Provenance string `json:"provenance,omitempty"`
Count int `json:"count"`
}
@@ -32,6 +36,10 @@ type CandidateEvidenceStateSummary struct {
Fused int `json:"fused"`
MultiLevelConfirmed int `json:"multi_level_confirmed"`
DerivedOnly int `json:"derived_only"`
SupportOnly int `json:"support_only"`
PrimaryPresent int `json:"primary_present"`
DerivedPresent int `json:"derived_present"`
SupportPresent int `json:"support_present"`
PrimaryOnly int `json:"primary_only"`
}

@@ -71,10 +79,12 @@ func buildSurveillanceLevelSummaries(set pipeline.SurveillanceLevelSet, spectra
if binHz == 0 && level.SampleRate > 0 && level.FFTSize > 0 {
binHz = float64(level.SampleRate) / float64(level.FFTSize)
}
kind := evidenceKind(level)
out[name] = SurveillanceLevelSummary{
Name: name,
Role: level.Role,
Truth: level.Truth,
Kind: kind,
SampleRate: level.SampleRate,
FFTSize: level.FFTSize,
BinHz: binHz,
@@ -114,6 +124,8 @@ func buildCandidateEvidenceSummary(candidates []pipeline.Candidate) []CandidateE
}
type key struct {
level string
role string
kind string
provenance string
}
counts := map[key]int{}
@@ -123,7 +135,9 @@ func buildCandidateEvidenceSummary(candidates []pipeline.Candidate) []CandidateE
if name == "" {
name = "unknown"
}
k := key{level: name, provenance: ev.Provenance}
role := strings.TrimSpace(ev.Level.Role)
kind := evidenceKind(ev.Level)
k := key{level: name, role: role, kind: kind, provenance: ev.Provenance}
counts[k]++
}
}
@@ -132,12 +146,15 @@ func buildCandidateEvidenceSummary(candidates []pipeline.Candidate) []CandidateE
}
out := make([]CandidateEvidenceSummary, 0, len(counts))
for k, v := range counts {
out = append(out, CandidateEvidenceSummary{Level: k.level, Provenance: k.provenance, Count: v})
out = append(out, CandidateEvidenceSummary{Level: k.level, Role: k.role, Kind: k.kind, Provenance: k.provenance, Count: v})
}
sort.Slice(out, func(i, j int) bool {
if out[i].Count == out[j].Count {
if out[i].Level == out[j].Level {
return out[i].Provenance < out[j].Provenance
if out[i].Kind == out[j].Kind {
return out[i].Provenance < out[j].Provenance
}
return out[i].Kind < out[j].Kind
}
return out[i].Level < out[j].Level
}
@@ -166,6 +183,18 @@ func buildCandidateEvidenceStateSummary(candidates []pipeline.Candidate) *Candid
if state.DerivedOnly {
summary.DerivedOnly++
}
if state.SupportOnly {
summary.SupportOnly++
}
if state.PrimaryLevelCount > 0 {
summary.PrimaryPresent++
}
if state.DerivedLevelCount > 0 {
summary.DerivedPresent++
}
if state.SupportLevelCount > 0 {
summary.SupportPresent++
}
if state.PrimaryLevelCount > 0 && state.DerivedLevelCount == 0 {
summary.PrimaryOnly++
}
@@ -175,3 +204,16 @@ func buildCandidateEvidenceStateSummary(candidates []pipeline.Candidate) *Candid
}
return &summary
}

func evidenceKind(level pipeline.AnalysisLevel) string {
if pipeline.IsPresentationLevel(level) {
return "presentation"
}
if pipeline.IsSupportLevel(level) {
return "support"
}
if pipeline.IsDetectionLevel(level) {
return "detection"
}
return "unknown"
}

+ 12
- 0
cmd/sdrd/pipeline_runtime.go Просмотреть файл

@@ -898,6 +898,18 @@ func (rt *dspRuntime) buildSurveillancePlan(policy pipeline.Policy) surveillance

presentation := analysisLevel("presentation", pipeline.RolePresentation, "presentation", baseRate, rt.cfg.Surveillance.DisplayBins, rt.cfg.CenterHz, span, "display", 1, baseRate)
context.Presentation = presentation
if len(derivedLevels) == 0 && detectionPolicy.DerivedDetectionEnabled {
detectionPolicy.DerivedDetectionEnabled = false
detectionPolicy.DerivedDetectionReason = "levels"
}
switch {
case len(derivedLevels) > 0:
detectionPolicy.DerivedDetectionMode = "detection"
case len(supportLevels) > 0:
detectionPolicy.DerivedDetectionMode = "support"
default:
detectionPolicy.DerivedDetectionMode = "disabled"
}
levelSet := pipeline.SurveillanceLevelSet{
Primary: primary,
Derived: append([]pipeline.AnalysisLevel(nil), derivedLevels...),


+ 10
- 4
internal/pipeline/evidence.go Просмотреть файл

@@ -26,6 +26,7 @@ type CandidateEvidenceState struct {
Provenance []string `json:"provenance,omitempty"`
Fused bool `json:"fused,omitempty"`
DerivedOnly bool `json:"derived_only,omitempty"`
SupportOnly bool `json:"support_only,omitempty"`
MultiLevelConfirmed bool `json:"multi_level_confirmed,omitempty"`
MultiLevelConfirmedHint string `json:"multi_level_confirmed_hint,omitempty"`
}
@@ -65,10 +66,14 @@ func IsPresentationLevel(level AnalysisLevel) bool {
// IsSupportLevel reports whether a level is a non-detection support level.
func IsSupportLevel(level AnalysisLevel) bool {
role := strings.ToLower(strings.TrimSpace(level.Role))
name := strings.ToLower(strings.TrimSpace(level.Name))
if role == RoleSurveillanceSupport {
return true
}
return strings.Contains(role, "surveillance-support") || strings.Contains(role, "support")
if strings.Contains(role, "surveillance-support") || strings.Contains(role, "support") {
return true
}
return strings.Contains(name, "support")
}

// IsDetectionLevel reports whether a level is intended for detection/analysis.
@@ -107,11 +112,11 @@ func isPrimarySurveillanceLevel(level AnalysisLevel) bool {
}

func isDerivedSurveillanceLevel(level AnalysisLevel) bool {
role := strings.ToLower(strings.TrimSpace(level.Role))
name := strings.ToLower(strings.TrimSpace(level.Name))
if role == RoleSurveillanceSupport {
if IsSupportLevel(level) {
return false
}
role := strings.ToLower(strings.TrimSpace(level.Role))
name := strings.ToLower(strings.TrimSpace(level.Name))
if role == RoleSurveillanceDerived {
return true
}
@@ -181,6 +186,7 @@ func CandidateEvidenceStateFor(candidate Candidate) CandidateEvidenceState {
state.Provenance = sortedKeys(provenanceSet)
state.Fused = state.LevelCount > 1 || len(state.Provenance) > 1
state.DerivedOnly = state.DerivedLevelCount > 0 && state.PrimaryLevelCount == 0 && state.DetectionLevelCount == state.DerivedLevelCount
state.SupportOnly = state.SupportLevelCount > 0 && state.DetectionLevelCount == 0 && state.PresentationLevelCount == 0
state.MultiLevelConfirmed = state.DetectionLevelCount >= 2
if state.MultiLevelConfirmed {
if state.PrimaryLevelCount > 0 && state.DerivedLevelCount > 0 {


+ 49
- 0
internal/pipeline/evidence_test.go Просмотреть файл

@@ -43,3 +43,52 @@ func TestCandidateEvidenceStateTracksSupportLevels(t *testing.T) {
t.Fatalf("unexpected confirmation flags: %+v", state)
}
}

func TestCandidateEvidenceStateSupportOnly(t *testing.T) {
candidate := Candidate{
ID: 2,
Evidence: []LevelEvidence{
{Level: AnalysisLevel{Name: "surveillance-support", Role: RoleSurveillanceSupport, Truth: "surveillance"}, Provenance: "support"},
},
}
state := CandidateEvidenceStateFor(candidate)
if state.DetectionLevelCount != 0 || state.SupportLevelCount != 1 {
t.Fatalf("unexpected support-only counts: %+v", state)
}
if !state.SupportOnly || state.DerivedOnly || state.MultiLevelConfirmed {
t.Fatalf("unexpected support-only flags: %+v", state)
}
}

func TestCandidateEvidenceStatePrimaryWithSupport(t *testing.T) {
candidate := Candidate{
ID: 3,
Evidence: []LevelEvidence{
{Level: AnalysisLevel{Name: "surveillance", Role: RoleSurveillancePrimary, Truth: "surveillance"}, Provenance: "primary"},
{Level: AnalysisLevel{Name: "surveillance-support", Role: RoleSurveillanceSupport, Truth: "surveillance"}, Provenance: "support"},
},
}
state := CandidateEvidenceStateFor(candidate)
if state.DetectionLevelCount != 1 || state.SupportLevelCount != 1 {
t.Fatalf("unexpected primary+support counts: %+v", state)
}
if state.SupportOnly || state.DerivedOnly || state.MultiLevelConfirmed {
t.Fatalf("unexpected primary+support flags: %+v", state)
}
}

func TestCandidateEvidenceStateDerivedOnly(t *testing.T) {
candidate := Candidate{
ID: 4,
Evidence: []LevelEvidence{
{Level: AnalysisLevel{Name: "surveillance-lowres", Role: RoleSurveillanceDerived, Truth: "surveillance"}, Provenance: "derived"},
},
}
state := CandidateEvidenceStateFor(candidate)
if state.DetectionLevelCount != 1 || state.DerivedLevelCount != 1 {
t.Fatalf("unexpected derived-only counts: %+v", state)
}
if !state.DerivedOnly || state.SupportOnly || state.MultiLevelConfirmed {
t.Fatalf("unexpected derived-only flags: %+v", state)
}
}

+ 20
- 4
internal/pipeline/surveillance_policy.go Просмотреть файл

@@ -7,6 +7,7 @@ type SurveillanceDetectionPolicy struct {
DerivedDetection string `json:"derived_detection"`
DerivedDetectionEnabled bool `json:"derived_detection_enabled"`
DerivedDetectionReason string `json:"derived_detection_reason,omitempty"`
DerivedDetectionMode string `json:"derived_detection_mode,omitempty"`
PrimaryRole string `json:"primary_role"`
DerivedRole string `json:"derived_role"`
SupportRole string `json:"support_role"`
@@ -38,17 +39,23 @@ func strategyIsMulti(strategy string) bool {
// SurveillanceDetectionPolicyFromPolicy derives detection governance from policy intent/profile.
func SurveillanceDetectionPolicyFromPolicy(policy Policy) SurveillanceDetectionPolicy {
mode := normalizeDerivedDetection(policy.SurveillanceDerivedDetection)
strategyMulti := strategyIsMulti(policy.SurveillanceStrategy)
enabled := false
reason := ""
switch mode {
case "on":
enabled = true
reason = "config"
if strategyMulti {
enabled = true
reason = "config"
} else {
enabled = false
reason = "strategy"
}
case "off":
enabled = false
reason = "config"
default:
if !strategyIsMulti(policy.SurveillanceStrategy) {
if !strategyMulti {
enabled = false
reason = "strategy"
} else {
@@ -64,14 +71,23 @@ func SurveillanceDetectionPolicyFromPolicy(policy Policy) SurveillanceDetectionP
reason = "legacy"
default:
enabled = true
reason = "strategy"
reason = "auto"
}
}
}
modeState := "disabled"
if strategyMulti {
if enabled {
modeState = "detection"
} else {
modeState = "support"
}
}
return SurveillanceDetectionPolicy{
DerivedDetection: mode,
DerivedDetectionEnabled: enabled,
DerivedDetectionReason: reason,
DerivedDetectionMode: modeState,
PrimaryRole: RoleSurveillancePrimary,
DerivedRole: RoleSurveillanceDerived,
SupportRole: RoleSurveillanceSupport,


+ 32
- 0
internal/pipeline/surveillance_policy_test.go Просмотреть файл

@@ -14,6 +14,9 @@ func TestSurveillanceDetectionPolicyAuto(t *testing.T) {
if !got.DerivedDetectionEnabled {
t.Fatalf("expected auto policy to enable derived detection, got %+v", got)
}
if got.DerivedDetectionMode != "detection" {
t.Fatalf("expected detection mode, got %+v", got)
}

policy.Profile = "archive"
policy.Intent = "archive-and-triage"
@@ -21,10 +24,39 @@ func TestSurveillanceDetectionPolicyAuto(t *testing.T) {
if got.DerivedDetectionEnabled {
t.Fatalf("expected archive policy to disable derived detection, got %+v", got)
}
if got.DerivedDetectionMode != "support" {
t.Fatalf("expected support mode for archive policy, got %+v", got)
}

policy = Policy{SurveillanceStrategy: "single-resolution", SurveillanceDerivedDetection: "auto"}
got = SurveillanceDetectionPolicyFromPolicy(policy)
if got.DerivedDetectionEnabled {
t.Fatalf("expected single-resolution to disable derived detection, got %+v", got)
}
if got.DerivedDetectionMode != "disabled" {
t.Fatalf("expected disabled mode for single-resolution, got %+v", got)
}
}

func TestSurveillanceDetectionPolicyOverrides(t *testing.T) {
policy := Policy{SurveillanceStrategy: "single-resolution", SurveillanceDerivedDetection: "on"}
got := SurveillanceDetectionPolicyFromPolicy(policy)
if got.DerivedDetectionEnabled {
t.Fatalf("expected single-resolution to force derived detection off, got %+v", got)
}
if got.DerivedDetectionReason != "strategy" || got.DerivedDetectionMode != "disabled" {
t.Fatalf("expected strategy-based disable, got %+v", got)
}

policy = Policy{SurveillanceStrategy: "multi-resolution", SurveillanceDerivedDetection: "off"}
got = SurveillanceDetectionPolicyFromPolicy(policy)
if got.DerivedDetectionEnabled || got.DerivedDetectionMode != "support" {
t.Fatalf("expected off to yield support mode, got %+v", got)
}

policy = Policy{SurveillanceStrategy: "multi-resolution", SurveillanceDerivedDetection: "on", Profile: "archive"}
got = SurveillanceDetectionPolicyFromPolicy(policy)
if !got.DerivedDetectionEnabled || got.DerivedDetectionReason != "config" || got.DerivedDetectionMode != "detection" {
t.Fatalf("expected config on to override archive, got %+v", got)
}
}

Загрузка…
Отмена
Сохранить