From ca7a0f21f3f3ca9d6e22a61cdd20974deacaca0e Mon Sep 17 00:00:00 2001 From: jedarden Date: Mon, 6 Jul 2026 19:11:29 -0400 Subject: [PATCH] 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 Bead-Id: bf-4qto --- .beads/issues.jsonl | 8 +- .needle-predispatch-sha | 2 +- mothership/cmd/mothership/main.go | 24 +++++ .../explainability/identity_fields_test.go | 88 ++++++++++++++++++ .../internal/tracker/identity_fields_test.go | 86 ++++++++++++++++++ .../internal/tracking/identity_fields_test.go | 86 ++++++++++++++++++ .../internal/volume/identity_fields_test.go | 89 +++++++++++++++++++ 7 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 mothership/internal/explainability/identity_fields_test.go create mode 100644 mothership/internal/tracker/identity_fields_test.go create mode 100644 mothership/internal/tracking/identity_fields_test.go create mode 100644 mothership/internal/volume/identity_fields_test.go diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 4a698ca..6640273 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -11,6 +11,7 @@ {"id":"bf-1k3zg","title":"API: GET /api/doctor — pre-flight configuration diagnostic","description":"## Goal\nAdd a GET /api/doctor endpoint that diagnoses common misconfiguration before the user concludes the system is broken. Complements /healthz (runtime state) with pre-flight checks (configuration correctness).\n\n## Endpoint\n\nGET /api/doctor\n- Requires session cookie (same as all /api/* endpoints)\n- Returns 200 with a JSON report regardless of check results (HTTP status reflects reachability, not check results)\n\n## Checks to run\n\n| Check | Pass condition | Fail message |\n|---|---|---|\n| data_dir_writable | /data is writable and has >100 MB free | 'Data directory not writable' or 'Disk space low: Nf MB free' |\n| db_integrity | PRAGMA integrity_check returns 'ok' | 'SQLite integrity check failed' |\n| firmware_dir | /firmware contains at least one *.bin file | 'No firmware binaries found — OTA updates unavailable' |\n| mdns_binding | mDNS service is registered (or SPAXEL_MDNS_ENABLED=false) | 'mDNS not advertising — nodes cannot auto-discover mothership' |\n| mqtt_reachable | If SPAXEL_MQTT_BROKER is set: TCP connect to broker succeeds within 3s | 'MQTT broker unreachable: ' |\n| ntp_reachable | UDP ping to SPAXEL_NTP_SERVER:123 resolves within 3s | 'NTP server unreachable — node clock sync may fail' |\n| install_secret | install_secret row exists in auth table | 'Installation secret missing — re-run container to regenerate' |\n| pin_configured | pin_bcrypt is non-null in auth table | 'Dashboard PIN not configured — run first-time setup' |\n| node_token_consistency | All nodes in registry have non-null node_token | 'N nodes missing auth tokens — re-provision via Web Serial' |\n\n## Response format\n\n{\n 'checks': [\n {'name': 'db_integrity', 'status': 'ok', 'message': null},\n {'name': 'mqtt_reachable', 'status': 'warn', 'message': 'MQTT broker unreachable: mqtt://ha.local:1883'},\n {'name': 'firmware_dir', 'status': 'error', 'message': 'No firmware binaries found'}\n ],\n 'overall': 'warn', // 'ok' | 'warn' | 'error' (worst of all checks)\n 'checked_at': '2024-03-15T07:00:00Z'\n}\n\nStatus levels: 'ok' (pass), 'warn' (degraded but functional), 'error' (action required).\n\n## Dashboard integration\n- Command palette: 'doctor' → calls /api/doctor, shows results inline\n- Guided troubleshooting (Component 36): 'Node offline' flow links to 'Run diagnostics' which calls /api/doctor\n- /healthz already covers runtime health; /api/doctor covers configuration health — keep them separate\n\n## Acceptance\n- GET /api/doctor returns 200 with all checks when fully configured\n- Reports 'firmware_dir: error' when /firmware is empty\n- Reports 'mqtt_reachable: warn' when MQTT broker env is set but broker is unreachable\n- Unit tests cover each check in isolation with mocked dependencies","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:22:51.188946318Z","updated_at":"2026-05-05T12:39:28.566216915Z","closed_at":"2026-05-05T12:39:28.566216915Z","close_reason":"Completed","source_repo":".","compaction_level":0} {"id":"bf-1kid","title":"Add host test asserting default-placement sim produces non-zero fusion peaks","description":"Child of bf-18yn umbrella (final child). Depends on child 3. This is the bf-18yn acceptance-criteria test.\n\nScope:\n- Add a sim/host (or package) test that, using only the DEFAULT node placement (no manual positioning), constructs/seeds the 3D fusion engine and asserts the accumulation grid produces non-zero peaks, i.e. len(blobs) > 0 or grid max above threshold, closing the bf-4q5w symptom. Extend the bf-6s3d fleet->engine invariant test or add a sibling.\n\nAcceptance criteria:\n- Test passes with default placement, and fails (or is skipped with a clear reason) if nodes collapse to (0,0,1).\n- go build ./... passes; existing simulator and fusion tests still pass.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T22:09:38.566010752Z","updated_at":"2026-07-03T23:24:56.503387578Z","closed_at":"2026-07-03T23:24:56.503387578Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1kid","depends_on_id":"bf-xrej","type":"blocks","created_at":"2026-07-03T22:10:08.938736179Z","created_by":"cli","thread_id":""}],"comments":[{"id":58,"issue_id":"bf-1kid","author":"cli","text":"Implemented and pushed (commit 1df0bfa, on origin/main).\n\nAdded two sibling tests to TestEngine_SeedNodePositions (bf-6s3d) in mothership/internal/fusion/fusion_test.go:\n\n1. TestEngine_DefaultPlacementProducesPeaks — the bf-18yn acceptance test / bf-4q5w fix. Seeds the 3D fusion engine using ONLY the default placement (simulator.DefaultNodePositions, the spread geometry a freshly-onboarded virtual/sim fleet gets with no manual positioning — bf-3fr6/bf-xrej), drives a synthetic walker through the room centre, and asserts the accumulation grid produces non-zero peaks (len(blobs) > 0 OR gridMax above threshold) across default placements of 2/3/4/6 nodes. assertPlacementNotCollapsed fails loudly if placement ever collapses to (0,0,1).\n\n2. TestEngine_CoLocatedOriginYieldsNoPeaks — counter-example pinning the bf-4q5w symptom: nodes left at the (0,0,1) DB default are co-located, every link is degenerate, the grid stays at zero, and Fuse emits 0 blobs. Proves the non-zero-peak assertion is meaningful, not trivially satisfiable.\n\nAcceptance criteria met: passes with default placement; co-located (0,0,1) collapse yields no peaks (documented). go build ./... / go vet ./... / go test ./... all green across mothership, cmd/sim, test/acceptance.","created_at":"2026-07-03T23:19:39.087176089Z"},{"id":59,"issue_id":"bf-1kid","author":"cli","text":"Closed bf-18yn acceptance test (commit 1df0bfa, pushed to origin/main).\n\nTestEngine_DefaultPlacementProducesPeaks: seeds the 3D fusion engine using ONLY the default node placement — simulator.DefaultNodePositions, the spread geometry a freshly-onboarded virtual/sim fleet receives with no manual positioning (bf-3fr6/bf-xrej). Runs over 2/3/4/6-node default placements, drives a synthetic walker (buildSyntheticLinks) through the room centre, and asserts len(blobs) > 0 OR gridMax above threshold. assertPlacementNotCollapsed fails loudly if default placement ever collapses to the co-located (0,0,1) DB default (the bf-4q5w root cause).\n\nTestEngine_CoLocatedOriginYieldsNoPeaks: counter-example pinning bf-4q5w — nodes left collapsed at (0,0,1) are co-located, every link is degenerate, the grid stays at zero, and Fuse emits 0 blobs. Proves the non-zero-peak assertion is meaningful, not trivially satisfiable.\n\nSibling to bf-6s3d's TestEngine_SeedNodePositions (which locks the seeding invariant; this locks its downstream consequence).\n\nAcceptance verified: go build/vet/test ./... pass across all three go.work modules (mothership incl. internal/fusion + internal/simulator, cmd/sim, test/acceptance). Targeted run: DefaultPlacement subtests nodes=2/3/4/6 PASS, CoLocatedOriginYieldsNoPeaks PASS, SeedNodePositions PASS.\n\n(implementation was complete in a prior attempt; this attempt verified correctness and closed the bead with the working comment-then-close path — the dispatch footer's --body flag is invalid for this br build)","created_at":"2026-07-03T23:24:47.063692741Z"}]} {"id":"bf-1m2x","title":"Verify blob-creation inventory is current at HEAD","description":"## Context\nbf-4bhd's discovery objective (find all blob creation code paths) is ALREADY DELIVERED by its closed dependency subtree (bf-1q3m, bf-3ldj, bf-55rp, bf-5uzm, bf-4wwt, bf-1bmg) and consolidated in `notes/bf-1q3m-consolidated.md`, which explicitly supersedes `notes/bf-4bhd.md`. This child CLOSES the \"is the inventory actually complete and current?\" loop rather than re-doing discovery from scratch (the anti-pattern that has burned the prior 4 attempts).\n\n## Scope\n- Confirm the consolidated report's 23 production construction sites (5 primary + 16 projection + 2 JS) still match the CURRENT HEAD (report was verified at `1a26c12`).\n- Re-run the completeness greps from report section 1 against current HEAD:\n - `grep -rnE \"Blob\\{|TrackedBlob\\{\" mothership/ --include=*.go | grep -v _test.go` (expect 5 primary)\n - projection-type literals grep (expect 15 non-test projection literals)\n- Record current HEAD SHA + per-category counts; document any site added/removed since `1a26c12`.\n\n## Acceptance Criteria\n- [ ] Short verification note records current HEAD SHA + grep counts (expected: 5 primary / 15 non-test projection / 2 JS production; zero deltas vs `1a26c12`).\n- [ ] Any delta vs `1a26c12` is explicitly documented (expected none — no Go/JS code changed since 1a26c12).\n- [ ] Note cites `notes/bf-1q3m-consolidated.md` as the source of truth and does NOT re-search from scratch.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T20:13:42.925805896Z","updated_at":"2026-07-06T20:20:02.068987123Z","closed_at":"2026-07-06T20:20:02.068987123Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":83,"issue_id":"bf-1m2x","author":"cli","text":"Implemented: notes/bf-1q3m-consolidated.md inventory confirmed current at HEAD 0ae0c03. Re-ran the two completeness greps (Blob{|TrackedBlob{ -> 5 primary; BlobSnapshot{|BlobState{|... -> 15 non-test named projection) plus type-def grep (15 types) and JS sites (2) — zero deltas vs the report's verification commit 1a26c12 (no Go/JS source changed in the interval; only notes/ + .beads/). The 16-sites vs 15-literals distinction reconciled (P3 main.go:2288 is an anonymous-struct literal, absent from the named-type grep). Cited notes/bf-1q3m-consolidated.md as source of truth; did NOT re-search from scratch. Verification note at notes/bf-1m2x-verification.md (commit 64a2202, pushed). Notes-only; no source modified.","created_at":"2026-07-06T20:19:10.410924018Z"}]} +{"id":"bf-1mwy","title":"Verify all blob-creation sites covered (coverage gate)","description":"## Objective\nProve no blob-creation site is missed: re-run the bf-1q3m inventory greps to confirm every production blob-creation type either carries the three canonical identity fields or is explicitly documented out-of-scope. Build + tests green. This is the coverage gate that closes bf-5151's \"No blob creation code is missed\" acceptance criterion.\n\n## Scope (grounded in notes/bf-1q3m-consolidated.md §1 + §6)\n- Re-run the inventory greps from bf-1q3m §1.1:\n - `grep -rnE \"Blob\\{|TrackedBlob\\{\" mothership/ --include=*.go | grep -v \"_test.go\"`\n - `grep -rnE \"BlobSnapshot\\{|BlobState\\{|BlobPos\\{|BlobUpdate\\{|BlobEvent\\{|BlobResult\\{|BlobExplanation\\{\" mothership/ --include=*.go | grep -v \"_test.go\"`\n- Build a coverage table (write to `notes/bf-5151-coverage.md`) mapping EACH production blob-creation site to one of: (a) has the 3 canonical fields [list the type], or (b) out-of-scope per bf-1q3m §6 with the reason (fusion peak C1 pre-identity; synthetic replay/sim P8-P10; dead api.BlobPos; empty fallbacks P4/P5; etc.).\n- Confirm the in-scope types now all carry PersonName/AssignedColor/IdentityResolved: signal.TrackedBlob, tracker.Blob, tracking.Blob, automation.TrackedBlob (1446ccf); api.Track, dashboard.blobJSON (248d0e0); explainability.BlobSnapshot + volume.BlobPos (children 1 & 2).\n- Run `gofmt -l mothership/`, `go vet ./mothership/...`, `go test ./mothership/...` — all must be clean.\n\n## Acceptance Criteria\n- [ ] notes/bf-5151-coverage.md exists mapping every production site → has-fields / out-of-scope-with-reason\n- [ ] Every in-scope blob-creation type confirmed to carry the 3 canonical fields at zero values\n- [ ] Out-of-scope sites explicitly enumerated (none silently dropped)\n- [ ] gofmt clean, go vet ./mothership/... clean, go test ./mothership/... passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T21:23:54.926143294Z","updated_at":"2026-07-06T22:27:55.636212012Z","closed_at":"2026-07-06T22:27:55.636212012Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-1mwy","depends_on_id":"bf-3wkz","type":"blocks","created_at":"2026-07-06T21:24:06.418876849Z","created_by":"cli","thread_id":""}],"comments":[{"id":97,"issue_id":"bf-1mwy","author":"cli","text":"Coverage gate complete and on origin (commits 7f697a3 + 1871a8a).\n\nRe-ran both bf-1q3m §1.1 inventory greps verbatim at the real code HEAD 908adf2 (confirmed ancestor of HEAD): Grep A = 5 primary sites, Grep B = 15 projection sites + the P3 anon-struct boundary = 21 production sites total, matching bf-1q3m §3.\n\nIndependently verified all 8 in-scope blob types carry the 3 canonical identity fields (PersonName/AssignedColor/IdentityResolved) at the cited defs: signal.TrackedBlob, tracker.Blob, tracking.Blob, automation.TrackedBlob, api.Track (+ api.TrackedBlob alias), dashboard.blobJSON, explainability.BlobSnapshot, volume.BlobPos — all left at Go zero values at construction (omitempty → undefined in JS).\n\nAll 15 out-of-scope sites enumerated by reason in §5 (pre-identity fusion peak C1; synthetic replay/sim P8-P10; dead api.BlobPos; empty fallbacks P4/P5; lifecycle events P11/P12; trigger-state volume.BlobState P13-P16; anon falldetect P3; distinct falldetect.BlobSnapshot P7). None silently dropped.\n\nGates green at HEAD 908adf2 + this bead: gofmt -l mothership/ clean; go vet ./mothership/... exit 0; go test ./mothership/... exit 0 (all packages ok).\n\nNote: the committed doc 7f697a3 originally cited a dangling HEAD ref 37d628e (pre-rebase, same stale-hash class the doc's §6 warns about); corrected to real ancestor 908adf2 in 1871a8a. Doc-only bead — no Go/JS source modified.","created_at":"2026-07-06T22:25:17.010933128Z"}]} {"id":"bf-1oj7","title":"Node placement UI: TransformControls for dragging live registered nodes in 3D dashboard","description":"Plan Phase 3 item 18 requires the ability to drag actual registered (live) nodes in the 3D Three.js dashboard using TransformControls to adjust their physical placement. The pre-deployment simulator supports this, but dragging live nodes in the operational dashboard is not implemented. Add TransformControls to the live node meshes in the dashboard with position persistence via the mothership API.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-26T22:14:42.792062952Z","updated_at":"2026-07-06T03:47:54.761005897Z","closed_at":"2026-07-06T03:47:54.761005897Z","close_reason":"Completed","source_repo":".","compaction_level":0} {"id":"bf-1q3m","title":"Consolidate and verify all blob-creation findings into single report","description":"Scope: consolidate children 1-4 into a SINGLE verified source-of-truth report and produce the tiered fix-target list the next implementation bead consumes. This is the \"report ready for the next bead\" deliverable.\n\nInputs: notes from children 1-4 (primary types bf-55rp, conversions bf-5uzm, projections bf-4wwt, JS/TS bf-1bmg).\n\nDeliverable: a single consolidated report (notes/bf--consolidated.md) that:\n- Merges the full inventory (primary types + conversions + projections + JS/TS) into one catalogue\n- Re-verifies EVERY file:line against current HEAD (prior reports drifted before — this is the whole reason the parent kept failing)\n- Flags any stale entries found in the sub-bead notes\n- Produces the tiered fix-target list for the identity-leak fix (Tier 1: identity dropped at main.go:2213 automation + :2116 explainability; Tier 2-4 as appropriate)\n- Includes the blob data-flow diagram showing where identity enters and where it leaks\n\nAcceptance Criteria:\n- [ ] Single consolidated report exists at notes/bf--consolidated.md\n- [ ] Every file:line in the catalogue re-verified against current HEAD (run the greps to confirm)\n- [ ] Stale entries from sub-bead notes explicitly flagged\n- [ ] Tiered fix-target list for the identity leak produced\n- [ ] Report explicitly states it supersedes notes/bf-3ldj-findings.md and notes/bf-4bhd.md for the inventory\n- [ ] Report is ready for the implementation bead to consume without re-searching","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:04:01.063024261Z","updated_at":"2026-07-06T20:07:06.993145748Z","closed_at":"2026-07-06T20:07:06.993145748Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-1q3m","depends_on_id":"bf-1bmg","type":"blocks","created_at":"2026-07-06T15:04:21.508791539Z","created_by":"cli","thread_id":""}],"comments":[{"id":82,"issue_id":"bf-1q3m","author":"cli","text":"Deliverable complete: notes/bf-1q3m-consolidated.md is the single re-verified source-of-truth report (commit f144aad, pushed to origin/main). Re-verified every file:line against current HEAD by running the greps (did not trust child notes): all 15 blob types, all 5 primary construction sites, and all 4 named leak boundaries are exact -- E2 main.go:2303 (automation.TrackedBlob, no identity fields), E3 main.go:2206 (explainability.BlobSnapshot), volume main.go:2326 (BlobPos PersonID field exists, left empty), E1 main.go:5494. Reference pattern is analytics.TrackUpdate at main.go:2271 which pulls PersonID from identityMatcher.GetMatch at :2267. Completeness sweeps confirm exactly 5 primary + 15 projection non-test literals, so no production site is missed. Stale entries flagged in section 4: bf-3ldj and bf-4bhd are SUPERSEDED (their E2 :2213 / E3 :2116 / E1 :5384 drifted to :2303 / :2206 / :5494; 2D tracking.Blob field list also drifted). Report explicitly supersedes both for the inventory. Tiered fix-target list in section 6: Tier 1 = add identity fields and populate from the ble.IdentityMatcher sidecar at main.go:2303, :2206 (fold in identityMap :2216), and :2326; Tiers 2-4 documented. Report is ready for the implementation bead to consume without re-searching. NOTE: the prior failure-count:1 was the dispatch footer's invalid 'br close --body' flag (no such flag exists) -- closing via br comments add + br close as the tool requires.","created_at":"2026-07-06T20:05:09.702563495Z"}]} {"id":"bf-1rzd","title":"Search TypeScript files for blob-shaped object literals","description":"## Objective\nSearch all TypeScript files for blob-shaped object literals.\n\n## Scope\n- Search only .ts and .tsx files\n- Use the pattern defined in the parent bead\n- Identify all locations with blob structure\n\n## Acceptance Criteria\n- [ ] All TypeScript files searched\n- [ ] Blob literal locations identified with file paths and line numbers\n- [ ] Results saved to notes/bf-26ta-typescript-results.md\n- [ ] Each match includes context snippet showing the blob structure","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T06:01:46.201406389Z","updated_at":"2026-07-06T06:08:54.854421997Z","closed_at":"2026-07-06T06:08:54.854421997Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-1rzd","depends_on_id":"bf-3aij","type":"blocks","created_at":"2026-07-06T06:01:58.385955215Z","created_by":"cli","thread_id":""}]} @@ -30,6 +31,7 @@ {"id":"bf-2enwo","title":"Simulator: ray-based propagation engine (internal/sim/propagation.go — direct path + first-order reflections)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.873911203Z","updated_at":"2026-05-06T01:55:05.181196917Z","closed_at":"2026-05-06T01:55:05.181196917Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} {"id":"bf-2fz8","title":"Investigate which fusion engine feeds the live blob loop and IO-6 gate","description":"Prerequisite investigation for the bf-3gw1 umbrella. The live 10Hz loop in mothership/cmd/mothership/main.go:1867 calls pm.GetTrackedBlobs(), but grep confirms mothership/internal/fusion.NewEngine (3D Fresnel engine, fusion.go:93) is NEVER constructed in non-test code. Determine definitively which engine produces the blobs consumed by the live loop and the IO-6 hard-gate.\n\nTasks:\n- Trace pm (processing manager) and GetTrackedBlobs() to the engine that actually emits tracked blobs in the live path.\n- Confirm whether internal/fusion.Engine is the intended blob source, or whether blobs come from a different engine (e.g. the localization tracker).\n- Find/confirm any existing non-test construction site of internal/fusion.NewEngine (expected: none).\n- Record the finding as a bead comment: engine package, construction site (or 'none - must be added'), and the exact line where the live loop reads blobs.\n\nAcceptance criteria:\n- A documented determination (bead comment) naming the blob-producing engine with file:line references.\n- States explicitly whether internal/fusion.NewEngine must be newly constructed in non-test code.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-03T19:53:40.123789285Z","updated_at":"2026-07-03T20:15:51.969894260Z","closed_at":"2026-07-03T20:15:51.969894260Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":44,"issue_id":"bf-2fz8","author":"cli","text":"INVESTIGATION COMPLETE (bf-2fz8). Determination of which fusion engine feeds the live blob loop + IO-6 gate:\n\nBLOB SOURCE: NONE. No engine currently produces tracked blobs for the live path.\n- pm.trackedBlobs (internal/signal/processor.go:227) is always nil: SetTrackedBlobs (processor.go:625) has ZERO callers.\n- GetTrackedBlobs() is read by: the live 10Hz loop (cmd/mothership/main.go:1866), /api/blobs (main.go:4056), anomalyPositionAdapter (main.go:4840), and API wrappers (internal/api/status.go:84, internal/api/tracks.go:99) — ALL see zero blobs. IO-6 ('walker produces a tracked blob') cannot pass.\n\nENGINE INVENTORY:\n- internal/fusion.NewEngine (3D Fresnel, internal/fusion/fusion.go:93) = the INTENDED blob producer, but NEVER constructed in non-test code. The package is imported by exactly one file, internal/localizer/fusion/timing_budget_test.go (a test); the only NewEngine call sites are that test and internal/fusion/fusion_test.go.\n- internal/localization.NewEngine (2D Fresnel, internal/localization/fusion.go:53) = IS live, via NewSelfImprovingLocalizer (main.go:1005), runs Fuse (main.go:2593), but the returned *FusionResult is DISCARDED — nothing reads .Peaks, and no signal.TrackedBlob literal exists outside test code.\n\nCONCLUSION: 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 recorded in code as a doc comment on SetTrackedBlobs (commit a7ec9d2).","created_at":"2026-07-03T20:10:24.738723116Z"},{"id":45,"issue_id":"bf-2fz8","author":"cli","text":"INVESTIGATION COMPLETE (committed a7ec9d2, on origin/main; go vet + go test green).\n\nFINDING: NO engine currently feeds the live blob loop. pm.trackedBlobs is always nil.\n\n- Blob source determination: SetTrackedBlobs (internal/signal/processor.go:631) is the ONLY writer of pm.trackedBlobs and has ZERO callers -> GetTrackedBlobs() always returns nil -> IO-6 cannot pass.\n- Exact live-loop read line: cmd/mothership/main.go:1866 (blobs := pm.GetTrackedBlobs()). Other readers: /api/blobs main.go:4056, anomalyPositionAdapter main.go:4840, internal/api/status.go:84, internal/api/tracks.go:99.\n- internal/fusion.NewEngine (3D Fresnel, internal/fusion/fusion.go:93): construction site = NONE in non-test code. The internal/fusion package is imported by ZERO non-test files; only NewEngine call sites are internal/localizer/fusion/timing_budget_test.go and internal/fusion/fusion_test.go.\n- internal/localization.NewEngine (2D Fresnel, internal/localization/fusion.go:53) IS constructed live (NewSelfImprovingLocalizer main.go:1005, Fuse main.go:2593) but the returned *FusionResult is DISCARDED -> nothing reads .Peaks; only GetLearnedWeights() consumed (main.go:2599). No signal.TrackedBlob literal outside tests (automation.TrackedBlob at main.go:2111 is a distinct type).\n\nCONCLUSION: internal/fusion.NewEngine MUST be newly constructed in non-test code and wired Fuse->TrackedBlob->SetTrackedBlobs, OR localization.Engine FusionResult.Peaks converted to TrackedBlobs via SetTrackedBlobs. Findings also in doc comments (internal/signal/processor.go:601-630).","created_at":"2026-07-03T20:15:38.634306719Z"}]} {"id":"bf-2gmx","title":"Verify backward compatibility for blob identity fields","description":"## Objective\nEnsure existing blobs without identity info work without errors.\n\n## Scope\n- Test the dashboard with existing blob data that lacks identity fields\n- Verify no console errors from undefined personName, assignedColor, identityResolved\n- Check that rendering code handles missing identity data gracefully\n- Confirm backward compatibility is maintained\n\n## Acceptance Criteria\n- [ ] Dashboard loads with existing blob data without errors\n- [ ] No console errors related to undefined identity fields\n- [ ] Rendering code checks for undefined values before using identity fields\n- [ ] Both identity-resolved and non-identity-resolved blobs display correctly\n\n## Notes\nVerification task. Run the app and check console for errors.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T04:41:53.534620139Z","updated_at":"2026-07-06T04:41:53.534620139Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2gmx","depends_on_id":"bf-1wvm","type":"blocks","created_at":"2026-07-06T04:41:56.600702603Z","created_by":"cli","thread_id":""}]} +{"id":"bf-2ibc","title":"Add identity fields to explainability.BlobSnapshot type","description":"## Objective\nAdd the three canonical identity fields — PersonName, AssignedColor, IdentityResolved — to the `explainability.BlobSnapshot` struct type, which currently lacks them. This is the Tier-1 #2 type gap from the bf-1q3m/bf-5cgc handoff.\n\n## Scope (grounded in notes/bf-1q3m-consolidated.md §6 Tier-1 #2)\n- Edit `mothership/internal/explainability/handler.go:95` — `type BlobSnapshot struct` is today `{ID, X, Y, Z, Confidence, Weight}` with NO identity fields (verified: handler.go is absent from the AssignedColor grep set at HEAD).\n- Add `PersonName string`, `AssignedColor string`, `IdentityResolved *bool` with JSON tags matching the dashboard camelCase keys (`person_name`, `assigned_color`, `identity_resolved`), mirroring exactly how commit 1446ccf added them to signal.TrackedBlob / tracker.Blob / tracking.Blob / automation.TrackedBlob.\n- The production creation literal at `cmd/mothership/main.go:2206` needs NO change — Go zero values serialize to undefined in JS via omitempty. Fields-as-undefined, consistent with 1446ccf + 248d0e0.\n- OUT OF SCOPE (do not do here — it is a separate downstream bead): populating these fields from the `ble.IdentityMatcher` sidecar, and folding the parallel `identityMap` (main.go:2216) into the snapshot. This child adds field DECLARATIONS only, left at zero values.\n\n## Acceptance Criteria\n- [ ] PersonName, AssignedColor, IdentityResolved added to explainability.BlobSnapshot (handler.go:95) with correct JSON tags\n- [ ] Fields left at zero values at the main.go:2206 literal — NO populate/sidecar logic added\n- [ ] gofmt clean; `go vet ./internal/explainability/...` clean; `go test ./internal/explainability/...` passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T21:23:30.086543914Z","updated_at":"2026-07-06T21:39:07.645885664Z","closed_at":"2026-07-06T21:39:07.645885664Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":93,"issue_id":"bf-2ibc","author":"cli","text":"Implemented in commit c3f4b0d (pushed to origin). Added PersonName, AssignedColor, IdentityResolved to explainability.BlobSnapshot (handler.go) with camelCase JSON tags mirroring 1446ccf and the dashboard Blob type. Left at zero values at the main.go creation literal (declarations only; BLE sidecar populate is a separate downstream bead). Verified gofmt/vet/test/build all clean and zero-values serialize as omitted (undefined). Reconciled a divergent origin (duplicate bf-4bhd commit, identical patch-id) via rebase.","created_at":"2026-07-06T21:38:16.087174010Z"}]} {"id":"bf-2irz","title":"Verify registry bridge receives simulator positions","description":"Verify that registry_bridge.go actually receives position data from the simulator.\n\nScope:\n- Trace the data flow from simulator to registry_bridge.go\n- Add logging/debugging to verify position reception\n- Confirm positions are reaching the bridge\n\nAcceptance criteria:\n- Data flow from simulator to registry_bridge.go is traced\n- Logging confirms positions are received\n- Reception path is documented","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T05:23:44.753973594Z","updated_at":"2026-07-06T05:37:57.313816406Z","closed_at":"2026-07-06T05:37:57.313816406Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-2irz","depends_on_id":"bf-2r5o","type":"blocks","created_at":"2026-07-06T05:24:11.877031865Z","created_by":"cli","thread_id":""}]} {"id":"bf-2jyk","title":"Dashboard: fix API path mismatch for crowd flow endpoint","description":"Crowd flow endpoint is served at /api/analytics/flow but plan spec and dashboard expect /api/flow. Fix the route definition in the Go backend or update the dashboard fetch URL to match whichever is canonical. Tracked as bf-3f16 in spaxel bead store but P3 and stale.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-05-31T15:43:40.808655211Z","updated_at":"2026-07-06T04:10:04.861491826Z","closed_at":"2026-07-06T04:10:04.861491826Z","close_reason":"Completed","source_repo":".","compaction_level":0} {"id":"bf-2lfti","title":"Activity timeline (Component 27)","description":"## Goal\nImplement universal event stream timeline that serves as primary navigation for time and space.\n\n## Scope\n- Event types: detections, zone transitions, portal crossings, automation triggers, alerts (fall/anomaly/security), system events (node online/offline, OTA, baseline changes), learning milestones\n- Tap any event → 3D view jumps to that exact moment via time-travel\n- Inline actions per event: thumbs up/down (feedback), 'Why?' (explainability), create automation from event\n- Filters: By person, by zone, by event type, by time range (combinable)\n- Search: Natural language queries like 'kitchen occupied after midnight last week'\n- Scroll up = go back in time. Open dashboard after being away → scroll up to see everything that happened\n\n## Location\ndashboard/static/js/timeline.js (new module)\ninternal/api/events.go (GET /api/events endpoint already exists)\n\n## Acceptance\n- Timeline sidebar in expert mode shows all events in scrollable stream\n- Simple mode: timeline IS the main view as activity feed, with room cards above it\n- Tap event → 3D scene shows state at that moment\n- Search filters events correctly\n- FTS5 index on events table for natural language search","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.262510021Z","updated_at":"2026-05-05T22:00:56.387536287Z","closed_at":"2026-05-05T22:00:56.387536287Z","close_reason":"Component 27 - Activity Timeline already fully implemented.\n\nTimeline sidebar (expert mode): sidebar-timeline.js provides collapsible panel with scrollable events, category checkboxes, person/zone filters, date range selector, text search with fuzzy matching, cursor pagination, virtualization for 1000+ events, WebSocket real-time updates.\n\nSimple mode activity feed: simple.html imports timeline.js as ES6 module; timeline is main view below room cards.\n\nTap-to-time-travel: Both timeline.js and sidebar-timeline.js implement handleSeek() calling SpaxelReplay.jumpToTime() to jump 3D view to event timestamp.\n\nSearch: Fuzzy matching client-side + FTS5 server-side via /api/events?q= parameter.\n\nFTS5 index: events.go creates events_fts virtual table with triggers for automatic full-text indexing.\n\nAll acceptance criteria met.","source_repo":".","compaction_level":0} @@ -62,6 +64,7 @@ {"id":"bf-3tlw","title":"Compile blob literal locations documentation","description":"## Objective\nCompile all findings into a comprehensive documentation.\n\n## Scope\n- Merge TypeScript and JavaScript search results\n- Create comprehensive documentation with all locations\n- Note blob structure patterns for each site\n\n## Acceptance Criteria\n- [ ] TypeScript and JavaScript results merged\n- [ ] All blob-shaped object literals documented in single file\n- [ ] Each location includes: file path, line number, blob structure pattern\n- [ ] Final documentation saved to notes/bf-26ta-findings.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-07-06T06:02:13.129510178Z","updated_at":"2026-07-06T06:20:54.599604029Z","closed_at":"2026-07-06T06:20:54.599604029Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-3tlw","depends_on_id":"bf-4ly4","type":"blocks","created_at":"2026-07-06T06:02:17.268268599Z","created_by":"cli","thread_id":""}]} {"id":"bf-3ukb","title":"Fix crowd flow API path mismatch: /api/analytics/flow vs plan spec /api/flow","description":"Two open beads (bf-2jyk, bf-3f16) both describe this same bug: the crowd flow analytics endpoint is registered at /api/analytics/flow but the dashboard JS and plan spec reference /api/flow.\n\nFix:\n- Find the crowd flow endpoint registration in mothership/cmd/mothership/main.go\n- Add an alias at /api/flow that delegates to the same handler, OR\n- Update the dashboard JS to use /api/analytics/flow consistently\n- Choose the approach that aligns with the OpenAPI docs\n\nRelated open beads this resolves: bf-2jyk and bf-3f16 (same root cause).","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-alpha","created_at":"2026-06-04T03:16:28.407609539Z","updated_at":"2026-07-06T04:13:12.269404105Z","closed_at":"2026-07-06T04:13:12.269404105Z","close_reason":"Completed","source_repo":".","compaction_level":0} {"id":"bf-3v39","title":"Capture baseline and verify presence detection with first node","description":"After bf-2po1 (node connected and streaming CSI):\n\nIn passive radar mode (single node, using home AP beacons as TX source):\n1. Ensure room is empty, then POST /api/baseline/capture to capture empty-room baseline\n curl -X POST https://spaxel.ardenone.com/api/baseline/capture (auth required)\n2. Wait 30s for baseline to stabilize (EMA with tau=30s)\n3. Walk through the detection area between the node and the home WiFi AP\n4. In the dashboard, observe: deltaRMS should spike from ~0.02 to ~0.10+\n5. A blob should appear in the 3D visualization at the detected position\n\nWith a single node, only presence detection is reliable (not precise localization).\nFor localization, a second node on the opposite side of the space is needed.\n\nSuccess: deltaRMS > 0.05 when person is present, blob visible in dashboard.\n\nTroubleshooting: if no signal change, check passive_bss NVS key has the AP BSSID,\nor switch node to TX_RX mode and use node-to-node CSI instead of passive radar.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":0,"issue_type":"task","assignee":"","created_at":"2026-06-04T03:15:41.581140007Z","updated_at":"2026-07-03T00:13:34.779002015Z","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"bf-3v39","depends_on_id":"bf-2po1","type":"blocks","created_at":"2026-07-03T00:12:30.706349666Z","created_by":"cli","thread_id":""}]} +{"id":"bf-3wkz","title":"Verify dashboard JS/TS identity-field surface","description":"## Objective\nVerify the dashboard JS/TS blob surface declares and correctly surfaces the three canonical identity fields (personName, assignedColor, identityResolved), aligning field names with the Go JSON tags emitted by the backend projections. This closes the Tier-4 dashboard track of notes/bf-1q3m-consolidated.md §6 (#7/#8/#9).\n\n## Scope (grounded in notes/bf-1q3m-consolidated.md §3.4 + §6 Tier-4)\n- Confirm `dashboard/types/spaxel.d.ts` Blob interface (lines 10-91) declares `personName`, `assignedColor`, `identityResolved` (preliminary grep shows all three present at lines 53/74/90 — verify exact, including the deprecated personLabel/personColor aliases).\n- Cross-check the TS field names against the Go JSON tags emitted by the projections that feed the dashboard: `dashboard.blobJSON` (internal/dashboard/hub.go), `api.Track` (internal/api/tracks.go), and — once children 1 & 2 land — `explainability.BlobSnapshot` and `volume.BlobPos`. Document the mapping; adjust TS or Go tags only if a real mismatch is found.\n- Confirm `dashboard/js/state.js:290` creation literal (`appState.blobs[id] = { id: id }`) correctly surfaces server-supplied identity via the `Object.assign(...)` at state.js:292 — no pre-init needed if the server sends the fields (it does, post-Tier-1).\n- `dashboard/js/websocket.js:167` (J2 dead-reckoning cache) is intentionally identity-free — confirm and leave alone.\n\n## Acceptance Criteria\n- [ ] spaxel.d.ts Blob interface confirmed to declare personName, assignedColor, identityResolved (+ deprecated aliases)\n- [ ] TS field names ↔ Go JSON-tag mapping documented; any mismatch fixed\n- [ ] state.js:290 creation path confirmed to surface server identity via Object.assign; no change needed unless broken","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T21:23:54.879479212Z","updated_at":"2026-07-06T21:59:39.656195409Z","closed_at":"2026-07-06T21:59:39.656195409Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-3wkz","depends_on_id":"bf-5v3q","type":"blocks","created_at":"2026-07-06T21:24:06.395105377Z","created_by":"cli","thread_id":""}],"comments":[{"id":95,"issue_id":"bf-3wkz","author":"cli","text":"Verified the dashboard JS/TS identity-field surface (Tier-4 track of notes/bf-1q3m-consolidated.md §6). spaxel.d.ts Blob interface declares personName/assignedColor/identityResolved (+ deprecated personLabel/personColor aliases). Cross-checked TS vs Go JSON tags across all 8 blob-shaped types: 7 already camelCase; FIXED volume.BlobPos — the sole outlier emitting snake_case (person_name/assigned_color/identity_resolved -> camelCase) to match the canonical convention (bf-5151) and the dashboard. Tags were inert (volume.BlobPos is consumed in Go by Store.Evaluate, which returns fired-trigger IDs; never marshalled to the dashboard) but would have leaked snake_case if ever surfaced. state.js:290 surfaces server identity via Object.assign (no change needed); websocket.js:167 dead-reckoning cache intentionally identity-free (left alone). go vet ./... and go test ./... pass. Mapping documented in notes/bf-3wkz-dashboard-identity-surface.md. Commit 7309564.","created_at":"2026-07-06T21:53:46.343461884Z"},{"id":96,"issue_id":"bf-3wkz","author":"cli","text":"Verified & closed (commit 908adf2, on origin/main).\n\nIndependent re-verification at HEAD 7309564 confirms the Tier-4 dashboard identity surface:\n- spaxel.d.ts Blob interface declares personName(53)/assignedColor(74)/identityResolved(90) + deprecated personLabel/personColor aliases + personId.\n- All 8 Go types emit the canonical camelCase trio: dashboard.blobJSON (hub.go:558-560), api.Track (tracks.go:31-33), explainability.BlobSnapshot (handler.go:109-111), volume.BlobPos (shape.go:1090-1092), signal.TrackedBlob, tracker.Blob (3D), tracking.Blob (2D), automation.TrackedBlob. The lone snake_case outlier (volume.BlobPos) was aligned to camelCase in commit 7309564.\n- state.js:288-294 updateBlob surfaces server identity via Object.assign at :292 (creation literal is {id} only) — no change needed.\n- websocket.js:159-174 dead-reckoning cache is intentionally identity-free ({x,z,vx,vz,ts}) — left alone.\n- go vet ./... exit 0; go test ./... all packages ok.\n\nFull mapping + provenance in notes/bf-3wkz-dashboard-identity-surface.md. No source change required in this pass.","created_at":"2026-07-06T21:58:58.109007041Z"}]} {"id":"bf-3y9r","title":"Verify position propagation through registry and fusion engine","description":"Child of bf-18yn (split). Depends on bf-5yff.\n\nEnsure that the realistic node positions flow through the entire pipeline from simulator to registry to fusion engine.\n\nScope:\n- Trace the wiring from prior children that connects simulator positions to registry\n- Verify positions are correctly propagated to internal/simulator/registry_bridge.go\n- Verify positions reach the fusion engine accumulation grid\n- Add any missing glue if positions don't flow through (should be wired from prior children)\n\nAcceptance criteria:\n- Positions from virtual node creation reach the registry correctly\n- Positions from registry reach the fusion engine correctly\n- If wiring was incomplete, add the missing connections\n- Integration test or log output shows positions flowing through all stages","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:22:26.661147812Z","updated_at":"2026-07-06T17:40:04.865168935Z","closed_at":"2026-07-06T17:40:04.865168935Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-3y9r","depends_on_id":"bf-5yff","type":"blocks","created_at":"2026-07-06T04:22:30.785332691Z","created_by":"cli","thread_id":""},{"issue_id":"bf-3y9r","depends_on_id":"bf-95tx","type":"blocks","created_at":"2026-07-06T04:56:36.064193893Z","created_by":"cli","thread_id":""}],"comments":[{"id":72,"issue_id":"bf-3y9r","author":"cli","text":"Closed by verification. Position propagation through the full chain (simulator VirtualNodeStore -> registry_bridge -> fleet.Registry SQLite -> fusion.Engine.nodePos mirror -> accumulation grid) was already wired by prior children (bf-5lii -> bf-69ym -> bf-u7ds -> bf-95tx).\n\nThis bead's acceptance criterion -- 'positions reach the fusion engine accumulation grid and actually drive localization' -- is locked in by TestPositionPropagation_ReachesAccumulationGrid in mothership/internal/api/simulator_test.go. It posts two explicitly-positioned nodes via the simulator REST API, runs one Fuse step with a link between the two MACs the engine now holds, and asserts a blob localizes near the link midpoint (confidence 1.0, 1 active link). That peak is only reachable if BOTH endpoints' positions reached the grid lookup (AddLinkInfluence reads nodePos[mac] for both endpoints; a missing/co-located position yields no peak).\n\nVerified: go test ./... PASS (internal/api incl.), go vet ./... clean. Grid trace under -v:\n [grid] position propagation midpoint=(3.00,1.00) peak=(2.90,0.90,0.90) conf=1.000 activeLinks=1\n\nNo production wiring change needed -- prior children completed it. Committed and pushed to origin/main.","created_at":"2026-07-06T17:32:00.336376744Z"},{"id":73,"issue_id":"bf-3y9r","author":"cli","text":"Verified position propagation through simulator → registry → fusion engine. The wiring from prior children is complete and correct: a posted node position flows VirtualNodeStore → fleetRegistryBridge.SyncToRegistry (SQLite write) → fleetRegistryAdapter forwardPos hook → fleetMgr.ForwardNodePosition → nodePositionSink → fusion.Engine.SetNodePosition, and Fuse reads nodePos[mac] for both endpoints into AddLinkInfluence on the accumulation grid. No production change was needed. Lock-in: TestPositionPropagation_ReachesAccumulationGrid posts two explicitly-positioned nodes through the simulator REST API and asserts one Fuse step localizes a blob near the link midpoint (peak=(2.90,0.90,0.90), conf=1.000, activeLinks=1) — only reachable if BOTH endpoints' positions reached the grid lookup (a missing/co-located position yields no peak, cf. TestEngine_CoLocatedOriginYieldsNoPeaks). go vet ./... clean; go test ./... fully green.","created_at":"2026-07-06T17:39:55.812604919Z"}]} {"id":"bf-49fp","title":"Dockerfile: bust Kaniko cache for firmware stage to force 4MB rebuild","description":"The 0.1.352 Docker image contains firmware compiled with CONFIG_ESPTOOLPY_FLASHSIZE_16MB (16MB header). The fix was already committed in d837598 (sdkconfig.defaults -> 4MB, partitions.csv redesigned for 4MB). But Kaniko served a cached firmware layer for the 0.1.352 build, ignoring the sdkconfig.defaults change.\n\nSymptom: ESP32-S3 (4MB flash) flashed via Web Serial crashes on every boot with:\n spi_flash: Detected size(4096k) smaller than binary image header(16384k). Probe failed.\n assert failed: do_core_init startup.c:326\n\nFix:\n1. In firmware stage of Dockerfile, add a cache-busting ARG before the COPY:\n ARG FIRMWARE_CACHE_BUST=2026-06-03\n This guarantees Kaniko treats the layer as new on the next build.\n\n2. Also add RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target\n so any stale tracked sdkconfig cannot survive into the build.\n\nAfter this change, idf.py set-target will regenerate sdkconfig from sdkconfig.defaults\nwhich has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, producing firmware with a 4MB header.\n\nReference: firmware/sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y)\n firmware/partitions.csv (4MB layout: factory+ota_0 fit in 4MB)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":0,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-06-04T03:13:10.895986057Z","updated_at":"2026-07-02T18:50:13.296108997Z","closed_at":"2026-07-02T18:50:13.296108997Z","close_reason":"Fix was already committed in d837598/301a588 (Kaniko cache-bust ARG + sdkconfig cleanup in the firmware Dockerfile stage) weeks ago. Bead was stuck retrying due to a dead ANTHROPIC_BASE_URL in the claude-code-glm-5 adapter (fixed 2026-07-02); the agent correctly verified the fix was in place and closed this itself, but a missing verify-changes.sh hook on lab (also fixed 2026-07-02) reopened it every time. No further action needed.","source_repo":".","compaction_level":0,"comments":[{"id":1,"issue_id":"bf-49fp","author":"cli","text":"Already implemented in commit 301a588 (pushed to origin/main). Firmware stage of Dockerfile now contains both required changes: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before 'COPY firmware/ ./' to guarantee a Kaniko cache miss on the next build; (2) RUN rm -f sdkconfig sdkconfig.old after COPY and before 'idf.py set-target' so sdkconfig is regenerated from sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y) every build. Verified: firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, firmware/partitions.csv is a 4MB layout (factory+ota_0 fit), VERSION already at 0.1.357 (past the 0.1.354 bump that accompanied this fix), so a fresh CI build producing the 4MB-header firmware has already run. Resolves the ESP32-S3 boot crash (spi_flash detected 4096k < binary header 16384k).","created_at":"2026-07-02T17:19:36.504115277Z"},{"id":2,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and shipped in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), on origin/main (live since v0.1.354). Verified on re-dispatch: (1) Dockerfile:31 has ARG FIRMWARE_CACHE_BUST=2026-06-03 before the firmware COPY; (2) Dockerfile:38 has 'RUN rm -f sdkconfig sdkconfig.old' after COPY and before 'idf.py set-target'. Confirmed firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y. No new commit needed - working tree had no code changes.","created_at":"2026-07-02T17:30:00.661481731Z"},{"id":3,"issue_id":"bf-49fp","author":"cli","text":"Resolved: this fix was already implemented in commit 301a588 (ancestor of HEAD, pushed to main). Verified both required lines present in current Dockerfile matching spec exactly: line 31 ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (line 34); line 38 RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target (line 44). Dockerfile working tree clean (git diff empty) — nothing new to commit/push. go vet ./... clean, go test ./... all pass. Fix shipped in 0.1.354+ (now at 0.1.357); the 0.1.352 16MB-header crash loop is resolved. Did not bump cache-bust value: task explicitly specifies 2026-06-03 and the one-time cache invalidation already served its purpose.","created_at":"2026-07-02T17:37:28.212905891Z"},{"id":4,"issue_id":"bf-49fp","author":"cli","text":"Verified complete — both requested Dockerfile changes are already present in the firmware stage and were committed in 301a588 (pushed to origin/main). (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before 'COPY firmware/ ./' at Dockerfile:31 busts Kaniko's firmware-layer cache. (2) 'RUN rm -f sdkconfig sdkconfig.old' after COPY and before 'idf.py set-target' at Dockerfile:38 removes stale generated sdkconfig so it regenerates from sdkconfig.defaults. Supporting state verified: sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y; partitions.csv is the 4MB layout (factory 0x10000 + ota_0 0x200000, each 0x1F0000); no stale generated sdkconfig tracked. No code change needed this session; go vet + go test ./... both pass (exit 0). Build/deploy/flash tracked in follow-up bead bf-15hy.","created_at":"2026-07-02T17:42:52.975800256Z"},{"id":5,"issue_id":"bf-49fp","author":"cli","text":"Already implemented in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), in HEAD and pushed to origin/main. Verified: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 added in firmware stage BEFORE the COPY at Dockerfile:31 — busts Kaniko layer cache; (2) RUN rm -f sdkconfig sdkconfig.old added AFTER COPY and BEFORE idf.py set-target at Dockerfile:38 — clears stale tracked sdkconfig so set-target regenerates from sdkconfig.defaults. Confirmed sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y and partitions.csv is the 4MB layout (factory 0x1F0000 + ota_0 0x1F0000 fits 4MB). Working tree clean; no further changes needed.","created_at":"2026-07-02T17:46:59.138102056Z"},{"id":6,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and committed in 301a588 (in branch history, pushed to origin). Dockerfile firmware stage has ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (line 31) and RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target (line 38). Forces Kaniko cache miss + regenerates sdkconfig from sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y), producing a 4MB-header firmware. Verified: go vet ./... clean, go test ./... all pass. No new commit needed - working tree Dockerfile is clean.","created_at":"2026-07-02T17:50:11.573756430Z"},{"id":7,"issue_id":"bf-49fp","author":"cli","text":"Implementation already present in commit 301a588 (fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop), verified in current Dockerfile on main: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (Dockerfile:31); (2) RUN rm -f sdkconfig sdkconfig.old after COPY and before idf.py set-target (Dockerfile:38). Coherent with firmware/sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y) and firmware/partitions.csv (4MB layout). No new code changes were needed — working tree held only dispatcher metadata and build artifacts.","created_at":"2026-07-02T17:55:44.367732542Z"},{"id":8,"issue_id":"bf-49fp","author":"cli","text":"Closed as already-complete: commit 301a588 (fix(firmware): bust Kaniko cache + force sdkconfig regen) already added both requested Dockerfile firmware-stage changes — ARG FIRMWARE_CACHE_BUST=2026-06-03 before the COPY (line 31), and RUN rm -f sdkconfig sdkconfig.old after COPY / before idf.py set-target (line 38). That commit is on origin/main; sdkconfig.defaults carries CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, partitions.csv is the 4MB layout (factory+ota_0 fit), and VERSION has since advanced to 0.1.357 so the 4MB-header fix is baked into released images. No new commit was needed.","created_at":"2026-07-02T18:03:06.315827627Z"},{"id":9,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and pushed. The exact two changes this bead requests landed in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), which is on origin/main and an ancestor of HEAD. Verified in current Dockerfile: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before 'COPY firmware/ ./' at line 31; (2) RUN rm -f sdkconfig sdkconfig.old after COPY and before 'idf.py set-target esp32s3' at line 38. Also confirmed firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y and firmware/partitions.csv is the 4MB layout. No working-tree changes were needed.","created_at":"2026-07-02T18:06:58.975466905Z"},{"id":10,"issue_id":"bf-49fp","author":"cli","text":"Fix already implemented and pushed (commit 301a588, present in origin/main and github/main). Verified the two required Dockerfile changes are in place in the firmware-builder stage, correctly ordered: line 31 ARG FIRMWARE_CACHE_BUST=2026-06-03 (cache-bust ARG before COPY), line 34 COPY firmware/ ./, line 38 RUN rm -f sdkconfig sdkconfig.old (stale sdkconfig removal after COPY, before set-target), line 44 idf.py set-target esp32s3. This guarantees Kaniko treats the firmware layer as new and forces set-target to regenerate sdkconfig from sdkconfig.defaults with CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y, producing firmware with a 4MB header that boots on 4MB-flash ESP32-S3 devices (fixes the spi_flash binary image header 16384k crash loop). No new commit needed; this task is a retry/duplicate of already-shipped work.","created_at":"2026-07-02T18:15:37.119821897Z"},{"id":11,"issue_id":"bf-49fp","author":"cli","text":"Already implemented in commit 301a588 (in main, pushed to origin/main). The Dockerfile firmware stage contains both required changes: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 before COPY firmware/ (line 31) to bust Kaniko's cached 16MB firmware layer; (2) RUN rm -f sdkconfig sdkconfig.old (line 38) after COPY and before idf.py set-target, so set-target regenerates sdkconfig from sdkconfig.defaults. Verified firmware/sdkconfig.defaults sets CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y and partitions.csv uses a 4MB layout (factory+ota_0 fit in 4MB). No code changes needed - Dockerfile working tree is clean.","created_at":"2026-07-02T18:31:31.018683964Z"},{"id":12,"issue_id":"bf-49fp","author":"cli","text":"Fix already implemented in commit 301a588 (fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop), dated 2026-06-03. Verified present and correct in current Dockerfile: (1) ARG FIRMWARE_CACHE_BUST=2026-06-03 at line 31, immediately before 'COPY firmware/ ./'; (2) RUN rm -f sdkconfig sdkconfig.old at line 38, after COPY and before 'idf.py set-target' at line 44. Reference files confirmed coherent: firmware/sdkconfig.defaults has CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y; firmware/partitions.csv is the 4MB layout (factory 0x10000 + ota_0 0x200000 + otadata 0x3F0000, all within 4MB). No new changes required — already committed and pushed (ancestor of HEAD). No Go code touched, so go test/vet not applicable to this Dockerfile-only fix.","created_at":"2026-07-02T18:37:55.559623529Z"},{"id":13,"issue_id":"bf-49fp","author":"cli","text":"Already implemented and shipped — no change needed.\n\nBoth requested Dockerfile firmware-stage changes are present:\n - Line 31: ARG FIRMWARE_CACHE_BUST=2026-06-03 (before the COPY)\n - Line 38: RUN rm -f sdkconfig sdkconfig.old (after COPY, before idf.py set-target)\n\nIntroduced in commit 301a588 ('fix(firmware): bust Kaniko cache + force sdkconfig regen to fix 16MB crash loop'), already on origin/main (verified via git branch -r --contains 301a588). Dockerfile working tree matches HEAD, no uncommitted changes.\n\nEffect: the ARG forces Kaniko to treat the firmware layer as new; the rm ensures idf.py set-target regenerates sdkconfig from sdkconfig.defaults (CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y), yielding a 4MB header — fixing the spi_flash 'binary image header(16384k)' boot crash on 4MB-flash ESP32-S3. Closing as already-complete.","created_at":"2026-07-02T18:45:49.875764400Z"}]} {"id":"bf-4b1c","title":"Add test coverage for geometry placement and fusion peaks","description":"Child of bf-18yn (split). Depends on bf-3y9r.\n\nWrite tests that verify the geometry placement produces meaningful fusion results.\n\nScope:\n- Add a sim or host test that creates virtual nodes with default placement\n- Verify the fusion accumulation grid produces non-zero peaks (not all zeros)\n- Ensure test would fail with old co-located placement but passes with new geometry\n- Run existing simulator tests to ensure no regressions\n\nAcceptance criteria:\n- New test passes with realistic geometry\n- Test verifies fusion accumulation grid has non-zero peaks\n- Existing simulator tests still pass\n- go build ./... passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:22:35.225244286Z","updated_at":"2026-07-06T17:54:27.194709041Z","closed_at":"2026-07-06T17:54:27.194709041Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4b1c","depends_on_id":"bf-3y9r","type":"blocks","created_at":"2026-07-06T04:22:39.781625261Z","created_by":"cli","thread_id":""}],"comments":[{"id":74,"issue_id":"bf-4b1c","author":"cli","text":"Implemented (commit c4d42e8, pushed to main). Added TestEngine_GeometryPlacementDrivesFusionPeaks to mothership/internal/fusion/fusion_test.go — a differential lock-in that holds node placement as the SOLE variable (engine grid, all-pairs motion link set, and room identical across both legs) and proves the non-zero-peak condition flips with geometry: default (spread) placement accumulates a non-zero fusion grid (gridMax>0; blobs>=1 for count>=4 where crossing links form a true maximum), while the old co-located (0,0,1) collapse makes every link degenerate (<0.1m), so AddLinkInfluence early-returns and the grid stays at zero (gridMax==0, 0 blobs). The trace shows the flip: spread gridMax=1.0 blobs=2 vs colocated gridMax=0.0 blobs=0. Gates: go test ./... , go vet ./... , and go build ./... (mothership + cmd/sim + test/acceptance) all pass.","created_at":"2026-07-06T17:50:18.895413358Z"},{"id":75,"issue_id":"bf-4b1c","author":"cli","text":"Verified and closed (re-dispatch after prior run failed on 'br close --body' which is unsupported). Implementation is commit c4d42e8 on origin/main — TestEngine_GeometryPlacementDrivesFusionPeaks in mothership/internal/fusion/fusion_test.go.\n\nAll bf-4b1c acceptance criteria verified green:\n- go build ./... clean across all 3 modules (mothership, cmd/sim, test/acceptance)\n- go vet ./... clean across all 3 modules\n- go test ./... — all packages pass (mothership incl. internal/fusion + internal/simulator, test/acceptance ok)\n- New test passes with realistic geometry: spread placement accumulates a non-zero grid (gridMax=1.0) and count>=4 crossing links extract >=1 blob (nodes=4 -> blobs=2, conf=0.768)\n- Differential lock-in holds placement as the SOLE variable (identical engine grid, all-pairs motion link set, room): spread gridMax=1.0 blobs=2 vs old co-located (0,0,1) collapse gridMax=0.0 blobs=0 — the non-zero-peak condition flips with geometry, proving the test is genuinely regression-sensitive to the bf-4q5w collapse.","created_at":"2026-07-06T17:53:23.492885933Z"}]} @@ -72,12 +75,12 @@ {"id":"bf-4oiz","title":"Verify and fix registry bridge position handling","description":"Ensure that positions from the simulator are correctly received and processed by internal/simulator/registry_bridge.go.\n\nScope:\n- Verify that registry_bridge.go receives position data from simulator\n- Check that positions are correctly formatted for registry consumption\n- Fix any missing or incorrect wiring\n\nAcceptance criteria:\n- registry_bridge.go receives position data from simulator\n- Positions are correctly formatted for registry\n- Any missing wiring is added\n- Positions flow from simulator to registry_bridge.go correctly","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T04:56:17.285977814Z","updated_at":"2026-07-06T16:23:13.948046549Z","closed_at":"2026-07-06T16:23:13.948046549Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child","umbrella"],"dependencies":[{"issue_id":"bf-4oiz","depends_on_id":"bf-3ifj","type":"blocks","created_at":"2026-07-06T04:56:20.008223894Z","created_by":"cli","thread_id":""},{"issue_id":"bf-4oiz","depends_on_id":"bf-5dpu","type":"blocks","created_at":"2026-07-06T05:24:19.551325547Z","created_by":"cli","thread_id":""},{"issue_id":"bf-4oiz","depends_on_id":"bf-69ym","type":"blocks","created_at":"2026-07-06T13:19:39.307840995Z","created_by":"cli","thread_id":""}],"comments":[{"id":69,"issue_id":"bf-4oiz","author":"cli","text":"Verified the registry-bridge position flow end-to-end for close-out. The compile blocker flagged by bf-4pqj (*fleet.Registry not satisfying simulator.RegistryNodeAdapter) was resolved by bf-5lii's fleetRegistryAdapter + immediate SyncToRegistry on AddNode/UpdateNode; bf-69ym added two real-SQLite-registry e2e tests. Re-verified: go build ./cmd/mothership OK, go vet ./... clean, go test ./... all green (simulator + api fresh). The two virtual-node position mutation sites (simulator.go CreateVirtualNode/UpdateNodePosition) are the only writers and both trigger immediate sync; 30s ticker stays as safety net. Acceptance criteria met. Doc-only close-out commit fd7918f.","created_at":"2026-07-06T16:22:53.997318825Z"}]} {"id":"bf-4pqj","title":"Audit and document current registry_bridge position wiring","description":"Read-only verification of the EXISTING registry_bridge position handling (split from bf-4oiz). Prior notes/bf-4oiz-findings.md claimed wiring was missing — that is now STALE: main.go already creates the VirtualNodeStore + FleetRegistryBridge and runs a 30s periodic SyncToRegistry (cmd/mothership/main.go:4269-4301).\n\nScope:\n- Confirm registry_bridge.go receives position data from the simulator via VirtualNodeStore (SyncToRegistry iterates the store's virtual nodes)\n- Confirm positions are correctly formatted for registry consumption (RegistryNodeAdapter.AddVirtualNode/SetNodePosition take x,y,z float64)\n- Update notes/bf-4oiz-findings.md to reflect the real current data flow (periodic sync IS wired)\n\nAcceptance criteria:\n- registry_bridge.go confirmed to receive position data from simulator (via store) — documented\n- Position formatting confirmed correct, backed by registry_bridge_test.go\n- notes/bf-4oiz-findings.md 'Current Data Flow' section corrected to reflect periodic sync wiring exists\n- No production code changes required (verification only)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T13:18:51.748554168Z","updated_at":"2026-07-06T15:21:01.369382114Z","closed_at":"2026-07-06T15:21:01.369382114Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"comments":[{"id":65,"issue_id":"bf-4pqj","author":"cli","text":"Re-verified (2nd dispatch) — audit deliverable in notes/bf-4oiz-findings.md (commit 1499c34) confirmed accurate against current source:\n\nACCEPTANCE CRITERIA — ALL MET:\n1. registry_bridge.go receives simulator positions via store — SyncToRegistry (registry_bridge.go:~147) calls b.store.ListNodes() -> []*VirtualNodeState{.Position}, then spreads pos.X/pos.Y/pos.Z into adapter.AddVirtualNode/SetNodePosition. Data flow store->bridge->adapter CONFIRMED.\n2. Position formatting correct + backed by tests — RegistryNodeAdapter uses float64 XYZ throughout (registry_bridge.go:113-121); fakeRegistry test double asserts exact round-trip; 'go test ./internal/simulator/' -> ok (9 tests, incl. ExplicitPositionsPreserved, SyncOneNode_ExplicitPreserved, OriginNodesGetSpreadGeometry, Idempotent, MatchesFullSync).\n3. notes/bf-4oiz-findings.md 'Current Data Flow' corrected — stale 'wiring missing' claim replaced with the real periodic-sync wiring (main.go:4195-4226: VirtualNodeStore + FleetRegistryBridge + 30s SyncToRegistry ticker).\n4. No production code changes — none made (verification only).\n\nKNOWN OUT-OF-SCOPE BLOCKER (documented, belongs to bf-5dpu / commit e92d5bc): 'go vet ./...' fails on TWO packages — cmd/mothership (main.go:4217: *fleet.Registry.GetAllNodes returns []fleet.NodeRecord, not []simulator.NodeRecord) and internal/api (simulator_test.go:19: NewSimulatorHandler signature changed to (store, bridge), test not updated). The internal/simulator package itself — the subject of THIS audit — builds and tests green. This bead cannot fix the blocker (no-prod-changes mandate); it is flagged for the wiring owner. Closing because all four acceptance criteria are satisfiable and complete; not closing would loop forever on out-of-scope breakage.","created_at":"2026-07-06T15:19:52.397270670Z"}]} {"id":"bf-4q5w","title":"Wire fusion engine SetNodePosition — nodes never positioned, sim/IO-6 produces 0 blobs","description":"Found during investigation of bf-3v39 (presence-detection verification).\n\nSYMPTOM: E2E tests (tests/e2e TestFullE2EIntegration, TestDetectionEvents) and the IO-6 hard-gate scenario produce ZERO detection events / blobs even with 4 nodes + 2 walkers running spaxel-sim. The fusion loop runs at 10Hz but never emits a peak.\n\nROOT CAUSE: internal/fusion/fusion.go Engine.SetNodePosition(mac,x,y,z) is defined but NEVER CALLED in the non-test wiring (grep shows only the definition + unrelated SetPositionProvider helpers). Nodes default to pos=(0,0,1) in the DB (internal/db/migrations.go:143-145). With all nodes co-located, the Fresnel zone excess path length |P-T|+|P-R|-|T-R| collapses to ~0 everywhere, so the accumulation grid has no meaningful peaks and confidence is undefined -> no blobs.\n\nWHY IT MATTERS: This breaks the IO-6 release hard-gate ('walker produces a tracked blob') and means the eventual physical presence-detection validation (bf-3v39) will also show no blobs regardless of hardware, until node positions feed the fusion engine.\n\nLIKELY FIX: In cmd/mothership/main.go, call fusion Engine.SetNodePosition for each node (a) at startup from the DB nodes table, and (b) whenever a node's position changes (PATCH /api/nodes/:mac position handler) or a node connects/registers. The spaxel-sim default node placement (createVirtualNodes) and/or a default zone may also need to seed positions so the simulator path exercises real geometry. Verify by re-running TestFullE2EIntegration and asserting len(blobs) > 0 during the run (the test currently ducks this with 'we verify the API responds correctly rather than asserting a minimum count').","design":"","acceptance_criteria":"","notes":"","status":"open","priority":1,"issue_type":"task","created_at":"2026-07-03T00:11:33.217563362Z","updated_at":"2026-07-03T19:23:52.843063709Z","source_repo":".","compaction_level":0,"labels":["bug","deferred","fusion","localization","umbrella"],"dependencies":[{"issue_id":"bf-4q5w","depends_on_id":"bf-5jeo","type":"blocks","created_at":"2026-07-03T19:23:17.400573758Z","created_by":"cli","thread_id":""}]} -{"id":"bf-4qto","title":"Add default values for blob identity fields","description":"## Objective\nEnsure all blob identity fields have appropriate default or undefined values across all contexts.\n\n## Scope\n- Review all updated blob creation code\n- Add proper undefined or default values where needed\n- Ensure consistency in default value handling\n\n## Acceptance Criteria\n- [ ] All personName fields set to undefined or appropriate default\n- [ ] All assignedColor fields set to undefined or appropriate default \n- [ ] All identityResolved fields set to undefined or appropriate default\n- [ ] Default handling is consistent across codebase","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T05:11:37.452698658Z","updated_at":"2026-07-06T05:11:37.452698658Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4qto","depends_on_id":"bf-5151","type":"blocks","created_at":"2026-07-06T05:11:57.963969215Z","created_by":"cli","thread_id":""}]} +{"id":"bf-4qto","title":"Add default values for blob identity fields","description":"## Objective\nEnsure all blob identity fields have appropriate default or undefined values across all contexts.\n\n## Scope\n- Review all updated blob creation code\n- Add proper undefined or default values where needed\n- Ensure consistency in default value handling\n\n## Acceptance Criteria\n- [ ] All personName fields set to undefined or appropriate default\n- [ ] All assignedColor fields set to undefined or appropriate default \n- [ ] All identityResolved fields set to undefined or appropriate default\n- [ ] Default handling is consistent across codebase","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T05:11:37.452698658Z","updated_at":"2026-07-06T22:54:18.019462186Z","source_repo":".","compaction_level":0,"labels":["failure-count:1","split-child"],"dependencies":[{"issue_id":"bf-4qto","depends_on_id":"bf-5151","type":"blocks","created_at":"2026-07-06T05:11:57.963969215Z","created_by":"cli","thread_id":""}]} {"id":"bf-4qwmy","title":"Simulator: GDOP overlay + shopping-list output (coverage gaps, recommended node count + positions)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.874114137Z","updated_at":"2026-05-06T03:18:31.808932181Z","closed_at":"2026-05-06T03:18:31.808932181Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} {"id":"bf-4truh","title":"Comprehensive notification system tests (open bead spaxel-40tl expansion)","description":"Open bead spaxel-40tl 'Write comprehensive tests for notification system' is open. The notify package (internal/notify/) has ntfy.go, pushover.go, webhook.go but tests are missing or incomplete. Needs tests covering: batching logic (30s dedup window), quiet hours gate (suppress non-critical during quiet window), morning digest aggregation, delivery retry logic, channel enable/disable, test-notification endpoint, and notification history API. The existing service_enhanced.go has complex batching logic that needs coverage.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-02T18:26:14.365679205Z","updated_at":"2026-05-02T18:26:51.067231767Z","closed_at":"2026-05-02T18:26:51.067231767Z","close_reason":"Duplicate of existing open bead spaxel-40tl which already comprehensively tracks notification system tests","source_repo":".","compaction_level":0} {"id":"bf-4wwt","title":"Inventory blob projection and derived types with construction sites","description":"Scope: the blob-shaped PROJECTION/derived types — read-only views built FROM a tracked blob for a subsystem (not new tracked entities). Any field added to a tracked blob that a projection copies must be propagated at each construction site.\n\nProjection types to document (definition file:line + every construction boundary site):\n- explainability.BlobSnapshot, explainability.BlobExplanation\n- falldetect.BlobSnapshot (distinct type, shared name)\n- volume.BlobState, volume.BlobPos, api.BlobPos\n- tracking.BlobEvent\n- replay.BlobUpdate\n- simulator.BlobResult\n\nReference (already enumerated — RE-VERIFY, do not re-discover): notes/bf-4bhd.md \"Related Blob-shaped projection types\" table (9 types, ~15 construction sites across handler.go, detector.go, pipeline.go, simulator/engine.go, tracking/tracker.go, volume/shape.go).\n\nAcceptance Criteria:\n- [ ] Each projection type definition recorded with file:line and which tracked-blob type it is built from\n- [ ] Every projection construction boundary site listed (file:line)\n- [ ] Re-locate moved sites with: grep -rn \"BlobSnapshot{|BlobState{|BlobPos{|BlobUpdate{|BlobEvent{|BlobResult{|BlobExplanation{\" mothership/\n- [ ] All line numbers verified against current HEAD\n- [ ] Findings written to notes/bf--projections.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:03:36.904550067Z","updated_at":"2026-07-06T19:39:55.785656755Z","closed_at":"2026-07-06T19:39:55.785656755Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-4wwt","depends_on_id":"bf-5uzm","type":"blocks","created_at":"2026-07-06T15:04:21.277568858Z","created_by":"cli","thread_id":""}],"comments":[{"id":80,"issue_id":"bf-4wwt","author":"cli","text":"Re-verified all 9 blob-shaped projection/derived types and 15 non-test construction sites against current HEAD (68bd308) — every type definition (handler.go:27/95, detector.go:69, shape.go:139/1080, triggers.go:624, tracker.go:52, types.go:303, engine.go:80) and every construction site (grep -rnE of BlobSnapshot{/BlobState{/BlobPos{/BlobUpdate{/BlobEvent{/BlobResult{/BlobExplanation{) matches notes/bf-4wwt-projections.md exactly. Work committed in 68bd308 and pushed to origin/main. Findings written to notes/bf-4wwt-projections.md (companion to bf-4bhd): 9 types, 15 sites + 1 falldetect boundary (main.go:2288), with corrections noting api.BlobPos is test-only/dead, replay.BlobUpdate & simulator.BlobResult are synthetic (not blob-derived), and volume.BlobState's only blob-derived site is shape.go:575.","created_at":"2026-07-06T19:38:49.791215302Z"}]} {"id":"bf-4zio","title":"BLE-to-blob 3D visualization: per-person colored humanoid + name labels in scene","description":"Phase 6 item 30 requires the 3D dashboard to show identified persons as colored humanoid figures with name labels ('Alice is in Kitchen') instead of generic 'Blob #2' markers. The BLE registry (spaxel-2wg) is closed but the frontend visualization using resolved identities is not implemented. Update the Three.js scene to render identity-matched blobs as named humanoid meshes.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-26T22:14:42.798449947Z","updated_at":"2026-07-06T12:46:52.964790297Z","source_repo":".","compaction_level":0,"labels":["umbrella"],"dependencies":[{"issue_id":"bf-4zio","depends_on_id":"bf-3dip","type":"blocks","created_at":"2026-07-06T04:05:42.020469537Z","created_by":"cli","thread_id":""}]} -{"id":"bf-5151","title":"Add identity fields to blob creation code","description":"## Objective\nUpdate all blob creation code to include the new identity fields (personName, assignedColor, identityResolved).\n\n## Scope\n- Use the list of blob creation sites from previous bead\n- Add the three identity fields to each blob creation\n- Set fields to undefined for existing blobs\n\n## Acceptance Criteria\n- [ ] All blob creation sites from previous bead are updated\n- [ ] personName, assignedColor, identityResolved added to each\n- [ ] Fields are set to undefined for existing code\n- [ ] No blob creation code is missed","design":"","acceptance_criteria":"","notes":"","status":"in_progress","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T05:11:33.679872377Z","updated_at":"2026-07-06T21:00:59.097572783Z","source_repo":".","compaction_level":0,"labels":["failure-count:1","split-child"],"dependencies":[{"issue_id":"bf-5151","depends_on_id":"bf-4bhd","type":"blocks","created_at":"2026-07-06T05:11:54.079027318Z","created_by":"cli","thread_id":""}],"comments":[{"id":86,"issue_id":"bf-5151","author":"cli","text":"bf-5cgc handoff → notes/bf-5cgc-handoff.md. You are the PRIMARY owner of the three Go Tier-1 identity-leak fixes: (1) automation cmd/mothership/main.go:2303 + automation.TrackedBlob def internal/automation/engine.go:1337 — add PersonID, populate from identityMatcher.GetMatch(b.ID), engine resolves label/color via personProvider (engine.go:952); (2) explainability main.go:2206 + explainability.BlobSnapshot handler.go:95 — add identity fields, FOLD the parallel identityMap (main.go:2216) into the snapshot, drop the side-channel arg from UpdateBlobs (:2236); (3) volume main.go:2326 + volume.BlobPos shape.go:1080 — one-line PersonID populate (field already exists, consumer stubbed at shape.go:624). Mirror the reference pattern analytics.TrackUpdate (main.go:2267-2271) which pulls PersonID via GetMatch. NOTE: despite the sibling beads' TS framing, these are GO backend edits — see scope note §3.","created_at":"2026-07-06T20:38:59.973880259Z"}]} +{"id":"bf-5151","title":"Add identity fields to blob creation code","description":"## Objective\nUpdate all blob creation code to include the new identity fields (personName, assignedColor, identityResolved).\n\n## Scope\n- Use the list of blob creation sites from previous bead\n- Add the three identity fields to each blob creation\n- Set fields to undefined for existing blobs\n\n## Acceptance Criteria\n- [ ] All blob creation sites from previous bead are updated\n- [ ] personName, assignedColor, identityResolved added to each\n- [ ] Fields are set to undefined for existing code\n- [ ] No blob creation code is missed","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T05:11:33.679872377Z","updated_at":"2026-07-06T22:44:51.417873267Z","closed_at":"2026-07-06T22:44:51.417873267Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","umbrella"],"dependencies":[{"issue_id":"bf-5151","depends_on_id":"bf-4bhd","type":"blocks","created_at":"2026-07-06T05:11:54.079027318Z","created_by":"cli","thread_id":""},{"issue_id":"bf-5151","depends_on_id":"bf-1mwy","type":"blocks","created_at":"2026-07-06T21:24:06.442551977Z","created_by":"cli","thread_id":""}],"comments":[{"id":86,"issue_id":"bf-5151","author":"cli","text":"bf-5cgc handoff → notes/bf-5cgc-handoff.md. You are the PRIMARY owner of the three Go Tier-1 identity-leak fixes: (1) automation cmd/mothership/main.go:2303 + automation.TrackedBlob def internal/automation/engine.go:1337 — add PersonID, populate from identityMatcher.GetMatch(b.ID), engine resolves label/color via personProvider (engine.go:952); (2) explainability main.go:2206 + explainability.BlobSnapshot handler.go:95 — add identity fields, FOLD the parallel identityMap (main.go:2216) into the snapshot, drop the side-channel arg from UpdateBlobs (:2236); (3) volume main.go:2326 + volume.BlobPos shape.go:1080 — one-line PersonID populate (field already exists, consumer stubbed at shape.go:624). Mirror the reference pattern analytics.TrackUpdate (main.go:2267-2271) which pulls PersonID via GetMatch. NOTE: despite the sibling beads' TS framing, these are GO backend edits — see scope note §3.","created_at":"2026-07-06T20:38:59.973880259Z"},{"id":98,"issue_id":"bf-5151","author":"cli","text":"Closed by claude-code-glm-5-alpha.\n\n## Summary\n\nAdded the three canonical identity fields (PersonName / AssignedColor / IdentityResolved) to every in-scope blob-creation site, per the bf-1q3m consolidated inventory. The fields surface as undefined (omitted) for existing blobs.\n\n### Field definitions (canonical, camelCase JSON keys match dashboard/types/spaxel.d.ts)\n- PersonName string json:\"personName,omitempty\" (\"\" = unresolved)\n- AssignedColor string json:\"assignedColor,omitempty\" (\"\" = unresolved)\n- IdentityResolved *bool json:\"identityResolved,omitempty\" (tri-state: nil=unattempted, &true=resolved, &false=failed)\n\n### In-scope types now carrying the 3 fields (8 total, verified at HEAD d0ac1b2)\nPrimary (added a612584): signal.TrackedBlob, tracker.Blob (3D), tracking.Blob (2D), automation.TrackedBlob.\nProjections (added 01a415d): api.Track (/api/tracks), dashboard.blobJSON (/ws/dashboard). /api/blobs needs no projection (serializes signal.TrackedBlob directly).\nTier-1 leak targets (added c3f4b0d / 29a114c + 7309564): explainability.BlobSnapshot, volume.BlobPos.\n\n### Construction sites — all 21 production sites accounted for\n5 primary + 16 projection = 21. The 6 in-scope sites' types carry the fields (left at Go zero values at construction → omitempty → undefined in JS). The 15 out-of-scope sites are enumerated with reasons in notes/bf-5151-coverage.md (pre-identity Fresnel peak C1; synthetic sim/replay P8-P10; anon-struct/lifecycle/trigger-state sites; rendering fallbacks; dead api.BlobPos). Zero sites silently dropped.\n\n### Tests added (this commit d0ac1b2)\nTable-driven serialization guards on the 3 types whose fields are reachable via JSON: signal/identity_fields_test.go, automation/identity_fields_test.go, api/tracks_identity_test.go. Each asserts (1) zero-value blobs omit all three (undefined), (2) resolved identity emits camelCase keys, (3) *bool tri-state: &false still emits identityResolved:false.\n\n### Gate\ngofmt clean; go vet ./... exit 0; go test ./... 50 packages ok, 0 failures.\n\nPopulation from the BLE identity sidecar (ble.IdentityMatcher.GetMatch) is intentionally a follow-up bead — this one only adds the type surface as undefined, exactly as scoped.","created_at":"2026-07-06T22:44:43.376331478Z"}]} {"id":"bf-5312","title":"Add the IO-6 hard-gate scenario test and a strict block-back triage gate","description":"Split from bf-5jeo (verification capstone). Depends on the TestFullE2EIntegration child. Child 4 of 4 (final, blocks the parent).\n\nCONTEXT: The IO-6 release hard-gate is \"walker produces a tracked blob\" with 4 nodes + 2 walkers running spaxel-sim (the config described in bf-4q5w; see also internal/signal/processor.go:607,626 which already documents that no engine feeds the live blob loop). There is currently no dedicated test that exercises this gate end-to-end.\n\nSCOPE:\n- Add a dedicated IO-6 hard-gate scenario (a tagged e2e test or runbook+test) that runs RunSimulator(ctx, 4 nodes, 2 walkers, 20 rate, duration) and asserts a walker produces a tracked blob end-to-end, reusing the assertion helpers from bf-2330.\n- CRITICAL TRIAGE GATE: if blobs are still zero after upstream bf-4q5w lands, DO NOT weaken the assertion. Instead: (a) keep this child blocked, (b) capture a finding as a comment with diagnostic evidence (fusion accumulation grid state, node positions, traces under .beads/traces/), and (c) feed that finding back to upstream bf-4q5w (fusion Engine.SetNodePosition wiring) rather than weakening the assertion.\n\nACCEPTANCE CRITERIA:\n- An IO-6 hard-gate scenario exists that asserts a tracked blob is produced with 4 nodes + 2 walkers running spaxel-sim.\n- When green: the IO-6 release hard-gate (walker produces a tracked blob) is satisfied end-to-end.\n- When zero blobs: the assertion stays strict, and a finding with trace evidence is filed back to bf-4q5w instead of weakening — never silently degrade this test.\n- bf-5jeo (this capstone umbrella) cannot close until this child is green.","design":"","acceptance_criteria":"","notes":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-07-06T18:51:03.299000490Z","updated_at":"2026-07-06T18:51:03.299000490Z","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5312","depends_on_id":"bf-2aqf","type":"blocks","created_at":"2026-07-06T18:51:13.389487862Z","created_by":"cli","thread_id":""}]} {"id":"bf-55rp","title":"Inventory primary tracked-blob Go types and direct construction sites","description":"Scope: the FIVE primary tracked-blob Go types that make up the tracked-blob lifecycle, plus the api.TrackedBlob alias. Document each type definition (file:line) and every DIRECT struct-literal construction site.\n\nTypes: tracking.Blob (2D), tracker.Blob (3D, identity-bearing), fusion.Blob (peak), automation.TrackedBlob, signal.TrackedBlob (+ api.TrackedBlob alias).\n\nThis work is mostly DONE in two existing reports — RE-VERIFY, do not re-discover:\n- notes/bf-3ldj-findings.md (sections 1-3)\n- notes/bf-4bhd.md (Blob Type Definitions + Patterns 1-2)\n\nAcceptance Criteria:\n- [ ] Each of the 5 primary types has its struct definition recorded with file:line, verified against current HEAD\n- [ ] Every direct struct-literal construction site listed (file:line + pattern: &Blob{} / Blob{} / TrackedBlob{})\n- [ ] The api.TrackedBlob alias relationship to signal.TrackedBlob is noted\n- [ ] Re-locate moved sites with: grep -rn \"Blob{|&Blob{|TrackedBlob{\" mothership/\n- [ ] Findings written to notes/bf--primary-types.md\n\nOut of scope: projection/derived types (child 3), JS/TS (child 4), test fixtures, the unrelated browser new Blob() download API.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:02:58.355494649Z","updated_at":"2026-07-06T18:59:02.545840787Z","closed_at":"2026-07-06T18:59:02.545840787Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"comments":[{"id":78,"issue_id":"bf-55rp","author":"cli","text":"Re-verified all 5 primary tracked-blob Go types + the api.TrackedBlob alias against HEAD c4d42e8. Findings in notes/bf-55rp-primary-types.md (committed d99f42d, pushed). Corrections vs prior reports: (1) two construction sites moved — automation.TrackedBlob main.go:2213->:2303, signal.TrackedBlob main.go:5384->:5494; (2) tracking.Blob (2D) field list drifted — PersonName/AssignedColor/IdentityResolved removed, identity fields now match tracker.Blob (3D) exactly. All 6 type definitions (5 structs + 1 pure = alias) confirmed at their reported file:line. Whole-repo sweep confirms exactly 5 production direct-literal construction sites, one per primary type (fusion:260 Blob{}, tracking:160 &Blob{}, tracker:162 &Blob{}, main.go:2303 automation.TrackedBlob{}, main.go:5494 sigproc.TrackedBlob{}). All 5 acceptance criteria met. Documentation only — no Go source changed.","created_at":"2026-07-06T18:57:23.168436965Z"}]} {"id":"bf-55sg5","title":"Mobile-responsive expert mode","description":"## Goal\nMake expert mode fully functional on mobile devices with touch gestures.\n\n## Scope\n- Touch orbit/pan/zoom: single-finger rotate, two-finger pan, pinch to zoom (already supported by Three.js OrbitControls)\n- Hamburger menu for panels: collapsible sidebar for fleet status, settings, zones, triggers\n- Responsive layout: panels slide in from bottom on mobile, from right on desktop\n- Touch-optimized buttons: minimum 44×44px tap targets\n- No hover-dependent UI: all interactions work with tap\n- Mobile-specific shortcuts: long-press for context menu (replaces right-click)\n\n## Location\ndashboard/static/js/mobile.js (new module)\ndashboard/static/css/mobile.css (new stylesheet)\n\n## Acceptance\n- Three.js scene responds to touch gestures (orbit, pan, zoom)\n- Hamburger menu opens panel navigation\n- Panels slide in from bottom on mobile\n- All buttons are touch-friendly (≥44px)\n- No features require hover\n- Long-press context menu works on mobile","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.813640292Z","updated_at":"2026-05-06T11:04:43.020442678Z","closed_at":"2026-05-06T11:04:43.020442678Z","close_reason":"Completed","source_repo":".","compaction_level":0} @@ -96,6 +99,7 @@ {"id":"bf-5rulx","title":"Simulator: synthetic walkers + CSI generation (user paths, random walk, propagation model integration)","description":"","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-06T00:40:16.874026391Z","updated_at":"2026-05-06T02:45:17.053495568Z","closed_at":"2026-05-06T02:45:17.053495568Z","close_reason":"Completed","source_system":"","source_repo":".","deleted_by":"","delete_reason":"","original_type":"","compaction_level":0,"compacted_at_commit":"","sender":""} {"id":"bf-5txbb","title":"Fleet status page","description":"## Goal\nFull table view of all registered nodes with all metrics, bulk actions, camera fly-to on click.\n\n## Scope\nTable columns:\n- Name: user-assigned friendly name\n- MAC: hardware address\n- Role: TX/RX/TX_RX — editable dropdown\n- Position: (x, y, z) — click to highlight node in 3D view and fly camera to it\n- Firmware: version string + 'Update available' badge\n- RSSI: last reported WiFi signal strength\n- Status: ONLINE/STALE/OFFLINE with colored indicator\n- Uptime: time since last boot\n- Actions: Restart, Update, Remove, Identify (blink LED)\n\nGlobal actions:\n- Update All (rolling OTA)\n- Re-baseline All\n- Export Config\n- Import Config\n\n## Location\ndashboard/static/js/fleet.js (new module, extract from existing code)\ninternal/api/fleet.go (already exists)\n\n## Acceptance\n- Table shows all registered nodes\n- Click position → camera flies to node in 3D view\n- Role dropdown changes node role\n- Actions execute correctly\n- Bulk actions work on all nodes\n- Export/import config works","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:06:29.834674580Z","updated_at":"2026-05-06T11:48:29.548615767Z","closed_at":"2026-05-06T11:48:29.548615767Z","close_reason":"Implemented fleet status page with full table view and bulk actions:\n- Added dashboard/static/js/fleet.js: core fleet management module with API functions\n- Added dashboard/static/css/fleet-page.css: complete styling for fleet table\n- Table columns: Name, MAC, Status, Firmware, Uptime, Position, Role, Health, Packet Rate, Temperature, Actions\n- Bulk actions: Update All (rolling OTA), Re-baseline All, Export Config, Import Config\n- Camera fly-to on position click\n- Node actions: identify (LED blink), update firmware, remove, re-assign role\n- CSV export for reporting","source_repo":".","compaction_level":0} {"id":"bf-5uzm","title":"Map cross-package blob conversion boundaries in main.go","description":"Scope: the cross-package conversion boundaries in cmd/mothership/main.go where a tracked blob is converted from one type to another. These are the CRITICAL identity-leak sites — the actual goal of the umbrella effort.\n\nSites to re-verify against HEAD:\n- main.go:5384 — fusion.Blob peak to sigproc.TrackedBlob (E1)\n- main.go:2213 — tracked blob to automation.TrackedBlob, IDENTITY DROPPED (E2)\n- main.go:2116 — tracked blob to explainability.BlobSnapshot, IDENTITY DROPPED (E3)\n- main.go:2236 — tracked blob to volume.BlobPos\n\nFor each: source type, target type, which fields are copied, which identity fields (if any) are dropped, and whether the target type even carries identity fields.\n\nReference: notes/bf-3ldj-findings.md section 5 (Tier 1) + section 6 (data flow), notes/bf-4bhd.md Pattern 4.\n\nAcceptance Criteria:\n- [ ] Every main.go blob-conversion loop enumerated with source type, target type, copied fields\n- [ ] The two identity-dropping boundaries (:2213 automation, :2116 explainability) explicitly flagged as the leak to fix\n- [ ] Whether the target type carries identity fields is noted for each boundary\n- [ ] All line numbers verified against current HEAD\n- [ ] Findings written to notes/bf--conversions.md","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T15:03:09.093500667Z","updated_at":"2026-07-06T19:23:33.753528151Z","closed_at":"2026-07-06T19:23:33.753528151Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["deferred","split-child"],"dependencies":[{"issue_id":"bf-5uzm","depends_on_id":"bf-55rp","type":"blocks","created_at":"2026-07-06T15:04:21.183973883Z","created_by":"cli","thread_id":""}],"comments":[{"id":79,"issue_id":"bf-5uzm","author":"cli","text":"Implemented: findings written to notes/bf-5uzm-conversions.md and pushed (commit 585532f). Re-verified all four named conversion sites against HEAD — task line numbers had drifted +90..+110; corrected to E1 main.go:5494, E2 :2303, E3 :2206, volume :2326 (grep-confirmed against current main.go). Enumerated all 9 blob-projection loops in the live 10 Hz fusion loop with source/target types + copied fields. Flagged E2 (automation.TrackedBlob, no identity field) and E3 (explainability.BlobSnapshot, no identity field) as the two identity-leak boundaries to fix; volume.BlobPos carries PersonID but leaves it unpopulated. Noted per-target whether the type carries identity fields. Recorded a material correction: the live loop runs blobTracker (greedy associator), not tracker.TrackManager/applyIdentity — resolved identity lives in the ble.IdentityMatcher sidecar and is never written onto the blob struct, so every identity-aware boundary must pull from GetMatch(blob.ID) at the conversion site (analytics.TrackUpdate :2267 is the reference pattern). All 5 acceptance criteria met.","created_at":"2026-07-06T19:22:15.401966536Z"}]} +{"id":"bf-5v3q","title":"Add identity fields to volume.BlobPos type","description":"## Objective\nAdd the three canonical identity fields — PersonName, AssignedColor, IdentityResolved — to the `volume.BlobPos` struct type, which today carries only `PersonID`. This is the Tier-1 #3 type gap from the bf-1q3m/bf-5cgc handoff.\n\n## Scope (grounded in notes/bf-1q3m-consolidated.md §6 Tier-1 #3)\n- Edit `mothership/internal/volume/shape.go:1080` — `type BlobPos struct` is today `{ID, PersonID, X, Y, Z}` (PersonID already present at :1082, left \"\" at the conversion site). Add `PersonName`, `AssignedColor`, `IdentityResolved *bool` with JSON tags (`person_name`, `assigned_color`, `identity_resolved`), mirroring commit 1446ccf.\n- The production creation literal at `cmd/mothership/main.go:2326` needs NO change for the field declarations — Go zero values = undefined in JS via omitempty. Fields-as-undefined, consistent with 1446ccf + 248d0e0.\n- OUT OF SCOPE (separate downstream bead): populating PersonID/PersonName/etc. from `identityMatcher.GetMatch(blob.ID)` at main.go:2326 (the one-line populate that unblocks person-filtered volume triggers). This child adds field DECLARATIONS only, left at zero. Do NOT add GetMatch calls.\n\n## Acceptance Criteria\n- [ ] PersonName, AssignedColor, IdentityResolved added to volume.BlobPos (shape.go:1080) alongside the existing PersonID, with correct JSON tags\n- [ ] Existing PersonID field retained; no populate/sidecar logic added at main.go:2326\n- [ ] gofmt clean; `go vet ./internal/volume/...` clean; `go test ./internal/volume/...` passes","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-5-alpha","created_at":"2026-07-06T21:23:54.831218800Z","updated_at":"2026-07-06T21:43:43.622017092Z","closed_at":"2026-07-06T21:43:43.622017092Z","close_reason":"Completed","source_repo":".","compaction_level":0,"labels":["split-child"],"dependencies":[{"issue_id":"bf-5v3q","depends_on_id":"bf-2ibc","type":"blocks","created_at":"2026-07-06T21:24:06.366185817Z","created_by":"cli","thread_id":""}],"comments":[{"id":94,"issue_id":"bf-5v3q","author":"cli","text":"Implemented: added PersonName, AssignedColor, and IdentityResolved (*bool) to volume.BlobPos in mothership/internal/volume/shape.go alongside the existing PersonID. Snake_case JSON tags (person_name, assigned_color, identity_resolved) with omitempty — fields left at zero values (declarations only); production literal at main.go:2326 unchanged since Go zero values serialize as omitted. No GetMatch/populate logic added (out of scope, downstream bead). Mirrors 1446ccf but with snake_case tags per the volume package's internal convention. Verified: gofmt clean, go vet ./... clean, go test ./... passes. Committed 29a114c, pushed to main.","created_at":"2026-07-06T21:42:58.610940269Z"}]} {"id":"bf-5vhya","title":"CI: pipeline timing benchmark gate","description":"## Goal\nAdd a benchmark that enforces the fusion loop timing budget as a CI quality gate, per plan §Quality Gates / Definition of Done (item 9).\n\n## What to build\n\nFile: internal/localizer/fusion/timing_budget_test.go\n\nRun the full fusion pipeline (phase sanitization → feature extraction → Fresnel accumulation → peak extraction → UKF update) against synthetic CSI data from spaxel-sim output.\n\nAssert:\n- Median fusion iteration < 15 ms over 600 iterations (60 seconds at 10 Hz)\n- P99 < 40 ms (hard limit)\n\n## CI integration\nAdd to Argo Workflows CI step after go test ./...:\n go test -bench=BenchmarkFusionLoop -benchtime=60s -count=1 ./internal/localizer/fusion/ | tee /tmp/bench.txt\n # fail if median exceeds 15ms threshold\n\n## Acceptance\n- Benchmark runs in the Argo CI workflow\n- Workflow fails if median latency exceeds 15 ms on the CI runner (allowance: 2x for slower hardware → 30 ms CI threshold, 15 ms production target)","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-golf","created_at":"2026-05-02T12:09:00.487025943Z","updated_at":"2026-05-04T14:25:01.352506963Z","closed_at":"2026-05-04T14:25:01.352506963Z","close_reason":"Implementation already complete in commit 7afbdc9. The timing budget benchmark:\n\n1. File: mothership/internal/localizer/fusion/timing_budget_test.go\n - Runs full fusion pipeline (phase sanitization → feature extraction → Fresnel accumulation → peak extraction → UKF update)\n - Uses synthetic CSI data simulating 4 nodes with 2 walkers\n - Runs 600 iterations (60 seconds at 10 Hz)\n\n2. Timing constraints enforced:\n - Median fusion iteration: 2.6ms (well below 15ms production target and 30ms CI threshold)\n - P99: ~10ms (well below 40ms hard limit)\n\n3. CI integration: .github/workflows/benchmark-ci.yml\n - Benchmark runs on every push/PR to main\n - Workflow fails if median exceeds 30ms (CI threshold)\n - Workflow fails if P99 exceeds 40ms (hard limit)\n\nAll acceptance criteria met.","source_repo":".","compaction_level":0} {"id":"bf-5wb3n","title":"MQTT bidirectional commands: security_mode and rebaseline subscriptions","description":"Plan specifies that the mothership subscribes to {prefix}/command/security_mode (arm|disarm) and {prefix}/command/rebaseline (zone name or 'all') so Home Assistant automations can control these without opening the dashboard. The mqtt package has SubscribeToSystemMode but no SubscribeToRebaseline, and neither command subscription is wired in main.go to actual arm/disarm or rebaseline actions. Needs: (1) SubscribeToRebaseline in mqtt/client.go, (2) wiring both subscriptions to the relevant internal handlers in main.go when MQTT is configured.","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-02T18:25:06.167277244Z","updated_at":"2026-05-05T16:52:27.277793127Z","closed_at":"2026-05-05T16:52:27.277793127Z","close_reason":"Completed","source_repo":".","compaction_level":0} {"id":"bf-5wfsa","title":"Pre-deployment simulator (Component 17)","description":"## Goal\nBefore purchasing hardware, users can define their space, place virtual nodes, and run physics-based simulation to see expected detection quality.\n\n## Scope\n- Space definition: same 3D editor used for real setup — draw room boxes, set dimensions\n- Virtual nodes: place ghost nodes (wireframe, dashed links) that participate in GDOP computation\n- Simulation engine: simplified ray-based propagation (direct path + first-order reflections)\n- Synthetic walkers: virtual people moving along user-defined paths or random walk\n- Visualization: GDOP overlay, expected detection quality, coverage gaps highlighted\n- Outputs: minimum node count recommendation, optimal positions for N nodes, accuracy estimates, shopping list\n\n## Location\ndashboard/static/js/simulator.js (new module)\ninternal/sim/propagation.go (new package)\n\n## Acceptance\n- User draws room, places 2-4 virtual nodes\n- Click 'Simulate' → synthetic walkers generate CSI using same propagation model\n- GDOP overlay shows expected detection quality across floor\n- 'Shopping list' shows recommended node count and positions\n- 'Add another node here' highlights worst-GDOP positions","design":"","acceptance_criteria":"","notes":"","status":"closed","priority":2,"issue_type":"task","assignee":"claude-code-glm-4.7-foxtrot","created_at":"2026-05-05T04:05:43.355796818Z","updated_at":"2026-05-06T03:35:02.980287642Z","closed_at":"2026-05-06T03:35:02.980287642Z","close_reason":"Completed","source_repo":".","compaction_level":0,"dependencies":[{"issue_id":"bf-5wfsa","depends_on_id":"bf-5xftp","type":"blocks","created_at":"2026-05-06T00:40:16.874228656Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-2enwo","type":"blocks","created_at":"2026-05-06T00:40:16.875275352Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-5rulx","type":"blocks","created_at":"2026-05-06T00:40:16.875320778Z","created_by":"batch","thread_id":""},{"issue_id":"bf-5wfsa","depends_on_id":"bf-4qwmy","type":"blocks","created_at":"2026-05-06T00:40:16.875374254Z","created_by":"batch","thread_id":""}]} diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index 9c1e9eb..4ad4bb5 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -1446ccf65fe5366db9a1ac0e1adef608548473d5 +520f9fa4f18ac1ce23d4711f3768a46147174e9e diff --git a/mothership/cmd/mothership/main.go b/mothership/cmd/mothership/main.go index e5e9e6a..dae96d6 100644 --- a/mothership/cmd/mothership/main.go +++ b/mothership/cmd/mothership/main.go @@ -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) diff --git a/mothership/internal/explainability/identity_fields_test.go b/mothership/internal/explainability/identity_fields_test.go new file mode 100644 index 0000000..220fad3 --- /dev/null +++ b/mothership/internal/explainability/identity_fields_test.go @@ -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 } diff --git a/mothership/internal/tracker/identity_fields_test.go b/mothership/internal/tracker/identity_fields_test.go new file mode 100644 index 0000000..1a6842a --- /dev/null +++ b/mothership/internal/tracker/identity_fields_test.go @@ -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 } diff --git a/mothership/internal/tracking/identity_fields_test.go b/mothership/internal/tracking/identity_fields_test.go new file mode 100644 index 0000000..6fccffa --- /dev/null +++ b/mothership/internal/tracking/identity_fields_test.go @@ -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 } diff --git a/mothership/internal/volume/identity_fields_test.go b/mothership/internal/volume/identity_fields_test.go new file mode 100644 index 0000000..a5a58fe --- /dev/null +++ b/mothership/internal/volume/identity_fields_test.go @@ -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 }