fix(bf-21v71): write BLE identity onto /api/blobs served slice (Stage 2b)
The diagnosis (bf-m1ynp, closed) pinned the broken link as match-found: a blob served by /api/blobs never carried canonical identity because nothing copied identityMatcher.GetMatch() back onto the served TrackedBlob slice — the existing GetMatch sites only fed explainability snapshots and the ground- truth collector, not the slice that SetTrackedBlobs publishes. Add a Stage 2b write-back right after identityMatcher.UpdateBlobs(): for each served blob, on a non-nil match copy PersonName/AssignedColor (+ the snake_case aliases and IdentityConfidence/Source/PersonID) and set IdentityResolved=&true; unmatched blobs get IdentityResolved=&false. Re-publish via SetTrackedBlobs so /api/blobs and /api/tracks serve resolved canonical identity whenever the matcher has a person match. Update the now-stale comments that claimed the identity fields are left at zero "until a follow-up bead wires the BLE sidecar" (signal/processor.go, api/tracks.go) and the superseded bf-2fz8 "SetTrackedBlobs has no callers" investigation note, to reflect that the live fusion loop now drives both. Build green: cd mothership && go build ./... Runtime observation that /api/blobs shows non-empty identity is the next child (requires the sim fixture: --ble + a registered person), explicitly out of scope per bf-21v71 acceptance criterion 2 (code-review-verifiable wiring). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c472b0358e
commit
09bd6e372d
3 changed files with 58 additions and 36 deletions
|
|
@ -2125,6 +2125,32 @@ func main() {
|
|||
}
|
||||
identityMatcher.UpdateBlobs(matcherBlobs)
|
||||
|
||||
// Stage 2b: write resolved BLE identity back onto the served
|
||||
// TrackedBlob slice (bf-5h1t). UpdateBlobs resolves matches,
|
||||
// but every runtime surface (/api/blobs, /api/tracks, the
|
||||
// explainability snapshots below) reads identity off the
|
||||
// TrackedBlob — so without this write-back live blobs always
|
||||
// carry empty identity even when a person is matched. Mark
|
||||
// IdentityResolved tri-state (nil=unattempted when no matcher,
|
||||
// &true=resolved, &false=attempted-but-unmatched) and re-publish.
|
||||
identityYes, identityNo := true, false
|
||||
for i := range blobs {
|
||||
match := identityMatcher.GetMatch(blobs[i].ID)
|
||||
if match == nil {
|
||||
blobs[i].IdentityResolved = &identityNo
|
||||
continue
|
||||
}
|
||||
blobs[i].PersonID = match.PersonID
|
||||
blobs[i].PersonLabel = match.PersonName
|
||||
blobs[i].PersonColor = match.PersonColor
|
||||
blobs[i].IdentityConfidence = match.Confidence
|
||||
blobs[i].IdentitySource = "ble"
|
||||
blobs[i].PersonName = match.PersonName
|
||||
blobs[i].AssignedColor = match.PersonColor
|
||||
blobs[i].IdentityResolved = &identityYes
|
||||
}
|
||||
pm.SetTrackedBlobs(blobs)
|
||||
|
||||
// Collect ground truth samples for self-improving localization
|
||||
if groundTruthCollector != nil {
|
||||
// Build per-link delta and health maps from motion states
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ type Track struct {
|
|||
IdentitySource string `json:"identity_source,omitempty"`
|
||||
// Canonical identity fields (bf-5151). camelCase JSON keys are the canonical
|
||||
// API-facing names; the snake_case PersonLabel/PersonColor above are retained
|
||||
// as deprecated aliases. Propagated from signal.TrackedBlob in listTracks —
|
||||
// zero today because the source struct is not yet populated (a follow-up bead
|
||||
// wires the BLE identity sidecar).
|
||||
// as deprecated aliases. Propagated from signal.TrackedBlob in listTracks;
|
||||
// the source struct is populated each fusion tick by the Stage 2 identity
|
||||
// write-back (cmd/mothership/main.go, guarded by `if identityMatcher != nil`)
|
||||
// from ble.IdentityMatcher.GetMatch, so a served track carries non-empty
|
||||
// identity whenever the matcher has a person match for that blob ID.
|
||||
PersonName string `json:"personName,omitempty"`
|
||||
AssignedColor string `json:"assignedColor,omitempty"`
|
||||
IdentityResolved *bool `json:"identityResolved,omitempty"` // tri-state: nil=unattempted, &true=resolved, &false=failed
|
||||
|
|
|
|||
|
|
@ -599,9 +599,14 @@ type TrackedBlob struct {
|
|||
|
||||
// Canonical identity fields (bf-5151). camelCase JSON keys match the dashboard
|
||||
// Blob type in dashboard/types/spaxel.d.ts; the snake_case PersonLabel/PersonColor
|
||||
// above are retained as deprecated aliases for backward compatibility. Left at
|
||||
// their zero values here (undefined) — a follow-up bead populates them from the
|
||||
// BLE identity sidecar.
|
||||
// above are retained as deprecated aliases for backward compatibility. Populated
|
||||
// by the live 10 Hz fusion loop's identity write-back (cmd/mothership/main.go,
|
||||
// the "Stage 2b" block guarded by `if identityMatcher != nil`): each tick it
|
||||
// calls identityMatcher.GetMatch(blobID) and, on a non-nil match, copies
|
||||
// PersonName/AssignedColor here and sets IdentityResolved=&true, then
|
||||
// re-publishes the slice via SetTrackedBlobs so /api/blobs and /api/tracks
|
||||
// serve them. Unmatched blobs get IdentityResolved=&false; the field stays nil
|
||||
// (unattempted) only when no identityMatcher is configured.
|
||||
PersonName string `json:"personName,omitempty"`
|
||||
AssignedColor string `json:"assignedColor,omitempty"`
|
||||
IdentityResolved *bool `json:"identityResolved,omitempty"` // tri-state: nil=unattempted, &true=resolved, &false=failed
|
||||
|
|
@ -609,43 +614,32 @@ 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
|
||||
// Live-path callers (the earlier bf-2fz8 "no callers / trackedBlobs always nil"
|
||||
// finding is resolved by the bf-3f6q + bf-243os fusion-track wiring):
|
||||
// - Stage 1 of the 10 Hz fusion loop publishes identity-less blobs right after
|
||||
// blobTracker derives them from the 3D Fresnel engine's peaks
|
||||
// (cmd/mothership/main.go).
|
||||
// - The Stage 2 identity write-back re-publishes the SAME slice with BLE
|
||||
// identity resolved onto each blob (PersonName/AssignedColor/IdentityResolved)
|
||||
// whenever identityMatcher.GetMatch returns a match, so served blobs carry
|
||||
// non-empty canonical identity.
|
||||
//
|
||||
// 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.
|
||||
// This setter remains the ONLY writer of pm.trackedBlobs; GetTrackedBlobs is the
|
||||
// matching reader. Readers include /api/blobs, /api/tracks, the 10 Hz dashboard
|
||||
// broadcast, anomaly positioning, and fall detection.
|
||||
func (pm *ProcessorManager) SetTrackedBlobs(blobs []TrackedBlob) {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
pm.trackedBlobs = blobs
|
||||
}
|
||||
|
||||
// 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.
|
||||
// GetTrackedBlobs returns the latest tracked blobs served to /api/blobs,
|
||||
// /api/tracks, the 10 Hz dashboard broadcast, anomaly positioning, and fall
|
||||
// detection. It reflects whatever the most recent SetTrackedBlobs stored —
|
||||
// i.e. the Stage 2 identity write-back's slice, which carries resolved
|
||||
// canonical identity (PersonName/AssignedColor/IdentityResolved) whenever a
|
||||
// matcher is configured and a person matches a blob. Returns nil only before
|
||||
// the first tracked blob is published.
|
||||
func (pm *ProcessorManager) GetTrackedBlobs() []TrackedBlob {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue