| @@ -135,12 +135,16 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det * | |||
| debugInfo = &SpectrumDebug{Thresholds: thresholds, NoiseFloor: noiseFloor, Scores: scoreDebug} | |||
| candidateSources := buildCandidateSourceSummary(state.surveillance.Candidates) | |||
| candidateEvidence := buildCandidateEvidenceSummary(state.surveillance.Candidates) | |||
| candidateEvidenceStates := buildCandidateEvidenceStateSummary(state.surveillance.Candidates) | |||
| if len(candidateSources) > 0 { | |||
| debugInfo.CandidateSources = candidateSources | |||
| } | |||
| if len(candidateEvidence) > 0 { | |||
| debugInfo.CandidateEvidence = candidateEvidence | |||
| } | |||
| if candidateEvidenceStates != nil { | |||
| debugInfo.CandidateEvidenceStates = candidateEvidenceStates | |||
| } | |||
| if hasPlan { | |||
| debugInfo.RefinementPlan = &plan | |||
| } | |||
| @@ -176,6 +176,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||
| levelSummaries := buildSurveillanceLevelSummaries(levelSet, snap.surveillance.Spectra) | |||
| candidateSources := buildCandidateSourceSummary(snap.surveillance.Candidates) | |||
| candidateEvidence := buildCandidateEvidenceSummary(snap.surveillance.Candidates) | |||
| candidateEvidenceStates := buildCandidateEvidenceStateSummary(snap.surveillance.Candidates) | |||
| out := map[string]any{ | |||
| "plan": snap.refinement.Input.Plan, | |||
| "windows": snap.refinement.Input.Windows, | |||
| @@ -210,6 +211,7 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime | |||
| "surveillance_spectra_bins": spectraBins, | |||
| "candidate_sources": candidateSources, | |||
| "candidate_evidence": candidateEvidence, | |||
| "candidate_evidence_states": candidateEvidenceStates, | |||
| "display_level": snap.surveillance.DisplayLevel, | |||
| "refinement_level": snap.refinement.Input.Level, | |||
| "presentation_level": snap.presentation, | |||
| @@ -26,6 +26,15 @@ type CandidateEvidenceSummary struct { | |||
| Count int `json:"count"` | |||
| } | |||
| type CandidateEvidenceStateSummary struct { | |||
| Total int `json:"total"` | |||
| WithEvidence int `json:"with_evidence"` | |||
| Fused int `json:"fused"` | |||
| MultiLevelConfirmed int `json:"multi_level_confirmed"` | |||
| DerivedOnly int `json:"derived_only"` | |||
| PrimaryOnly int `json:"primary_only"` | |||
| } | |||
| func buildSurveillanceLevelSummaries(set pipeline.SurveillanceLevelSet, spectra []pipeline.SurveillanceLevelSpectrum) map[string]SurveillanceLevelSummary { | |||
| if set.Primary.Name == "" && len(set.Derived) == 0 && set.Presentation.Name == "" && len(set.All) == 0 { | |||
| return nil | |||
| @@ -133,3 +142,33 @@ func buildCandidateEvidenceSummary(candidates []pipeline.Candidate) []CandidateE | |||
| }) | |||
| return out | |||
| } | |||
| func buildCandidateEvidenceStateSummary(candidates []pipeline.Candidate) *CandidateEvidenceStateSummary { | |||
| if len(candidates) == 0 { | |||
| return nil | |||
| } | |||
| summary := CandidateEvidenceStateSummary{Total: len(candidates)} | |||
| for _, cand := range candidates { | |||
| state := pipeline.CandidateEvidenceStateFor(cand) | |||
| if state.TotalLevelEntries == 0 { | |||
| continue | |||
| } | |||
| summary.WithEvidence++ | |||
| if state.Fused { | |||
| summary.Fused++ | |||
| } | |||
| if state.MultiLevelConfirmed { | |||
| summary.MultiLevelConfirmed++ | |||
| } | |||
| if state.DerivedOnly { | |||
| summary.DerivedOnly++ | |||
| } | |||
| if state.PrimaryLevelCount > 0 && state.DerivedLevelCount == 0 { | |||
| summary.PrimaryOnly++ | |||
| } | |||
| } | |||
| if summary.WithEvidence == 0 { | |||
| return nil | |||
| } | |||
| return &summary | |||
| } | |||
| @@ -476,6 +476,9 @@ func (rt *dspRuntime) detectDerivedCandidates(art *spectrumArtifacts, plan surve | |||
| if level.Name == "" { | |||
| continue | |||
| } | |||
| if !pipeline.IsDetectionLevel(level) { | |||
| continue | |||
| } | |||
| spectrum := spectra[level.Name] | |||
| if len(spectrum) == 0 { | |||
| continue | |||
| @@ -14,15 +14,16 @@ import ( | |||
| ) | |||
| type SpectrumDebug struct { | |||
| Thresholds []float64 `json:"thresholds,omitempty"` | |||
| NoiseFloor float64 `json:"noise_floor,omitempty"` | |||
| Scores []map[string]any `json:"scores,omitempty"` | |||
| CandidateSources map[string]int `json:"candidate_sources,omitempty"` | |||
| CandidateEvidence []CandidateEvidenceSummary `json:"candidate_evidence,omitempty"` | |||
| RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"` | |||
| Windows *RefinementWindowStats `json:"refinement_windows,omitempty"` | |||
| Refinement *RefinementDebug `json:"refinement,omitempty"` | |||
| Decisions *DecisionDebug `json:"decisions,omitempty"` | |||
| Thresholds []float64 `json:"thresholds,omitempty"` | |||
| NoiseFloor float64 `json:"noise_floor,omitempty"` | |||
| Scores []map[string]any `json:"scores,omitempty"` | |||
| CandidateSources map[string]int `json:"candidate_sources,omitempty"` | |||
| CandidateEvidence []CandidateEvidenceSummary `json:"candidate_evidence,omitempty"` | |||
| CandidateEvidenceStates *CandidateEvidenceStateSummary `json:"candidate_evidence_states,omitempty"` | |||
| RefinementPlan *pipeline.RefinementPlan `json:"refinement_plan,omitempty"` | |||
| Windows *RefinementWindowStats `json:"refinement_windows,omitempty"` | |||
| Refinement *RefinementDebug `json:"refinement,omitempty"` | |||
| Decisions *DecisionDebug `json:"decisions,omitempty"` | |||
| } | |||
| type RefinementWindowStats struct { | |||