docs(signal): record bf-2fz8 finding — no engine feeds the live blob loop
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run

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 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-07-03 16:09:07 -04:00
parent ca1b51007d
commit a7ec9d20b2
2 changed files with 52 additions and 0 deletions

View file

@ -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

View file

@ -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()