fix(bf-4qto): propagate canonical identity fields through all blob projections
The three canonical identity fields (PersonName, AssignedColor, IdentityResolved) had been added to every identity-bearing blob type (bf-5151/bf-2ibc/bf-5v3q/bf-3wkz) but three projection sites in the fusion loop were silently dropping them when converting the source sigproc.TrackedBlob: - explainability.BlobSnapshot (cmd/mothership/main.go) - automation.TrackedBlob (cmd/mothership/main.go) - volume.BlobPos (cmd/mothership/main.go) The api.Track (tracks.go) and dashboard.blobJSON (hub.go) projections already propagated them, so this left default-handling inconsistent across projection contexts. Propagate the three fields from the source blob at all three sites so the canonical fields flow through every projection instead of being zeroed. The fields stay at their Go zero values for existing blobs (identity is not yet resolved at the signal layer): empty strings and a nil *bool all carry omitempty, so they serialize as omitted (undefined in JS), which is the consistent default the acceptance criteria require. The BLE identity sidecar that actually populates them is a separate follow-up bead. Also add the companion serialization-consistency suites that were missing for the four types whose fields landed across the split beads: tracker.Blob, tracking.Blob, volume.BlobPos, explainability.BlobSnapshot. Each asserts the three task-required properties: zero-value blobs omit all three fields (undefined), a resolved identity emits the camelCase keys matching dashboard/types/spaxel.d.ts, and the *bool tri-state (nil=omitted, &true=emitted, &false still emits identityResolved:false). Verified: gofmt clean, go vet ./... clean, go test ./... passes (incl. the four new suites and the existing api/automation/signal ones). Co-Authored-By: Claude <noreply@anthropic.com> Bead-Id: bf-4qto
This commit is contained in:
parent
d0ac1b27b0
commit
ca7a0f21f3
7 changed files with 380 additions and 3 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
1446ccf65fe5366db9a1ac0e1adef608548473d5
|
||||
520f9fa4f18ac1ce23d4711f3768a46147174e9e
|
||||
|
|
|
|||
|
|
@ -2209,6 +2209,14 @@ func main() {
|
|||
Y: blob.Y,
|
||||
Z: blob.Z,
|
||||
Confidence: blob.Weight,
|
||||
// Canonical identity fields (bf-4qto): propagate from the source
|
||||
// sigproc.TrackedBlob so default-handling is consistent with the
|
||||
// api.Track (tracks.go) and dashboard.blobJSON (hub.go) projections.
|
||||
// Zero today (serializes as undefined); populated when the BLE
|
||||
// identity sidecar writes onto the source blob.
|
||||
PersonName: blob.PersonName,
|
||||
AssignedColor: blob.AssignedColor,
|
||||
IdentityResolved: blob.IdentityResolved,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2309,6 +2317,14 @@ func main() {
|
|||
VY: b.VY,
|
||||
VZ: b.VZ,
|
||||
Confidence: b.Weight,
|
||||
// Canonical identity fields (bf-4qto): propagate from the source
|
||||
// sigproc.TrackedBlob so default-handling is consistent with the
|
||||
// api.Track (tracks.go) and dashboard.blobJSON (hub.go) projections.
|
||||
// Zero today (serializes as undefined); populated when the BLE
|
||||
// identity sidecar writes onto the source blob.
|
||||
PersonName: b.PersonName,
|
||||
AssignedColor: b.AssignedColor,
|
||||
IdentityResolved: b.IdentityResolved,
|
||||
}
|
||||
}
|
||||
automationEngine.Evaluate(autoBlobs, func(blobID int) string {
|
||||
|
|
@ -2328,6 +2344,14 @@ func main() {
|
|||
X: blob.X,
|
||||
Y: blob.Y,
|
||||
Z: blob.Z,
|
||||
// Canonical identity fields (bf-4qto): propagate from the source
|
||||
// sigproc.TrackedBlob so default-handling is consistent with the
|
||||
// api.Track (tracks.go) and dashboard.blobJSON (hub.go) projections.
|
||||
// Zero today (serializes as undefined); populated when the BLE
|
||||
// identity sidecar writes onto the source blob.
|
||||
PersonName: blob.PersonName,
|
||||
AssignedColor: blob.AssignedColor,
|
||||
IdentityResolved: blob.IdentityResolved,
|
||||
}
|
||||
}
|
||||
volumeTriggersHandler.EvaluateTriggers(volumeBlobs)
|
||||
|
|
|
|||
88
mothership/internal/explainability/identity_fields_test.go
Normal file
88
mothership/internal/explainability/identity_fields_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package explainability
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// canonicalIdentityKeys is the set of bf-5151 canonical identity fields every
|
||||
// identity-bearing blob type must emit (camelCase, matching
|
||||
// dashboard/types/spaxel.d.ts). BlobSnapshot is the explainability ("Why is this
|
||||
// here?") view of a blob — the Tier-1 #2 type from bf-1q3m, which previously
|
||||
// lacked identity fields entirely. Its canonical fields were added in bf-5151
|
||||
// but left at zero values until a follow-up bead folds in the parallel
|
||||
// identityMap from the BLE identity sidecar.
|
||||
var canonicalIdentityKeys = []string{"personName", "assignedColor", "identityResolved"}
|
||||
|
||||
// TestBlobSnapshot_CanonicalIdentitySerialization guards the three canonical
|
||||
// identity fields on BlobSnapshot. Per the task scope they are left at their Go
|
||||
// zero values for existing blobs: with omitempty they must serialize as OMITTED
|
||||
// (undefined in JS). IdentityResolved is *bool so that a non-nil false
|
||||
// (resolution attempted, failed) is distinct from nil (unattempted) — omitempty
|
||||
// only drops the nil case. This locks in default-handling consistency with the
|
||||
// other identity-bearing blob types (signal.TrackedBlob, automation.TrackedBlob,
|
||||
// tracker.Blob, tracking.Blob, volume.BlobPos).
|
||||
func TestBlobSnapshot_CanonicalIdentitySerialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
blob BlobSnapshot
|
||||
wantKey map[string]bool // key → expected present in JSON
|
||||
}{
|
||||
{
|
||||
name: "zero-value blob omits all three (undefined)",
|
||||
blob: BlobSnapshot{ID: 1, X: 1, Y: 2, Z: 3, Confidence: 1},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resolved identity emits camelCase keys",
|
||||
blob: BlobSnapshot{
|
||||
ID: 2, PersonName: "Alice", AssignedColor: "#4488ff",
|
||||
IdentityResolved: ptrBoolExplain(true),
|
||||
},
|
||||
wantKey: map[string]bool{
|
||||
"personName": true, "assignedColor": true, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed resolution still emits identityResolved=false",
|
||||
blob: BlobSnapshot{ID: 3, IdentityResolved: ptrBoolExplain(false)},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
raw, err := json.Marshal(tc.blob)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
body := string(raw)
|
||||
for _, k := range canonicalIdentityKeys {
|
||||
got := strings.Contains(body, `"`+k+`"`)
|
||||
if got != tc.wantKey[k] {
|
||||
t.Errorf("key %q: present=%v, want %v (body=%s)", k, got, tc.wantKey[k], body)
|
||||
}
|
||||
}
|
||||
// Verify the tri-state value round-trips when present.
|
||||
if tc.wantKey["identityResolved"] {
|
||||
var decoded BlobSnapshot
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if decoded.IdentityResolved == nil {
|
||||
t.Fatalf("identityResolved unexpectedly nil after round-trip (body=%s)", body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ptrBoolExplain is a small helper for the *bool tri-state IdentityResolved field.
|
||||
func ptrBoolExplain(b bool) *bool { return &b }
|
||||
86
mothership/internal/tracker/identity_fields_test.go
Normal file
86
mothership/internal/tracker/identity_fields_test.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package tracker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// canonicalIdentityKeys is the set of bf-5151 canonical identity fields every
|
||||
// identity-bearing blob type must emit (camelCase, matching
|
||||
// dashboard/types/spaxel.d.ts). tracker.Blob is the 3D identity-bearing tracked
|
||||
// type; its canonical fields were added in bf-5151 but left at zero values until
|
||||
// a follow-up bead wires the BLE identity sidecar.
|
||||
var canonicalIdentityKeys = []string{"personName", "assignedColor", "identityResolved"}
|
||||
|
||||
// TestBlob_CanonicalIdentitySerialization guards the three bf-5151 canonical
|
||||
// identity fields on tracker.Blob. Per the task scope they are left at their Go
|
||||
// zero values for existing blobs: with omitempty they must serialize as OMITTED
|
||||
// (undefined in JS). IdentityResolved is *bool so that a non-nil false
|
||||
// (resolution attempted, failed) is distinct from nil (unattempted) — omitempty
|
||||
// only drops the nil case. This locks in default-handling consistency with the
|
||||
// other identity-bearing blob types (signal.TrackedBlob, automation.TrackedBlob,
|
||||
// tracking.Blob, volume.BlobPos, explainability.BlobSnapshot).
|
||||
func TestBlob_CanonicalIdentitySerialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
blob Blob
|
||||
wantKey map[string]bool // key → expected present in JSON
|
||||
}{
|
||||
{
|
||||
name: "zero-value blob omits all three (undefined)",
|
||||
blob: Blob{ID: 1, X: 1, Y: 2, Z: 3, Weight: 1},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resolved identity emits camelCase keys",
|
||||
blob: Blob{
|
||||
ID: 2, PersonName: "Alice", AssignedColor: "#4488ff",
|
||||
IdentityResolved: ptrBoolTracker(true),
|
||||
},
|
||||
wantKey: map[string]bool{
|
||||
"personName": true, "assignedColor": true, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed resolution still emits identityResolved=false",
|
||||
blob: Blob{ID: 3, IdentityResolved: ptrBoolTracker(false)},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
raw, err := json.Marshal(tc.blob)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
body := string(raw)
|
||||
for _, k := range canonicalIdentityKeys {
|
||||
got := strings.Contains(body, `"`+k+`"`)
|
||||
if got != tc.wantKey[k] {
|
||||
t.Errorf("key %q: present=%v, want %v (body=%s)", k, got, tc.wantKey[k], body)
|
||||
}
|
||||
}
|
||||
// Verify the tri-state value round-trips when present.
|
||||
if tc.wantKey["identityResolved"] {
|
||||
var decoded Blob
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if decoded.IdentityResolved == nil {
|
||||
t.Fatalf("identityResolved unexpectedly nil after round-trip (body=%s)", body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ptrBoolTracker is a small helper for the *bool tri-state IdentityResolved field.
|
||||
func ptrBoolTracker(b bool) *bool { return &b }
|
||||
86
mothership/internal/tracking/identity_fields_test.go
Normal file
86
mothership/internal/tracking/identity_fields_test.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package tracking
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// canonicalIdentityKeys is the set of bf-5151 canonical identity fields every
|
||||
// identity-bearing blob type must emit (camelCase, matching
|
||||
// dashboard/types/spaxel.d.ts). tracking.Blob is the 2D tracked type that backs
|
||||
// the dashboard WebSocket wire format; its canonical fields were added in bf-5151
|
||||
// but left at zero values until a follow-up bead wires the BLE identity sidecar.
|
||||
var canonicalIdentityKeys = []string{"personName", "assignedColor", "identityResolved"}
|
||||
|
||||
// TestBlob_CanonicalIdentitySerialization guards the three bf-5151 canonical
|
||||
// identity fields on tracking.Blob. Per the task scope they are left at their Go
|
||||
// zero values for existing blobs: with omitempty they must serialize as OMITTED
|
||||
// (undefined in JS). IdentityResolved is *bool so that a non-nil false
|
||||
// (resolution attempted, failed) is distinct from nil (unattempted) — omitempty
|
||||
// only drops the nil case. This locks in default-handling consistency with the
|
||||
// other identity-bearing blob types (signal.TrackedBlob, automation.TrackedBlob,
|
||||
// tracker.Blob, volume.BlobPos, explainability.BlobSnapshot).
|
||||
func TestBlob_CanonicalIdentitySerialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
blob Blob
|
||||
wantKey map[string]bool // key → expected present in JSON
|
||||
}{
|
||||
{
|
||||
name: "zero-value blob omits all three (undefined)",
|
||||
blob: Blob{ID: 1, X: 1, Z: 2, Weight: 1},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resolved identity emits camelCase keys",
|
||||
blob: Blob{
|
||||
ID: 2, PersonName: "Alice", AssignedColor: "#4488ff",
|
||||
IdentityResolved: ptrBoolTracking(true),
|
||||
},
|
||||
wantKey: map[string]bool{
|
||||
"personName": true, "assignedColor": true, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed resolution still emits identityResolved=false",
|
||||
blob: Blob{ID: 3, IdentityResolved: ptrBoolTracking(false)},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
raw, err := json.Marshal(tc.blob)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
body := string(raw)
|
||||
for _, k := range canonicalIdentityKeys {
|
||||
got := strings.Contains(body, `"`+k+`"`)
|
||||
if got != tc.wantKey[k] {
|
||||
t.Errorf("key %q: present=%v, want %v (body=%s)", k, got, tc.wantKey[k], body)
|
||||
}
|
||||
}
|
||||
// Verify the tri-state value round-trips when present.
|
||||
if tc.wantKey["identityResolved"] {
|
||||
var decoded Blob
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if decoded.IdentityResolved == nil {
|
||||
t.Fatalf("identityResolved unexpectedly nil after round-trip (body=%s)", body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ptrBoolTracking is a small helper for the *bool tri-state IdentityResolved field.
|
||||
func ptrBoolTracking(b bool) *bool { return &b }
|
||||
89
mothership/internal/volume/identity_fields_test.go
Normal file
89
mothership/internal/volume/identity_fields_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Package volume provides tests for trigger volume geometry and point-in-volume testing.
|
||||
package volume
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// canonicalIdentityKeys is the set of bf-5151 canonical identity fields every
|
||||
// identity-bearing blob type must emit (camelCase, matching
|
||||
// dashboard/types/spaxel.d.ts). volume.BlobPos is the per-blob position fed to
|
||||
// the trigger-volume evaluator; its canonical fields were added in bf-3wkz
|
||||
// (aligned with dashboard camelCase) but left at zero values until a follow-up
|
||||
// bead wires the BLE identity sidecar so person-aware volume triggers
|
||||
// ("when Alice enters…") can fire.
|
||||
var canonicalIdentityKeys = []string{"personName", "assignedColor", "identityResolved"}
|
||||
|
||||
// TestBlobPos_CanonicalIdentitySerialization guards the three canonical identity
|
||||
// fields on volume.BlobPos. Per the task scope they are left at their Go zero
|
||||
// values for existing blobs: with omitempty they must serialize as OMITTED
|
||||
// (undefined in JS). IdentityResolved is *bool so that a non-nil false
|
||||
// (resolution attempted, failed) is distinct from nil (unattempted) — omitempty
|
||||
// only drops the nil case. This locks in default-handling consistency with the
|
||||
// other identity-bearing blob types (signal.TrackedBlob, automation.TrackedBlob,
|
||||
// tracker.Blob, tracking.Blob, explainability.BlobSnapshot).
|
||||
func TestBlobPos_CanonicalIdentitySerialization(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
blob BlobPos
|
||||
wantKey map[string]bool // key → expected present in JSON
|
||||
}{
|
||||
{
|
||||
name: "zero-value blob omits all three (undefined)",
|
||||
blob: BlobPos{ID: 1, X: 1, Y: 2, Z: 3},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resolved identity emits camelCase keys",
|
||||
blob: BlobPos{
|
||||
ID: 2, PersonName: "Alice", AssignedColor: "#4488ff",
|
||||
IdentityResolved: ptrBoolVolume(true),
|
||||
},
|
||||
wantKey: map[string]bool{
|
||||
"personName": true, "assignedColor": true, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "failed resolution still emits identityResolved=false",
|
||||
blob: BlobPos{ID: 3, IdentityResolved: ptrBoolVolume(false)},
|
||||
wantKey: map[string]bool{
|
||||
"personName": false, "assignedColor": false, "identityResolved": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
raw, err := json.Marshal(tc.blob)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
body := string(raw)
|
||||
for _, k := range canonicalIdentityKeys {
|
||||
got := strings.Contains(body, `"`+k+`"`)
|
||||
if got != tc.wantKey[k] {
|
||||
t.Errorf("key %q: present=%v, want %v (body=%s)", k, got, tc.wantKey[k], body)
|
||||
}
|
||||
}
|
||||
// Verify the tri-state value round-trips when present.
|
||||
if tc.wantKey["identityResolved"] {
|
||||
var decoded BlobPos
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if decoded.IdentityResolved == nil {
|
||||
t.Fatalf("identityResolved unexpectedly nil after round-trip (body=%s)", body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ptrBoolVolume is a small helper for the *bool tri-state IdentityResolved field.
|
||||
func ptrBoolVolume(b bool) *bool { return &b }
|
||||
Loading…
Add table
Reference in a new issue