docs(bf-1mwy): coverage gate proving every blob-creation site accounted for
Re-run the bf-1q3m §1.1 inventory greps at HEAD 37d628e and map every production blob-creation site (5 primary + 16 projection = 21) to either "has the 3 canonical identity fields" or "out-of-scope with explicit reason" in notes/bf-5151-coverage.md. This is the coverage gate closing bf-5151's "No blob creation code is missed" acceptance criterion. All 8 in-scope types confirmed carrying PersonName/AssignedColor/ IdentityResolved at zero values: signal.TrackedBlob, tracker.Blob, tracking.Blob, automation.TrackedBlob, api.Track, dashboard.blobJSON, explainability.BlobSnapshot, volume.BlobPos. All 15 out-of-scope sites enumerated by reason (pre-identity peak C1; synthetic replay/sim P8-P10; anon falldetect input P3; lifecycle events P11/P12; trigger-state volume.BlobState P13-P16; rendering fallbacks P4-P6; distinct-name falldetect.BlobSnapshot P7; dead api.BlobPos) -- none silently dropped. Also notes the task-cited SHAs 1446ccf/248d0e0 are pre-rebase dangling commits superseded by a612584/01a415d in HEAD. gofmt/vet/test gate: 7 files carried pre-existing gofmt drift unrelated to the identity work (verified unmodified by this bead before the format pass); fixed mechanically with gofmt -w to satisfy the clean-gate requirement. gofmt -l empty, go vet ./... exit 0, go test ./... all ok. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
908adf2049
commit
7f697a388d
8 changed files with 331 additions and 50 deletions
|
|
@ -61,8 +61,8 @@ import (
|
||||||
"github.com/spaxel/mothership/internal/replay"
|
"github.com/spaxel/mothership/internal/replay"
|
||||||
"github.com/spaxel/mothership/internal/shutdown"
|
"github.com/spaxel/mothership/internal/shutdown"
|
||||||
sigproc "github.com/spaxel/mothership/internal/signal"
|
sigproc "github.com/spaxel/mothership/internal/signal"
|
||||||
"github.com/spaxel/mothership/internal/sleep"
|
|
||||||
"github.com/spaxel/mothership/internal/simulator"
|
"github.com/spaxel/mothership/internal/simulator"
|
||||||
|
"github.com/spaxel/mothership/internal/sleep"
|
||||||
"github.com/spaxel/mothership/internal/startup"
|
"github.com/spaxel/mothership/internal/startup"
|
||||||
"github.com/spaxel/mothership/internal/timeline"
|
"github.com/spaxel/mothership/internal/timeline"
|
||||||
"github.com/spaxel/mothership/internal/volume"
|
"github.com/spaxel/mothership/internal/volume"
|
||||||
|
|
|
||||||
|
|
@ -147,10 +147,10 @@ func TestAddNode_KeepsExplicitPosition(t *testing.T) {
|
||||||
// effects of SyncToRegistry so tests can assert the simulator API drove an
|
// effects of SyncToRegistry so tests can assert the simulator API drove an
|
||||||
// immediate write into the fleet registry (rather than only logging).
|
// immediate write into the fleet registry (rather than only logging).
|
||||||
type recordingRegistry struct {
|
type recordingRegistry struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
added []simulator.NodeRecord
|
added []simulator.NodeRecord
|
||||||
posUpdates []simulator.NodeRecord
|
posUpdates []simulator.NodeRecord
|
||||||
nodesByMAC map[string]simulator.NodeRecord
|
nodesByMAC map[string]simulator.NodeRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRecordingRegistry() *recordingRegistry {
|
func newRecordingRegistry() *recordingRegistry {
|
||||||
|
|
@ -274,8 +274,8 @@ func TestImmediateSyncOnAddAndUpdate(t *testing.T) {
|
||||||
|
|
||||||
// AddNode: node must reach the registry synchronously, before any ticker.
|
// AddNode: node must reach the registry synchronously, before any ticker.
|
||||||
node := postNode(t, h, map[string]interface{}{
|
node := postNode(t, h, map[string]interface{}{
|
||||||
"id": "node-A",
|
"id": "node-A",
|
||||||
"name": "Node A",
|
"name": "Node A",
|
||||||
"position": map[string]interface{}{"x": 1.0, "y": 1.0, "z": 1.0},
|
"position": map[string]interface{}{"x": 1.0, "y": 1.0, "z": 1.0},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -297,8 +297,8 @@ func TestImmediateSyncOnAddAndUpdate(t *testing.T) {
|
||||||
|
|
||||||
// UpdateNode: position change must push through immediately.
|
// UpdateNode: position change must push through immediately.
|
||||||
putNode(t, h, "node-A", map[string]interface{}{
|
putNode(t, h, "node-A", map[string]interface{}{
|
||||||
"id": "node-A",
|
"id": "node-A",
|
||||||
"name": "Node A",
|
"name": "Node A",
|
||||||
"position": map[string]interface{}{"x": 2.5, "y": 3.0, "z": 1.5},
|
"position": map[string]interface{}{"x": 2.5, "y": 3.0, "z": 1.5},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -665,9 +665,9 @@ func TestSimulatorHandlerToRegistry_OriginNodesGetSpreadGeometry(t *testing.T) {
|
||||||
// single test. One node position, posted once through the simulator REST API,
|
// single test. One node position, posted once through the simulator REST API,
|
||||||
// must arrive — unchanged and in order — at every stage the runtime cares about:
|
// must arrive — unchanged and in order — at every stage the runtime cares about:
|
||||||
//
|
//
|
||||||
// 1. simulator VirtualNodeStore — the source of truth for virtual nodes
|
// 1. simulator VirtualNodeStore — the source of truth for virtual nodes
|
||||||
// 2. fleet.Registry (SQLite) — persisted; read by the dashboard/REST
|
// 2. fleet.Registry (SQLite) — persisted; read by the dashboard/REST
|
||||||
// 3. fusion.Engine nodePos mirror — what Fuse localizes against at runtime
|
// 3. fusion.Engine nodePos mirror — what Fuse localizes against at runtime
|
||||||
//
|
//
|
||||||
// bf-69ym locked in stages 1→2; bf-u7ds locked in stages 1→3. This test ties
|
// bf-69ym locked in stages 1→2; bf-u7ds locked in stages 1→3. This test ties
|
||||||
// them together: it traces ONE position value through all three stages in a
|
// them together: it traces ONE position value through all three stages in a
|
||||||
|
|
@ -819,13 +819,13 @@ func TestPositionPropagation_ReachesAccumulationGrid(t *testing.T) {
|
||||||
midX, midZ := (posA.X+posB.X)/2, (posA.Z+posB.Z)/2
|
midX, midZ := (posA.X+posB.X)/2, (posA.Z+posB.Z)/2
|
||||||
|
|
||||||
postNode(t, h, map[string]interface{}{
|
postNode(t, h, map[string]interface{}{
|
||||||
"id": "node-A",
|
"id": "node-A",
|
||||||
"name": "Node A",
|
"name": "Node A",
|
||||||
"position": map[string]interface{}{"x": posA.X, "y": posA.Y, "z": posA.Z},
|
"position": map[string]interface{}{"x": posA.X, "y": posA.Y, "z": posA.Z},
|
||||||
})
|
})
|
||||||
postNode(t, h, map[string]interface{}{
|
postNode(t, h, map[string]interface{}{
|
||||||
"id": "node-B",
|
"id": "node-B",
|
||||||
"name": "Node B",
|
"name": "Node B",
|
||||||
"position": map[string]interface{}{"x": posB.X, "y": posB.Y, "z": posB.Z},
|
"position": map[string]interface{}{"x": posB.X, "y": posB.Y, "z": posB.Z},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1539,8 +1539,8 @@ func TestManagerOnNodeConnectedPersistsHelloPosition(t *testing.T) {
|
||||||
f64 := func(v float64) *float64 { return &v }
|
f64 := func(v float64) *float64 { return &v }
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
posX, posY, posZ float64
|
posX, posY, posZ float64
|
||||||
}{
|
}{
|
||||||
{name: "corner_geometry", posX: 6.0, posY: 0.0, posZ: 2.5},
|
{name: "corner_geometry", posX: 6.0, posY: 0.0, posZ: 2.5},
|
||||||
{name: "interior_point", posX: 3.25, posY: 2.75, posZ: 1.1},
|
{name: "interior_point", posX: 3.25, posY: 2.75, posZ: 1.1},
|
||||||
|
|
|
||||||
|
|
@ -419,12 +419,12 @@ func TestEngine_DefaultPlacementProducesPeaks(t *testing.T) {
|
||||||
// positions. The grid is sized to the same bounding box so every
|
// positions. The grid is sized to the same bounding box so every
|
||||||
// default-placed node lands in-bounds.
|
// default-placed node lands in-bounds.
|
||||||
e := NewEngine(&Config{
|
e := NewEngine(&Config{
|
||||||
Width: maxX - minX,
|
Width: maxX - minX,
|
||||||
Height: maxY - minY,
|
Height: maxY - minY,
|
||||||
Depth: maxZ - minZ,
|
Depth: maxZ - minZ,
|
||||||
OriginX: minX,
|
OriginX: minX,
|
||||||
OriginY: minY,
|
OriginY: minY,
|
||||||
OriginZ: minZ,
|
OriginZ: minZ,
|
||||||
CellSize: 0.2,
|
CellSize: 0.2,
|
||||||
MinDeltaRMS: 0.01,
|
MinDeltaRMS: 0.01,
|
||||||
MaxBlobs: 6,
|
MaxBlobs: 6,
|
||||||
|
|
@ -535,10 +535,10 @@ func gridMaxValue(data []float64) float64 {
|
||||||
// all-pairs motion link set (every node pair, DeltaRMS=1.0, Motion=true), and
|
// all-pairs motion link set (every node pair, DeltaRMS=1.0, Motion=true), and
|
||||||
// the room are identical across both legs. Only where the nodes sit differs.
|
// the room are identical across both legs. Only where the nodes sit differs.
|
||||||
//
|
//
|
||||||
// - Spread placement: links are non-degenerate, AddLinkInfluence paints the
|
// - Spread placement: links are non-degenerate, AddLinkInfluence paints the
|
||||||
// accumulation grid, it normalizes to a non-zero max, and Fuse emits peaks.
|
// accumulation grid, it normalizes to a non-zero max, and Fuse emits peaks.
|
||||||
// - Co-located placement: every link has length < 0.1 m, AddLinkInfluence
|
// - Co-located placement: every link has length < 0.1 m, AddLinkInfluence
|
||||||
// early-returns, the grid stays at zero, and Fuse emits no peaks.
|
// early-returns, the grid stays at zero, and Fuse emits no peaks.
|
||||||
//
|
//
|
||||||
// That the identical non-zero-peak assertion holds for one and fails for the
|
// That the identical non-zero-peak assertion holds for one and fails for the
|
||||||
// other is the demonstrable proof the bead's scope asks for: "test would fail
|
// other is the demonstrable proof the bead's scope asks for: "test would fail
|
||||||
|
|
@ -639,12 +639,12 @@ func fuseWithPlacement(minX, minY, minZ, maxX, maxY, maxZ float64,
|
||||||
count int, place func(int) []simulator.Point) (*Result, float64) {
|
count int, place func(int) []simulator.Point) (*Result, float64) {
|
||||||
|
|
||||||
e := NewEngine(&Config{
|
e := NewEngine(&Config{
|
||||||
Width: maxX - minX,
|
Width: maxX - minX,
|
||||||
Height: maxY - minY,
|
Height: maxY - minY,
|
||||||
Depth: maxZ - minZ,
|
Depth: maxZ - minZ,
|
||||||
OriginX: minX,
|
OriginX: minX,
|
||||||
OriginY: minY,
|
OriginY: minY,
|
||||||
OriginZ: minZ,
|
OriginZ: minZ,
|
||||||
CellSize: 0.2,
|
CellSize: 0.2,
|
||||||
MinDeltaRMS: 0.01,
|
MinDeltaRMS: 0.01,
|
||||||
MaxBlobs: 6,
|
MaxBlobs: 6,
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,10 @@ type RateController struct {
|
||||||
adjacentNodes func(nodeMAC string) []string // returns MACs of adjacent nodes; may be nil
|
adjacentNodes func(nodeMAC string) []string // returns MACs of adjacent nodes; may be nil
|
||||||
|
|
||||||
// Zone-aware fields
|
// Zone-aware fields
|
||||||
zones map[string]*zoneRateState // keyed by zone ID
|
zones map[string]*zoneRateState // keyed by zone ID
|
||||||
zoneMembership func(nodeMAC string) []string // returns zone IDs for a node
|
zoneMembership func(nodeMAC string) []string // returns zone IDs for a node
|
||||||
adjacentZones func(zoneID string) []string // returns zone IDs adjacent to a zone
|
adjacentZones func(zoneID string) []string // returns zone IDs adjacent to a zone
|
||||||
fleetIdle bool // true when all zones are idle
|
fleetIdle bool // true when all zones are idle
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRateController creates a RateController. configSender is called whenever a
|
// NewRateController creates a RateController. configSender is called whenever a
|
||||||
|
|
|
||||||
|
|
@ -102,10 +102,10 @@ func TestDefaultNodePositions_SpansRoom(t *testing.T) {
|
||||||
|
|
||||||
// Span must be a meaningful fraction of the room, not just an
|
// Span must be a meaningful fraction of the room, not just an
|
||||||
// epsilon. Require it to cover at least half of each axis.
|
// epsilon. Require it to cover at least half of each axis.
|
||||||
if (maxPX-minPX) < width*0.5 {
|
if (maxPX - minPX) < width*0.5 {
|
||||||
t.Errorf("count=%d: X span %v < half width %v", count, maxPX-minPX, width*0.5)
|
t.Errorf("count=%d: X span %v < half width %v", count, maxPX-minPX, width*0.5)
|
||||||
}
|
}
|
||||||
if (maxPY-minPY) < depth*0.5 {
|
if (maxPY - minPY) < depth*0.5 {
|
||||||
t.Errorf("count=%d: Y span %v < half depth %v", count, maxPY-minPY, depth*0.5)
|
t.Errorf("count=%d: Y span %v < half depth %v", count, maxPY-minPY, depth*0.5)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,10 @@ func (f *fakeRegistry) GetAllNodes() ([]NodeRecord, error) {
|
||||||
// storeWithNodes builds a fresh store (DefaultSpace) holding one virtual node
|
// storeWithNodes builds a fresh store (DefaultSpace) holding one virtual node
|
||||||
// per entry. Each entry gives (id, position). Positions at the default origin
|
// per entry. Each entry gives (id, position). Positions at the default origin
|
||||||
// are created as-is (the origin is in-bounds for DefaultSpace).
|
// are created as-is (the origin is in-bounds for DefaultSpace).
|
||||||
func storeWithNodes(t *testing.T, nodes ...struct{ ID string; Pos Point }) *VirtualNodeStore {
|
func storeWithNodes(t *testing.T, nodes ...struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}) *VirtualNodeStore {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
store, _ := tempStore(t)
|
store, _ := tempStore(t)
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
|
|
@ -83,9 +86,15 @@ func storeWithNodes(t *testing.T, nodes ...struct{ ID string; Pos Point }) *Virt
|
||||||
// originStore builds a store with count nodes all at the default DB origin.
|
// originStore builds a store with count nodes all at the default DB origin.
|
||||||
func originStore(t *testing.T, count int) *VirtualNodeStore {
|
func originStore(t *testing.T, count int) *VirtualNodeStore {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
entries := make([]struct{ ID string; Pos Point }, 0, count)
|
entries := make([]struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}, 0, count)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
entries = append(entries, struct{ ID string; Pos Point }{
|
entries = append(entries, struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}{
|
||||||
ID: fmt.Sprintf("node-%d", i+1),
|
ID: fmt.Sprintf("node-%d", i+1),
|
||||||
Pos: DefaultNodeOrigin,
|
Pos: DefaultNodeOrigin,
|
||||||
})
|
})
|
||||||
|
|
@ -168,8 +177,14 @@ func TestSyncToRegistry_ExplicitPositionsPreserved(t *testing.T) {
|
||||||
a := Point{X: 1, Y: 1, Z: 1.5}
|
a := Point{X: 1, Y: 1, Z: 1.5}
|
||||||
b := Point{X: 4, Y: 4, Z: 2}
|
b := Point{X: 4, Y: 4, Z: 2}
|
||||||
store := storeWithNodes(t,
|
store := storeWithNodes(t,
|
||||||
struct{ ID string; Pos Point }{"a", a},
|
struct {
|
||||||
struct{ ID string; Pos Point }{"b", b},
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}{"a", a},
|
||||||
|
struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}{"b", b},
|
||||||
)
|
)
|
||||||
bridge := NewFleetRegistryBridge(store)
|
bridge := NewFleetRegistryBridge(store)
|
||||||
reg := newFakeRegistry()
|
reg := newFakeRegistry()
|
||||||
|
|
@ -194,10 +209,22 @@ func TestSyncToRegistry_ExplicitPositionsPreserved(t *testing.T) {
|
||||||
// unset ones, and asserts the final set is distinct and non-origin.
|
// unset ones, and asserts the final set is distinct and non-origin.
|
||||||
func TestSyncToRegistry_MixedNodes(t *testing.T) {
|
func TestSyncToRegistry_MixedNodes(t *testing.T) {
|
||||||
store := storeWithNodes(t,
|
store := storeWithNodes(t,
|
||||||
struct{ ID string; Pos Point }{"explicit", Point{X: 5, Y: 4, Z: 2}},
|
struct {
|
||||||
struct{ ID string; Pos Point }{"o1", DefaultNodeOrigin},
|
ID string
|
||||||
struct{ ID string; Pos Point }{"o2", DefaultNodeOrigin},
|
Pos Point
|
||||||
struct{ ID string; Pos Point }{"o3", DefaultNodeOrigin},
|
}{"explicit", Point{X: 5, Y: 4, Z: 2}},
|
||||||
|
struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}{"o1", DefaultNodeOrigin},
|
||||||
|
struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}{"o2", DefaultNodeOrigin},
|
||||||
|
struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}{"o3", DefaultNodeOrigin},
|
||||||
)
|
)
|
||||||
bridge := NewFleetRegistryBridge(store)
|
bridge := NewFleetRegistryBridge(store)
|
||||||
reg := newFakeRegistry()
|
reg := newFakeRegistry()
|
||||||
|
|
@ -285,7 +312,10 @@ func TestSyncOneNode_MatchesFullSync(t *testing.T) {
|
||||||
// TestSyncOneNode_ExplicitPreserved asserts SyncOneNode passes an explicit
|
// TestSyncOneNode_ExplicitPreserved asserts SyncOneNode passes an explicit
|
||||||
// (non-origin) position through unchanged.
|
// (non-origin) position through unchanged.
|
||||||
func TestSyncOneNode_ExplicitPreserved(t *testing.T) {
|
func TestSyncOneNode_ExplicitPreserved(t *testing.T) {
|
||||||
store := storeWithNodes(t, struct{ ID string; Pos Point }{"a", Point{X: 2, Y: 3, Z: 1.2}})
|
store := storeWithNodes(t, struct {
|
||||||
|
ID string
|
||||||
|
Pos Point
|
||||||
|
}{"a", Point{X: 2, Y: 3, Z: 1.2}})
|
||||||
bridge := NewFleetRegistryBridge(store)
|
bridge := NewFleetRegistryBridge(store)
|
||||||
reg := newFakeRegistry()
|
reg := newFakeRegistry()
|
||||||
|
|
||||||
|
|
|
||||||
251
notes/bf-5151-coverage.md
Normal file
251
notes/bf-5151-coverage.md
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
# bf-5151 Coverage Gate — Every Blob-Creation Site Accounted For
|
||||||
|
|
||||||
|
> **Purpose:** the coverage gate that closes bf-5151's acceptance criterion
|
||||||
|
> *"No blob creation code is missed."* Re-runs the two inventory greps from
|
||||||
|
> `notes/bf-1q3m-consolidated.md` §1.1 at the current HEAD and maps **every** production
|
||||||
|
> blob-creation site to exactly one of:
|
||||||
|
> - **(a) has the 3 canonical identity fields** — the *type* carries them (left at Go zero
|
||||||
|
> values at construction; per the documented design they serialize as omitted / `undefined`
|
||||||
|
> in JS until the BLE sidecar populates them); **or**
|
||||||
|
> - **(b) out-of-scope** per bf-1q3m §6, with the explicit reason.
|
||||||
|
>
|
||||||
|
> No site is silently dropped. This is documentation only — no Go/JS source was modified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. TL;DR
|
||||||
|
|
||||||
|
| Question | Answer |
|
||||||
|
|---|---|
|
||||||
|
| HEAD verified | `37d628e` (`37d628ed2fd7033c70dfdad8b55e0646e41ad9b8`) |
|
||||||
|
| The three canonical identity fields | **`PersonName`** (`json:"personName,omitempty"`), **`AssignedColor`** (`json:"assignedColor,omitempty"`), **`IdentityResolved *bool`** (`json:"identityResolved,omitempty"` — tri-state: nil=unattempted, &true=resolved, &false=failed) |
|
||||||
|
| Production blob-creation sites total | **21** = 5 primary + 16 projection/boundary (matches bf-1q3m §3) |
|
||||||
|
| Sites whose **type carries the 3 canonical fields** (in-scope) | **6** |
|
||||||
|
| Sites out-of-scope (reasoned) | **15** |
|
||||||
|
| In-scope *types* confirmed carrying the 3 fields | **8** (4 primary tracked-blob structs + `api.Track` + `dashboard.blobJSON` + `explainability.BlobSnapshot` + `volume.BlobPos`) |
|
||||||
|
| Sites silently dropped | **0** — every grep hit + the anon-struct boundary + the dead type are enumerated below |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Method — the two greps, re-run at HEAD `37d628e`
|
||||||
|
|
||||||
|
Exactly the two greps from bf-1q3m §1.1, run verbatim against `mothership/` (non-test files):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Grep A — primary tracked-blob literals
|
||||||
|
grep -rnE "Blob\{|TrackedBlob\{" mothership/ --include=*.go | grep -v "_test.go"
|
||||||
|
# Grep B — projection/derived literals
|
||||||
|
grep -rnE "BlobSnapshot\{|BlobState\{|BlobPos\{|BlobUpdate\{|BlobEvent\{|BlobResult\{|BlobExplanation\{" mothership/ --include=*.go | grep -v "_test.go"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Grep A returns 5 hits** (every one is a primary construction site):
|
||||||
|
|
||||||
|
```
|
||||||
|
mothership/internal/fusion/fusion.go:260: blobs[i] = Blob{X: p[0], Y: p[1], Z: p[2], Confidence: p[3]}
|
||||||
|
mothership/cmd/mothership/main.go:2303: autoBlobs[i] = automation.TrackedBlob{
|
||||||
|
mothership/cmd/mothership/main.go:5494: b := sigproc.TrackedBlob{
|
||||||
|
mothership/internal/tracking/tracker.go:168: b := &Blob{
|
||||||
|
mothership/internal/tracker/tracker.go:170: b := &Blob{
|
||||||
|
```
|
||||||
|
|
||||||
|
**Grep B returns 15 hits** (named projection/derived sites):
|
||||||
|
|
||||||
|
```
|
||||||
|
mothership/internal/tracking/tracker.go:181: t.onBlobAppear(BlobEvent{
|
||||||
|
mothership/internal/tracking/tracker.go:196: t.onBlobDisappear(BlobEvent{
|
||||||
|
mothership/cmd/mothership/main.go:2206: blobSnapshots = append(blobSnapshots, explainability.BlobSnapshot{
|
||||||
|
mothership/cmd/mothership/main.go:2326: volumeBlobs[i] = volume.BlobPos{
|
||||||
|
mothership/internal/falldetect/detector.go:277: snapshot := BlobSnapshot{
|
||||||
|
mothership/internal/explainability/handler.go:206: explanation = &BlobExplanation{
|
||||||
|
mothership/internal/explainability/handler.go:267: explanation := &BlobExplanation{
|
||||||
|
mothership/internal/explainability/handler.go:369: explanation := &BlobExplanation{
|
||||||
|
mothership/internal/replay/pipeline.go:114: blobs = append(blobs, BlobUpdate{
|
||||||
|
mothership/internal/replay/pipeline.go:132: blobs = append(blobs, BlobUpdate{
|
||||||
|
mothership/internal/simulator/engine.go:460: blobs = append(blobs, BlobResult{
|
||||||
|
mothership/internal/volume/shape.go:375: state.Blobs[blobID] = &BlobState{
|
||||||
|
mothership/internal/volume/shape.go:575: state.Blobs[blob.ID] = &BlobState{
|
||||||
|
mothership/internal/volume/shape.go:820: state.Blobs[-999] = &BlobState{
|
||||||
|
mothership/internal/volume/shape.go:879: blobState = &BlobState{
|
||||||
|
```
|
||||||
|
|
||||||
|
= **20 named-literal sites**. The **16th projection site (P3)** is an *anonymous* struct literal
|
||||||
|
(`falldetect` boundary at `main.go:2288`) — it does not match either grep's type-name pattern
|
||||||
|
and is therefore **not** in the 20 above. It is enumerated separately in §4 so nothing is
|
||||||
|
silent. **20 + 1 = 21 production sites**, matching bf-1q3m's "5 primary + 16 projection."
|
||||||
|
|
||||||
|
> **Line-drift note vs bf-1q3m** (which verified at `1a26c12`): the four identity-field
|
||||||
|
> commits landed since pushed several sites down by a few lines — e.g. `tracking.Blob` A2
|
||||||
|
> `:160→:168`, `tracker.Blob` A1 `:162→:170`, `BlobEvent` `:173/:188→:181/:196`,
|
||||||
|
> `BlobExplanation` `:194/:255/:357→:206/:267/:369`, `api.TrackedBlob` alias `:30→:38`,
|
||||||
|
> `tracking.BlobEvent` type def `:52→:60`. All four named leak boundaries from bf-1q3m
|
||||||
|
> (`main.go:2206` E3, `:2303` E2, `:2326` volume, `:5494` E1) are **unchanged**. None of this
|
||||||
|
> drift changes the in/out-of-scope classification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The three canonical identity fields (definition of "covered")
|
||||||
|
|
||||||
|
Established by `a612584` (HEAD commit; *not* the task-cited `1446ccf`, which is a pre-rebase
|
||||||
|
dangling commit — see §6). The camelCase JSON keys match the dashboard `Blob` interface in
|
||||||
|
`dashboard/types/spaxel.d.ts`:
|
||||||
|
|
||||||
|
| Field | Go type | JSON key | Tri-state semantics |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `PersonName` | `string` | `personName,omitempty` | `""` = unresolved |
|
||||||
|
| `AssignedColor` | `string` | `assignedColor,omitempty` | `""` = unresolved |
|
||||||
|
| `IdentityResolved` | `*bool` | `identityResolved,omitempty` | `nil`=unattempted, `&true`=resolved, `&false`=failed |
|
||||||
|
|
||||||
|
At construction the in-scope literals set **none** of these (Go zero values: `""`, `nil`).
|
||||||
|
With `omitempty` they serialize as omitted → `undefined` in JS. Population from the BLE
|
||||||
|
identity sidecar (`ble.IdentityMatcher.GetMatch`) is a follow-up bead; the gate here is only
|
||||||
|
that the *type surface* exists so a populated value can flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. In-scope types — all 8 carry the 3 canonical fields ✓
|
||||||
|
|
||||||
|
Each verified at HEAD `37d628e`. Field file:line is where the field is declared on the struct.
|
||||||
|
|
||||||
|
| # | Type | Type def | `PersonName` | `AssignedColor` | `IdentityResolved` | Added by |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | `signal.TrackedBlob` (canonical; `api.TrackedBlob` is a type alias of it) | `internal/signal/processor.go:587` | `:605` | `:606` | `:607` | `a612584` |
|
||||||
|
| 2 | `tracker.Blob` (3D) | `internal/tracker/tracker.go:36` | `:58` | `:59` | `:60` | `a612584` |
|
||||||
|
| 3 | `tracking.Blob` (2D) | `internal/tracking/tracker.go:21` | `:45` | `:46` | `:47` | `a612584` |
|
||||||
|
| 4 | `automation.TrackedBlob` | `internal/automation/engine.go:1337` | `:1347` | `:1348` | `:1349` | `a612584` |
|
||||||
|
| 5 | `api.Track` (`/api/tracks` JSON) | `internal/api/tracks.go:12` | `:31` | `:32` | `:33`; propagated `:124-126` from `signal.TrackedBlob` | `01a415d` |
|
||||||
|
| 6 | `dashboard.blobJSON` (`/ws/dashboard` feed) | `internal/dashboard/hub.go:539` | `:558` | `:559` | `:560`; propagated `:587-589` from `tracking.Blob` | `01a415d` |
|
||||||
|
| 7 | `explainability.BlobSnapshot` (Tier-1 #2) | `internal/explainability/handler.go:95` | `:109` | `:110` | `:111` | `c3f4b0d` (bf-2ibc) |
|
||||||
|
| 8 | `volume.BlobPos` (Tier-1 #3) | `internal/volume/shape.go:1080` | `:1092` | `:1093` | `:1094` | `29a114c` (bf-5v3q); JSON tags camelCased by `7309564` (bf-3wkz) |
|
||||||
|
|
||||||
|
> **`api.TrackedBlob`** (`internal/api/tracks.go:38`) is `type TrackedBlob = signal.TrackedBlob`
|
||||||
|
> — a pure Go type alias. The fields on #1 surface automatically; no separate edit. ✓
|
||||||
|
> **`/api/blobs`** serializes `signal.TrackedBlob` directly (`pm.GetTrackedBlobs`), so it
|
||||||
|
> needs no projection struct — it inherits #1's fields.
|
||||||
|
|
||||||
|
**All 8 confirmed carrying the 3 canonical fields.** This satisfies acceptance criterion
|
||||||
|
*"Every in-scope blob-creation type confirmed to carry the 3 canonical fields at zero values."*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Coverage table — every production site → has-fields / out-of-scope
|
||||||
|
|
||||||
|
### 4.1 Primary construction sites (5)
|
||||||
|
|
||||||
|
| ID | Site (HEAD) | Type built (def) | Status | Reason |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **A1** | `internal/tracker/tracker.go:170` | `tracker.Blob` (`tracker.go:36`) | ✅ **has 3 fields** | Type #2 carries them; spawn literal leaves zero (populated later). In-scope |
|
||||||
|
| **A2** | `internal/tracking/tracker.go:168` | `tracking.Blob` (`tracker.go:21`) | ✅ **has 3 fields** | Type #3 carries them; spawn literal leaves zero. In-scope |
|
||||||
|
| **E1** | `cmd/mothership/main.go:5494` | `signal.TrackedBlob` (`processor.go:587`) | ✅ **has 3 fields** | Type #1 carries them; built from a pre-identity `fusion.Blob` peak so identity is zero by design (bf-1q3m Tier-3 #6). In-scope type |
|
||||||
|
| **E2** | `cmd/mothership/main.go:2303` | `automation.TrackedBlob` (`engine.go:1337`) | ✅ **has 3 fields** | Type #4 carries them; conversion literal leaves zero (bf-1q3m Tier-1 #1). In-scope |
|
||||||
|
| **C1** | `internal/fusion/fusion.go:260` | `fusion.Blob` (`fusion.go:36`) | ⛔ **out-of-scope** | **Pre-identity Fresnel peak.** Type is `{X,Y,Z,Confidence}` only — no ID, no velocity, no identity by design. Identity attaches *after* a peak becomes a tracked blob (bf-1q3m §6 Tier-3) |
|
||||||
|
|
||||||
|
### 4.2 Projection / boundary construction sites (16)
|
||||||
|
|
||||||
|
| ID | Site (HEAD) | Type built (def) | Status | Reason |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **P1** | `cmd/mothership/main.go:2206` | `explainability.BlobSnapshot` (`handler.go:95`) | ✅ **has 3 fields** | Type #7 carries them; bf-1q3m Tier-1 #2. In-scope |
|
||||||
|
| **P2** | `cmd/mothership/main.go:2326` | `volume.BlobPos` (`shape.go:1080`) | ✅ **has 3 fields** | Type #8 carries them; bf-1q3m Tier-1 #3. In-scope |
|
||||||
|
| **P3** | `cmd/mothership/main.go:2288` | anon struct `{ID,X,Y,Z,VX,VY,VZ,Posture}` → `fallDetector.Update` | ⛔ **out-of-scope** | **Anonymous** struct literal (no named type → not matched by either grep; enumerated here). Z-trajectory input to the fall-detect state machine — posture/velocity only, no identity. `Posture` field declared but also unset. Promote only if fall alerts need a person name (bf-1q3m §6 flags this for a future bead) |
|
||||||
|
| **P4** | `internal/explainability/handler.go:206` | `explainability.BlobExplanation` (`handler.go:27`) | ⛔ **out-of-scope** | Empty fallback for an unknown blob ID (no snapshot/history) — renders an "unknown" "Why?" overlay, not a tracked blob |
|
||||||
|
| **P5** | `internal/explainability/handler.go:267` | `explainability.BlobExplanation` (`handler.go:27`) | ⛔ **out-of-scope** | Empty fallback (blob has no recorded history). Rendering result, not a tracked blob |
|
||||||
|
| **P6** | `internal/explainability/handler.go:369` | `explainability.BlobExplanation` (`handler.go:27`) | ⛔ **out-of-scope** | Substantive derivation (copies ID/X/Y/Z/Confidence + attaches links + `BLEMatch`). A derived **rendering** of the "Why?" overlay — identity arrives via `BLEMatch` (`*BLEMatch`), not as a canonical blob field. Out-of-scope by design |
|
||||||
|
| **P7** | `internal/falldetect/detector.go:277` | `falldetect.BlobSnapshot` (`detector.go:69`) | ⛔ **out-of-scope** | **Distinct type** from `explainability.BlobSnapshot` despite the shared name. Z-trajectory input: `{ID,X,Y,Z,VX,VY,VZ,Posture,Timestamp,DeltaRMS}` — no identity (bf-1q3m §6) |
|
||||||
|
| **P8** | `internal/replay/pipeline.go:114` | `replay.BlobUpdate` (`types.go:303`) | ⛔ **out-of-scope** | **Synthetic** — figure-8 trig formula, not blob-derived. Demo replay fixture (bf-1q3m §3.2 note D) |
|
||||||
|
| **P9** | `internal/replay/pipeline.go:132` | `replay.BlobUpdate` (`types.go:303`) | ⛔ **out-of-scope** | **Synthetic** — circular trig formula, not blob-derived. Demo replay fixture |
|
||||||
|
| **P10** | `internal/simulator/engine.go:460` | `simulator.BlobResult` (`engine.go:80`) | ⛔ **out-of-scope** | **Synthetic** — `spaxel-sim` fusion grid fixture, not blob-derived (bf-1q3m §3.2 note D) |
|
||||||
|
| **P11** | `internal/tracking/tracker.go:181` | `tracking.BlobEvent` (`tracker.go:60`) | ⛔ **out-of-scope** | Lifecycle event `{BlobID,X,Z,Timestamp}` (2D; X,Z only) — fired `onBlobAppear`. Not a tracked blob; no identity surface |
|
||||||
|
| **P12** | `internal/tracking/tracker.go:196` | `tracking.BlobEvent` (`tracker.go:60`) | ⛔ **out-of-scope** | Lifecycle event fired `onBlobDisappear`. Same as P11 |
|
||||||
|
| **P13** | `internal/volume/shape.go:375` | `volume.BlobState` (`shape.go:139`) | ⛔ **out-of-scope** | **Deserialization** — reconstructed from persisted `int64` timestamps, not from a blob. Trigger state machine `{BlobID,Inside,EnterTime,LastCheckTime}` (no position, no identity) |
|
||||||
|
| **P14** | `internal/volume/shape.go:575` | `volume.BlobState` (`shape.go:139`) | ⛔ **out-of-scope** | First-seen trigger state, keyed by `volume.BlobPos.ID`. Trigger state (same struct as P13) — carries no identity by design; identity lives on `volume.BlobPos` (#8), which is the boundary that feeds this |
|
||||||
|
| **P15** | `internal/volume/shape.go:820` | `volume.BlobState` (`shape.go:139`) | ⛔ **out-of-scope** | Sentinel key `-999` — in-memory only, not blob-derived |
|
||||||
|
| **P16** | `internal/volume/shape.go:879` | `volume.BlobState` (`shape.go:139`) | ⛔ **out-of-scope** | Prediction synthetic state, keyed by a negative hash — not blob-derived |
|
||||||
|
|
||||||
|
### 4.3 Non-production type (dead code — do not rely on it)
|
||||||
|
|
||||||
|
| Type | Def | Status | Reason |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `api.BlobPos` | `internal/api/triggers.go:624` | ⛔ **out-of-scope (dead)** | `{ID,X,Y,Z}`. `EvaluateTriggers` for this type is **unwired** in `main.go` (the live volume path uses `volume.BlobPos` #8 instead). Vestigial duplicate; per bf-1q3m §6, do not use it for field propagation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Out-of-scope enumeration (none silently dropped)
|
||||||
|
|
||||||
|
All **15** out-of-scope production sites, grouped by reason — every grep hit is accounted for:
|
||||||
|
|
||||||
|
- **Pre-identity peak (1):** C1 `fusion.Blob` (`fusion.go:260`).
|
||||||
|
- **Synthetic / demo / sim (3):** P8 `replay.BlobUpdate` figure-8, P9 `replay.BlobUpdate` circular, P10 `simulator.BlobResult`.
|
||||||
|
- **Anonymous Z-trajectory input (1):** P3 falldetect anon struct (`main.go:2288`).
|
||||||
|
- **Lifecycle events (2):** P11/P12 `tracking.BlobEvent` (appear/disappear).
|
||||||
|
- **Trigger state machine, not blob-derived (4):** P13 deserialization, P14 first-seen, P15 sentinel `-999`, P16 prediction synthetic — all `volume.BlobState`.
|
||||||
|
- **Rendering / fallback results (3):** P4/P5 empty `BlobExplanation` fallbacks, P6 substantive `BlobExplanation` derivation (identity via `BLEMatch` side-channel).
|
||||||
|
- **Distinct-name Z-trajectory input (1):** P7 `falldetect.BlobSnapshot`.
|
||||||
|
- **Dead code (1, non-production):** `api.BlobPos`.
|
||||||
|
|
||||||
|
**Every site returned by Grep A and Grep B, plus the P3 anon-struct boundary not matched by
|
||||||
|
either grep, plus the dead `api.BlobPos` type, appears in §4 with a status.** Zero sites are
|
||||||
|
silently dropped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Commit-hash accuracy note
|
||||||
|
|
||||||
|
The task scope cites commits **`1446ccf`** (canonical fields on primary types) and
|
||||||
|
**`248d0e0`** (propagation to projections). Both are **pre-rebase dangling commits not in
|
||||||
|
HEAD's history** (`git merge-base --is-ancestor` → NO for both). The equivalent work lands in
|
||||||
|
HEAD as:
|
||||||
|
|
||||||
|
| Task-cited (dangling) | Actual HEAD commit | Subject |
|
||||||
|
|---|---|---|
|
||||||
|
| `1446ccf` | **`a612584`** | feat(bf-5151): add canonical identity fields to blob creation types |
|
||||||
|
| `248d0e0` | **`01a415d`** | feat(bf-5151): propagate canonical identity fields to blob JSON projections |
|
||||||
|
|
||||||
|
(Both dangling SHAs carry the *same commit subject* as their HEAD counterparts — they were
|
||||||
|
rebased, not abandoned.) The §3 table cites the HEAD SHAs. This is the same "re-verify, don't
|
||||||
|
trust cited hashes" discipline bf-1q3m §4 applied to its child notes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Build & test gate
|
||||||
|
|
||||||
|
Run from `mothership/` against HEAD `37d628e` + this bead's changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofmt -l . # → empty (clean)
|
||||||
|
go vet ./... # → exit 0 (clean)
|
||||||
|
go test ./... # → all packages ok
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result (2026-07-06): all three green.** `go vet ./...` exit 0; `go test ./...` every
|
||||||
|
package `ok` (`api` 8.4s, `fleet` 0.9s, `simulator`, `test/acceptance` ran fresh after the
|
||||||
|
gofmt rewrite; the rest cached-but-green).
|
||||||
|
|
||||||
|
> **Pre-existing gofmt drift (unrelated to identity):** on first run `gofmt -l` flagged 7
|
||||||
|
> files already-unformatted at HEAD — `cmd/mothership/main.go`, `internal/api/simulator_test.go`,
|
||||||
|
> `internal/fleet/handler_test.go`, `internal/fusion/fusion_test.go`, `internal/ingestion/ratecontrol.go`,
|
||||||
|
> `internal/simulator/node_positions_test.go`, `internal/simulator/registry_bridge_test.go`
|
||||||
|
> (verified: no `.go` file was modified by this bead's working tree before the format pass —
|
||||||
|
> the drift predates it; `main.go`'s only issue was an import-order swap of `simulator`/`sleep`).
|
||||||
|
> Since the gate requires `gofmt -l` clean, these were fixed with `gofmt -w` (purely mechanical:
|
||||||
|
> import ordering + whitespace; zero semantic change, tests re-passed). The coverage analysis
|
||||||
|
> itself (this file) is doc-only and touched no Go source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Acceptance criteria status
|
||||||
|
|
||||||
|
- [x] **`notes/bf-5151-coverage.md` exists mapping every production site → has-fields / out-of-scope-with-reason** — this file, §4 (5 primary + 16 projection + dead type).
|
||||||
|
- [x] **Every in-scope blob-creation type confirmed to carry the 3 canonical fields at zero values** — §3, all 8 types verified at HEAD `37d628e`; construction literals leave them at Go zero values (`""`/`nil`), serialized as omitted.
|
||||||
|
- [x] **Out-of-scope sites explicitly enumerated (none silently dropped)** — §5 lists all 15 production out-of-scope sites + the dead type, grouped by reason; §4.2 explicitly carries P3 (the anon-struct site that neither grep matches).
|
||||||
|
- [x] **gofmt clean, `go vet ./mothership/...` clean, `go test ./mothership/...` passes** — §7: all three green at HEAD `37d628e` + this bead's changes (7 pre-existing-drift files reformatted mechanically).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Provenance
|
||||||
|
|
||||||
|
| Source | Used for | Status at HEAD `37d628e` |
|
||||||
|
|---|---|---|
|
||||||
|
| `notes/bf-1q3m-consolidated.md` §1.1 | the two inventory greps | re-run verbatim; 20 named hits + P3 anon-struct = 21 production sites (matches) |
|
||||||
|
| `notes/bf-1q3m-consolidated.md` §6 | out-of-scope reasons (Tier list + "out of scope" list) | all reasons carried into §4/§5; classifications unchanged |
|
||||||
|
| `notes/bf-1q3m-consolidated.md` §3 | the 21-site catalogue | all 21 present; minor line drift only (§1) |
|
||||||
|
| Commits `a612584` / `01a415d` / `c3f4b0d` / `29a114c` / `7309564` | the field additions under test | all in HEAD; supersede task-cited `1446ccf`/`248d0e0` (§6) |
|
||||||
|
| This report (bf-5151 coverage gate) | the gate that closes bf-5151's "no site missed" criterion | verified against `37d628e`, 2026-07-06 |
|
||||||
Loading…
Add table
Reference in a new issue