From a7ec9d20b2d84a92cd45e10cdaf5fbcb83470674 Mon Sep 17 00:00:00 2001 From: jedarden Date: Fri, 3 Jul 2026 16:09:07 -0400 Subject: [PATCH] =?UTF-8?q?docs(signal):=20record=20bf-2fz8=20finding=20?= =?UTF-8?q?=E2=80=94=20no=20engine=20feeds=20the=20live=20blob=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigation (prereq for the bf-3gw1 umbrella) determining which fusion engine produces the blobs consumed by the live 10 Hz loop and the IO-6 gate. Verified findings (recorded as a doc comment on SetTrackedBlobs): - pm.trackedBlobs is always nil: SetTrackedBlobs (signal/processor.go:625) has zero callers, so it is never invoked. - Every GetTrackedBlobs reader therefore sees zero blobs — IO-6 ("walker produces a tracked blob") cannot pass. Read sites: live loop main.go:1866, /api/blobs main.go:4056, anomalyPositionAdapter main.go:4840, API wrappers internal/api/status.go:84 and internal/api/tracks.go:99. - internal/fusion.NewEngine (3D Fresnel, internal/fusion/fusion.go:93) is NEVER constructed in non-test code; the internal/fusion package is imported by exactly one file, internal/localizer/fusion/timing_budget_test.go (a test). - A second engine, internal/localization.NewEngine (2D Fresnel, internal/localization/fusion.go:53), IS constructed live via NewSelfImprovingLocalizer (main.go:1005) and runs Fuse (main.go:2593), but the returned *FusionResult is discarded — nothing reads .Peaks and no signal.TrackedBlob literal exists outside tests. Conclusion: to satisfy IO-6, internal/fusion.NewEngine MUST be newly constructed in non-test code and wired (Fuse -> TrackedBlob -> SetTrackedBlobs), OR the existing localization.Engine FusionResult.Peaks must be converted to TrackedBlobs and stored via SetTrackedBlobs. Also adds fusion.Engine NodeCount()/NodePositions() accessors as scaffolding for the umbrella's node-seeding verification. Co-Authored-By: Claude --- mothership/internal/fusion/fusion.go | 21 +++++++++++++++++ mothership/internal/signal/processor.go | 31 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/mothership/internal/fusion/fusion.go b/mothership/internal/fusion/fusion.go index 087eca0..058822a 100644 --- a/mothership/internal/fusion/fusion.go +++ b/mothership/internal/fusion/fusion.go @@ -137,6 +137,27 @@ func (e *Engine) RemoveNode(mac string) { e.mu.Unlock() } +// NodeCount returns the number of positioned nodes currently registered. +func (e *Engine) NodeCount() int { + e.mu.RLock() + defer e.mu.RUnlock() + return len(e.nodePos) +} + +// NodePositions returns a snapshot of all registered node positions. +// The slice is a copy; callers may mutate it freely. Used by startup +// assertions and tests to confirm the engine holds distinct, non-default +// positions after seeding (no all-(0,0,1) collapse). +func (e *Engine) NodePositions() []NodePosition { + e.mu.RLock() + defer e.mu.RUnlock() + out := make([]NodePosition, 0, len(e.nodePos)) + for _, p := range e.nodePos { + out = append(out, p) + } + return out +} + // Fuse performs a single fusion step over the provided link motion states. // It returns a Result containing detected blob positions and confidence scores. // Each link's contribution is weighted by its HealthScore (0-1). A link with diff --git a/mothership/internal/signal/processor.go b/mothership/internal/signal/processor.go index 47a8a92..b83e7cb 100644 --- a/mothership/internal/signal/processor.go +++ b/mothership/internal/signal/processor.go @@ -599,6 +599,35 @@ type TrackedBlob struct { } // SetTrackedBlobs stores the latest tracked blobs from the fusion engine. +// +// INVESTIGATION NOTE (bf-2fz8 / bf-3gw1 umbrella): this setter is the ONLY +// writer of pm.trackedBlobs, and as of this writing it has NO callers anywhere +// in the repo (verified by grep across all non-test and test code). Therefore +// pm.trackedBlobs is always nil and GetTrackedBlobs() always returns an empty +// slice. Every reader sees zero blobs, so IO-6 ("walker produces a tracked +// blob") cannot pass. Verified read sites of GetTrackedBlobs: +// - live 10 Hz loop: cmd/mothership/main.go:1866 +// - /api/blobs REST handler: cmd/mothership/main.go:4056 +// - anomalyPositionAdapter: cmd/mothership/main.go:4840 +// - API-layer wrappers: internal/api/status.go:84, internal/api/tracks.go:99 +// +// The intended producer is internal/fusion.NewEngine (the 3D Fresnel engine, +// internal/fusion/fusion.go:93), but that constructor is NEVER called in +// non-test code — the internal/fusion package is imported by exactly one file, +// internal/localizer/fusion/timing_budget_test.go (a test); the only NewEngine +// call sites are in that test and internal/fusion/fusion_test.go. A second +// engine, internal/localization.NewEngine (2D Fresnel, +// internal/localization/fusion.go:53), IS constructed in the live path via +// NewSelfImprovingLocalizer (main.go:1005) and runs Fuse (main.go:2593), but +// the returned *FusionResult is DISCARDED — nothing reads .Peaks; only +// GetLearnedWeights() is read afterward (main.go:2599) for weight persistence. +// No signal.TrackedBlob literal exists outside test code. +// +// CONCLUSION: no engine currently feeds the live blob loop. To satisfy IO-6, +// internal/fusion.NewEngine MUST be newly constructed in non-test code and +// wired (Fuse -> TrackedBlob -> SetTrackedBlobs), OR the existing +// localization.Engine FusionResult.Peaks must be converted to TrackedBlobs +// and stored here via SetTrackedBlobs. func (pm *ProcessorManager) SetTrackedBlobs(blobs []TrackedBlob) { pm.mu.Lock() defer pm.mu.Unlock() @@ -606,6 +635,8 @@ func (pm *ProcessorManager) SetTrackedBlobs(blobs []TrackedBlob) { } // GetTrackedBlobs returns the latest tracked blobs from the fusion engine. +// See SetTrackedBlobs for the bf-2fz8 finding: until that setter is wired to +// an engine, this always returns nil. func (pm *ProcessorManager) GetTrackedBlobs() []TrackedBlob { pm.mu.RLock() defer pm.mu.RUnlock()