Commit graph

992 commits

Author SHA1 Message Date
jedarden
23876557bd test(dashboard): runtime backward-compat for renderer with identity-less blobs (bf-20cl7)
Add a jsdom test (dashboard/js/backward-compat.test.js) that loads the
ambient Canvas 2D renderer for real and drives a render frame — the first
test suite to actually exercise the renderer (ambient.test.js skips itself
when SpaxelAmbientRenderer is undefined; viz3d cannot load under jsdom due
to Three.js/WebGL).

Covers parent bf-2gmx's core runtime acceptance criterion:
- identity-LESS blob (no personName/assignedColor/identityResolved, e.g. sim
  without --ble): renders with no throw, sane fallback label '?' and neutral
  grey color, no 'undefined'/'NaN' in output.
- identity-resolved blob: renders WITH the person name + per-person color.
- mixed fleet: both paths together, no undefined/NaN.

To make the identity-field read path reachable, fix a latent bug in the
renderer's updateState(): it copied zones/portals/nodes/alerts but not
blobs, so drawPeople() (which iterates currentState.blobs) never drew any
people — ambient mode rendered no one. Add the missing
`if (state.blobs) currentState.blobs = state.blobs;` mirroring the
neighboring copies. getPersonColor()/getFirstName() already guard against
undefined names (bf-gcotl).

9/9 pass; full suite shows no new regressions (proactive/sidebar-timeline
failures are pre-existing, confirmed by stash baseline).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 18:37:36 -04:00
jedarden
ce115e2f66 fix(dashboard): harden identity-name color helpers against undefined (bf-gcotl)
Audit bf-gcotl found every dashboard read of the blob identity fields
(personName/assignedColor/identityResolved + snake_case aliases
person_name/assigned_color/person_label/person_color) is already guarded at
the call site — zero unguarded reads (full verdict table recorded on bf-2gmx).

The only two spots that dereference an identity NAME (.length/.charCodeAt)
without a local guard were the reusable color helpers, both safe today but
hardened defensively so the guard contract holds for future callers:
- ambient_renderer.js getPersonColor: if (!personName) return '#6b7280'
- ble-panel.js getColorForPerson:    if (!personName) return '#888888'

Type contract unchanged and verified: tsc --noEmit -p dashboard/tsconfig.json
passes; spaxel.d.ts still declares personName?/assignedColor?/identityResolved?
optional with deprecated aliases; blob-identity.check.ts asserts the
identityResolved true/false/undefined tri-state.
2026-07-08 18:14:34 -04:00
jedarden
14e0d0712d docs(bf-15oi): capture end-to-end identity-blob runtime logs (zero errors)
Final acceptance step of parent bf-f841. Run the full hardware-free
identity-blob scenario (mothership + spaxel-sim --ble, nodes=4 walkers=1
rate=30 seed=42 duration=25s) and capture the runtime log proving there are
no errors from undefined/missing identity fields.

- run-sim-identity.sh: additive CAPTURE_DIR hook — on cleanup, copy the
  text log artifacts (mothership.log, sim.log, identity_blob.json) to a
  durable dir before the temp data dir is removed. Deliberately excludes
  giant/regenerable binaries (csi_replay.bin, *.db). Unset = prior behavior.
- docs/notes/bf-15oi-runtime-capture/: mothership.log (2414 lines), sim.log,
  identity_blob.json (PASS evidence — blob 7 carries non-empty identity),
  SUMMARY.md.

Independently verified against the captured log:
  panic=0  nil-deref=0  runtime/fatal/SIGSEGV=0
  undefined/missing-field=0  [ERROR]=0  [FATAL]=0
Log levels: 2340 INFO / 10 DEBUG / 10 WARN (all benign hw-free noise) /
0 ERROR / 0 FATAL. Script exit 0; full 10-step graceful shutdown.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-15oi
2026-07-08 18:14:34 -04:00
jedarden
1bfcc2769b test(bf-2v9g): add runtime identity-propagation tests for notify/analytics/falldetect
Adds three projection-level tests proving blob identity flows through every
runtime projection (not just the unit-test structs guarded by bf-5151):
- notify: GenerateFloorPlanThumbnail observes blob.Identity (identified green
  vs unknown blue), nil-safe when empty
- analytics: NotificationAlertHandler.SendAlert propagates PersonName into
  Identity / person_name / person_id, nil-safe for unidentified events
- falldetect: triggerFallAlert carries identityFunc(blob.ID) into event.Identity
  (observes "Alice"), nil-safe when identityFunc unset

Runtime wiring independently verified at HEAD: cmd/mothership main.go:2532
(SetIdentityFunc -> identityMatcher.GetMatch(blobID).Label()), :1823
(anomalyAlertAdapter wired as alert handler bridging to notify), and
:2476/:2552 (event.Identity / personName = match.Label()).

go vet ./... clean (exit 0); all three suites PASS.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-2v9g
2026-07-08 18:14:34 -04:00
jedarden
5d3f3180cc docs(bf-gdfwx): record identity capstone PASS on live /api/blobs
run-sim-identity.sh (the bf-2m534/bf-gdfwx THIRD-link acceptance harness)
exit-0s against the live hardware-free runtime: a served /api/blobs blob
carries non-empty canonical identity — personName="Alice",
assignedColor="#ec4899", identityResolved=true, identity_source="ble" —
corroborated by GET /api/ble/matches (blob_id=7, Alice, conf=0.600). No
panic. Records the captured JSON + exact flags/sequence in
notes/hardware-free-runtime.md so the capstone is reused, not re-derived.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 15:04:11 -04:00
jedarden
58faa9ac4a docs(bf-1yr1w): record --ble opt-in (SIM_BLE=1) in run-sim-local + re-verify matcher PASS
bf-1yr1w criterion 3: the --ble opt-in must be recorded in the canonical
runtime script (not a fork). scripts/run-sim-local.sh now carries an
opt-in env passthrough:

  SIM_BLE=1 ./scripts/run-sim-local.sh   # appends --ble to the sim invocation
  ./scripts/run-sim-local.sh             # default: --ble OFF, CSI->blob path unchanged

Default-off preserves the script's existing gate (CSI->blob). The --ble +
fixture + matcher gate remains scripts/run-sim-ble-match.sh; only the
opt-in itself is added here. A matching "--ble opt-in (SIM_BLE=1)"
subsection is added to notes/hardware-free-runtime.md so the recipe is
recorded there, not only in the fixture note.

Also chmod +x scripts/run-sim-ble-match.sh: it was committed non-executable
(mode 100644), so its documented invocation (./scripts/run-sim-ble-match.sh)
failed with EACCES. Now 100755 like the other run-sim-*.sh scripts.

Re-verified PASS (fresh run, 2026-07-08): bash scripts/run-sim-ble-match.sh
-> exit 0, GetMatch() returns "Alice" for blob_id=7 (confidence=0.714,
is_ble_only=false) via /api/ble/matches; 0 rejects, no panic. This is the
evidence for criteria 1+2: the bf-21v71 wire fix (GetMatch() copied onto
the served slice) fires for the sim fixture and resolves the seeded person
onto a live CSI blob.

Hygiene (change is shell+md+mode only, zero Go source): go vet ./... clean;
go test ./internal/... ./cmd/... all ok. The containerized integration
packages (test/acceptance, tests/e2e) are unaffected and tests/e2e remains
deliberately RED per notes/hardware-free-runtime.md (gated on bf-4q5w /
bf-5jeo), not a regression of this shell+md change.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-1yr1w
2026-07-08 14:28:58 -04:00
jedarden
c42989e608 docs(bf-12h75): document --ble matcher recipe in hardware-free-runtime.md
Add a "BLE identity matcher (--ble)" subsection to
notes/hardware-free-runtime.md capturing the working --ble opt-in recipe
that scripts/run-sim-ble-match.sh exercises, so the parent capstone
(bf-2m534 / scripts/run-sim-identity.sh) reuses rather than re-derives.
Closes the bf-1yr1w acceptance gap: the --ble opt-in was documented only
for the FIXTURE (notes/ble-identity-fixture.md), not the matcher script.

Records: the script + the sim --ble invocation it runs; env params; the
key gotcha (SIM_WALKERS=1, NOT run-sim-local's 2 -- with two walkers the
nearest CSI blob is the second walker's, ~1.73m off, killing matchConf
below the 0.60 gate); the /api/ble/matches observation surface
(identityMatcher.GetAllMatches); and PASS evidence captured at HEAD
(blob_id=7, person_name=Alice, confidence=0.714, is_ble_only=false).
Links ble-identity-fixture.md for the fixture half rather than duplicating.

Also commits scripts/run-sim-ble-match.sh (child-2 artifact, was untracked)
so the recipe is reproducible at HEAD.

Verified at HEAD 7757149: bash scripts/run-sim-ble-match.sh -> PASS
(exit 0, blob_id=7 conf=0.714); go vet ./mothership/... clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-12h75
2026-07-08 14:10:50 -04:00
jedarden
7757149440 fix(bf-39mli): co-locate Alice CSI blob with triangulated point so matcher clears f_distance
The BLE identity matcher never attached Alice's triangulated device to a CSI
blob (matchConf stayed below MinMatchConfidence=0.60). Two compounding root
causes, both verified via [BLEDBG] instrumentation (SPAXEL_BLE_DEBUG):

1. RSSI round-trip inconsistency (cmd/sim/main.go): the sim emitted RSSI under
   a different log-distance path-loss model (-50-20log10(d)) than the matcher
   inverts (internal/ble: RefRSSI=-65, PathLossExp=2.5 -> rssiToDistance at
   identity.go:376). rssiToDistance then systematically underestimated true
   distance (~0.22x), triangulating every device too close to the node cluster
   and off its CSI blob. The BLE key was also "rssi" instead of "rssi_dbm",
   which ingestion maps to, leaving RSSIdBm at 0 and collapsing triangulation.
   Now emit under the matcher's own model constants and the "rssi_dbm" key, so
   rssiToDistance inverts RSSI back to the true node->walker distance.

2. Axis convention (identity.go): horizontal blob distance was computed over
   (X,Z), but Z is the height axis -- triangulated Z is pinned to the anchor
   mounting plane while the CSI blob centroid sits at person height (~1m), so
   (X,Z) misread that vertical gap as horizontal distance and suppressed the
   distance gate. Use the (X,Y) floor plane, matching createBLEOnlyTracks and
   the sim/fusion runtime convention (X,Y floor; Z up).

The [BLEDBG] instrumentation (bleDebug/SPAXEL_BLE_DEBUG gate + log lines) is
retained as live matcher diagnostics.

Verified (HEAD, uncommitted, independently re-run):
- SPAXEL_BLE_DEBUG=1 scripts/run-sim-ble-match.sh -> PASS:
  blob_id=7 conf=0.7139527093753523, [BLEDBG] assign hDist=0.61m matchConf=0.714
  (gate 0.60), reject_in_sim_log=0, peak_blobs=2.
- scripts/run-sim-local.sh -> PASS: peak_blob_count=2, reject_in_sim_log=0,
  sim --verify PASSED, 4 nodes online, fps 363 (no base-blob-path regression).
- go vet ./mothership/... + go build ./mothership/... + cmd/sim -> clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-39mli
2026-07-08 13:59:41 -04:00
jedarden
83c761a5e8 docs(bf-14wx5): document BLE identity fixture + verifiable run-sim-ble-fixture.sh
The bf-14wx5 deliverables were written but uncommitted (stuck-open-but-done —
auto-split fired at failure-count:3 only because the templated br close --body
had been failing). Commit the focused set so the bead is genuinely closable.

Adds:
- scripts/run-sim-ble-fixture.sh — canonical, self-verifying fixture: starts a
  fresh-data-dir mothership, registers a person (POST /api/people), binds the
  sim's advertised AA:BB:CC:DD:EE:00 (walker 0) via preregister + PUT, and gates
  on GET /api/ble/devices?registered=true showing person_id/person_name in BOTH
  phases.
- notes/ble-identity-fixture.md — reuse-note recording the exact REST sequence,
  the observed sim BLE address convention (AA:BB:CC:DD:EE:%02X per walker, ids
  from 0), and the answer to "must the device be seen-live first?": NO —
  PreregisterDevice sets last_seen_at=now so it falls inside the
  GetDevicesSeenInHours window with no live sighting, and the binding survives
  live sim --ble advertisements (ProcessRelayMessage upserts never touch
  person_id).

Verified 2026-07-08 at HEAD 36b9729: ./scripts/run-sim-ble-fixture.sh PASSes
phaseA (bind with NO live adv) and phaseB (binding survives live adv), exit 0.
All four acceptance criteria met; deliberately excludes scripts/run-sim-
identity.sh (parent bf-2m534 capstone) and notes/hardware-free-runtime.md
(bf-40hc re-verify).

Bead-Id: bf-14wx5
2026-07-08 13:59:41 -04:00
jedarden
09bd6e372d fix(bf-21v71): write BLE identity onto /api/blobs served slice (Stage 2b)
The diagnosis (bf-m1ynp, closed) pinned the broken link as match-found: a
blob served by /api/blobs never carried canonical identity because nothing
copied identityMatcher.GetMatch() back onto the served TrackedBlob slice —
the existing GetMatch sites only fed explainability snapshots and the ground-
truth collector, not the slice that SetTrackedBlobs publishes.

Add a Stage 2b write-back right after identityMatcher.UpdateBlobs(): for each
served blob, on a non-nil match copy PersonName/AssignedColor (+ the snake_case
aliases and IdentityConfidence/Source/PersonID) and set IdentityResolved=&true;
unmatched blobs get IdentityResolved=&false. Re-publish via SetTrackedBlobs so
/api/blobs and /api/tracks serve resolved canonical identity whenever the
matcher has a person match.

Update the now-stale comments that claimed the identity fields are left at zero
"until a follow-up bead wires the BLE sidecar" (signal/processor.go,
api/tracks.go) and the superseded bf-2fz8 "SetTrackedBlobs has no callers"
investigation note, to reflect that the live fusion loop now drives both.

Build green: cd mothership && go build ./...
Runtime observation that /api/blobs shows non-empty identity is the next child
(requires the sim fixture: --ble + a registered person), explicitly out of scope
per bf-21v71 acceptance criterion 2 (code-review-verifiable wiring).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 08:02:50 -04:00
jedarden
c472b0358e docs(bf-m1ynp): diagnose empty blob identity — match-found link broken
Read-only diagnosis for bf-5h1t child. The broken link in
matcher-active -> match-found -> written-onto-served-blob -> observable
is match-found: IdentityMatcher.GetMatch always returns nil in the
canonical reproduce path. Three defects feed it, all must be fixed:
(1) run-sim-local.sh omits --ble so no BLE adv / rssiCache empty;
(2) no person registered so GetAllPersonDevices (person_id NOT NULL)
    returns nothing;
(3) sim emits "rssi" but parser reads "rssi_dbm" -> RSSI=0.
The other three links are verified OK; the stale bf-2fz8 note in
processor.go is superseded by bf-243os.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 07:17:02 -04:00
jedarden
751cfaa49b docs(bf-40hc): record hardware-free runtime blob path acceptance
Re-verified ./scripts/run-sim-local.sh at HEAD f859f3e for bf-40hc: it
deterministically starts the mothership (healthy, no panic) and emits a
tracked blob (peak=2 via /api/blobs, first_blob=1s, sim --verify PASS,
0 rejects). Maps each bf-40hc acceptance criterion to evidence and
documents that the deliberately-RED tests/e2e tests are gated on bf-4q5w
(bf-5jeo capstone) — out of scope for bf-40hc, not a regression.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 07:17:02 -04:00
jedarden
eba9e74457 docs(bf-5cr3): capture deterministic hardware-free runtime reproduce artifact
Adds the single canonical reproduce for the working end-to-end runtime path
(sim connect -> CSI ingest -> phase/fusion -> tracked blob), so later beads
(e.g. bf-f841 blob-identity runtime checks) and reruns reuse it instead of
re-deriving the command sequence or the rate tuning that caused bf-40hc's
repeated failures.

- scripts/run-sim-local.sh: builds mothership + canonical sim
  (mothership/cmd/sim, the one with --verify) from source, starts the
  mothership on an ephemeral port/data dir, streams simulated CSI, and gates
  on peak>=1 blob via GET /api/blobs. No hardware/Docker/manual IP/manual token.
- notes/hardware-free-runtime.md: full reproduce sequence, the two gotchas
  (two cmd/sim packages; rate=30 not 20), the token path, env/params, and the
  determinism contract.

Verified clean-checkout run at HEAD 5bbe52c: peak_blob_count=2, first blob
@1s, sim --verify PASSED, 0 rejects, ~363 FPS, exit 0. Closes the chain
bf-3zll -> bf-3hji -> bf-51fq -> bf-5cr3.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 01:19:25 -04:00
jedarden
5bbe52ce45 test(bf-51fq): add manual /api/blobs observation harness for runtime blob tracking
bf-51fq requires observing >=1 tracked blob at runtime via /api/blobs
while spaxel-sim streams CSI — reproducing OUTSIDE the e2e harness what
bf-2aqf asserts inside it (AssertBlobObserved).

blob_observation.sh spins up an ephemeral mothership + the canonical
mothership/cmd/sim binary (the only sim with --verify), streams CSI, and
polls GET /api/blobs over the run window tracking peak count + first-blob
time. It cross-checks against sim --verify.

Working params (reproducible): nodes=4 walkers=2 rate=30 duration=25
seed=42. rate=30 (not the default 20) is required — at 20 Hz fusion only
peaks while a walker crosses the DeltaRMS threshold, so a run can land
peak=2 *or* 0 nondeterministically; 30 Hz hits peak=2 every observed run.

Verified at HEAD 231d756: peak_blob_count=2 (== walkers), first_blob_at=1s,
4 nodes online throughout, 0 rejects, ~363 FPS; sim --verify PASSED
("[SIM] PASS: 2 blobs detected for 2 walkers"). go vet clean; mothership
unit packages pass. (Pre-existing e2e package failures at HEAD — rate=20
blob nondeterminism + tokenless SimulateNode under the strict window — are
unrelated to this shell-script deliverable and tracked under the bf-2aqf
harness bead family.)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 00:58:20 -04:00
jedarden
231d7566cc fix(bf-243os): strip Fuse debug instrumentation; complete runtime sim->blob path
The runtime bridge produced zero tracked blobs from spaxel-sim CSI. Four
fixes land together to deliver bf-243os (strip + verify the path):

- main.go: app-lifetime ctx was a child of startupCtx, so startupCancel()
  killed the Phase-6 fusion loop at startup completion -> Fusion() never ran
  -> 0 blobs. Make ctx a child of context.Background().
- sim/verify.go: /api/blobs was 404ing (trimmed "/ws" but the node endpoint
  is /ws/node, leaving "/node"). Drop the WS path entirely and poll over
  12x500ms keeping the peak count (blobs are intermittent).
- sim/main.go: don't cancel() CSI streaming before -verify runs; tracked
  blobs decay to 0 once CSI stops, so verifying after cancel() saw an
  empty set.
- fusion.go: remove the transient bf-2eub9 FUSE-DBG instrumentation
  (debugFuseTick/debugEvery, dbg* accumulators, log block) now that the
  runtime sim->blob path is verified.

Verified: spaxel-sim -verify -> [SIM] PASS: 2 blobs detected for 2 walkers
(peak_blob_count=2 == walkers=2, exit 0); /api/blobs non-empty on 8 of 35
ticks; 0 rejects; fusion+tracker unit tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 20:17:32 -04:00
jedarden
b9f362c748 fix(bf-561zr): split colon-joined LinkID so sim CSI reaches Fuse
CSIFrame.LinkID() joins the two MACs with ':' ("AA:BB:CC:DD:EE:FF:11:22:33:44:55:66"),
but splitLinkID scanned only for '-' and returned nil for every such link. Both fusion
paths (3D gatherFusionLinks at main.go:5446 and 2D selfImprovingLocalizer at main.go:2786)
share splitLinkID, so gatherFusionLinks returned an empty slice -> Fuse([]) -> 0 peaks
-> 0 blobs (the bf-20q9o root cause).

Fix with a fixed-width split: each MAC is exactly 17 chars, so the canonical two-MAC
form is 35 chars with the boundary at index 17 regardless of whether the separator is
':' (actual producer format) or '-' (documented format). The '-' scan stays as a
defensive fallback. This is Option A from bf-20q9o (one function, fixes both paths,
no key-format change).

Adds splitlink_test.go (table-driven producer-contract cases + a gatherFusionLinks
function-level proof that the colon-joined producer LinkID yields >=1 link) and a
transient FUSE-DBG tick log in internal/fusion to confirm Fuse receives links>=1 at
runtime (marked for removal once the sim->blob path is verified end-to-end).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 18:30:53 -04:00
jedarden
56a2793d08 docs(bf-3hji): re-verify spaxel-sim streams CSI — no reject, frames/s=240, 4 nodes online
Fresh re-dispatch against the running strict-window mothership
(SPAXEL_MIGRATION_WINDOW_HOURS=0). Rebuilt sim from repo-root go.work,
ran the exact documented command for 30 s: 4 nodes connected, 0 REJECT,
Average FPS 240.0, /healthz nodes_online:4 with all rows online. All four
bf-3hji acceptance criteria met. Records the exact run command.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 16:01:15 -04:00
jedarden
f281b7f246 docs(bf-3hji): re-verify spaxel-sim streams CSI — connect, no reject, frames/s>0, nodes online
Independently re-ran the exact documented command against a fresh strict-window
mothership (SPAXEL_MIGRATION_WINDOW_HOURS=0, clean mktemp data dir) with both
binaries built from source (repo-root cmd/sim, not the stale in-module sim):

  /tmp/spaxel-sim --mothership ws://localhost:8080/ws/node \
    --nodes 4 --walkers 1 --rate 20 --duration 30 --ble --seed 42

All 4 acceptance criteria hold: 4 nodes connect (4x [SIM] connected + 4x
mothership [INFO] Node connected, sim exits 0); 0 REJECT/401/403/invalid_token in
either log; frames/s=240.0 (Frames sent 7200 = 12 node-pairs x 20 Hz); /api/nodes
returns 4 rows all online (zero went_offline_at) at real perimeter positions,
/healthz nodes_online:4. Exact command + evidence recorded in the bead body
(comment) and PROGRESS.md.

go vet ./... clean; all non-e2e unit tests pass. blobs=0 is correctly out of scope
(next chain link, fusion SetNodePosition wiring bf-4q5w / IO-6 hard-gate); the
pre-existing tests/e2e sim-binary-trap RED state is also out of scope (bf-4q5w).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 15:55:50 -04:00
jedarden
98a9b7dd9f docs(bf-vuzie): verify /api/nodes lists the 4 sim nodes online (final chain gate)
Independently re-verified the last acceptance gate for the chain and parent
bf-3hji. Against a fresh strict-window mothership (SPAXEL_MIGRATION_WINDOW_HOURS=0,
clean mktemp data dir) with spaxel-sim --nodes 4 --walkers 1 --rate 20
--duration 30 --ble --seed 42 (both binaries built from source):

- GET /api/nodes mid-run returns 4 rows (4 sim MACs), each with a zero
  went_offline_at (online); /healthz reports nodes_online:4.
- All 4 last_seen_at within ~9s of the probe-time now (<30s => online per the
  e2e GetNodes definition, e2e_test.go:241).
- Provisioning path live: 4x POST /api/provision -> 200, 4x Node connected,
  0 reject/invalid_token/401/403 in either log, Frames sent: 7200.

go vet ./... clean; go build ./... clean; all non-e2e unit tests pass.
mothership/tests/e2e remains RED at HEAD from a pre-existing sim-binary
mismatch (documented in PROGRESS.md), not introduced by this bead.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 15:45:40 -04:00
jedarden
2c0119f8d5 docs(bf-4ads8): re-verify spaxel-sim streams CSI (frames/s > 0, 4 nodes, no REJECT)
Independently re-ran the documented command against a fresh bf-3zll healthy
mothership: 4 nodes connect (mothership nodes_online=4), 0 REJECT in both logs,
Frames sent 7200 / Average FPS 240.0 over 30s. Reproduces bf-3hji's result; the
exact command and evidence are recorded under the existing "Hardware-Free
Simulator → CSI Streaming" PROGRESS.md section. blobs=0 stays out of scope
(next chain link, bf-4q5w / IO-6).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 15:16:30 -04:00
jedarden
7277ff0029 docs(bf-4iewr): verify REJECT/token umbrella — strict-window + negative control
Records the bf-4iewr verification of the hardware-free mothership's token
path. The fixes are already at HEAD (bf-1o7qi header bridge + bf-4mle6 real
token provisioning); this commit documents the definitive runtime proof:

- Positive (default --provision, SPAXEL_MIGRATION_WINDOW_HOURS=0): 2 nodes
  provision real HMAC tokens, connect, stream CSI at ~40 fps; ZERO
  reject/invalid_token/401/403 in mothership stderr.
- Negative control (--token bogus): the strict-window mothership rejects the
  bogus-token node ("invalid token") and the sim exits on reject — proving the
  validator is live and enforcing, so legitimate nodes connect on real tokens,
  not because auth was disabled.

Both acceptance criteria of bf-4iewr hold. Doc-only; go vet ./... clean and
go test ./internal/... green (e2e package is the unrelated bf-4q5w blob gate,
out of scope here).

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-4iewr
2026-07-07 15:16:30 -04:00
jedarden
285d9eb189 feat(sim): provision real per-node HMAC token by default (bf-4mle6)
spaxel-sim now mints a real per-node HMAC token via POST /api/provision
(--provision, default true), presented as X-Spaxel-Token and validated
server-side regardless of the migration window. This makes the
hardware-free build's acceptance durable and window-independent: the sim
connects and streams identically under SPAXEL_MIGRATION_WINDOW_HOURS=0,
24, or larger.

- cmd/sim/main.go: --provision (default) / --provision=false (legacy
  window-dependent dummy) / --token <t> (verbatim override);
  resolveTokens() + provisionNodeToken().
- PROGRESS.md: document the window-independent auth policy with the exact
  human-run command (window=0 + --provision), and supersede the bf-2hdbg
  "24h-window mask, not a fix" conclusion (RESOLVED by the bf-1o7qi
  header bridge + this provisioning change; historical text preserved).

Verified: go build/vet ./cmd/sim clean. Window-independent acceptance is
proven by the e2e harness under SPAXEL_MIGRATION_WINDOW_HOURS=0
(bf-qzrmq, 250056c) and the hello.Token validation bridge (bf-1o7qi,
9c38cbb) — both already committed.

The bf-4q5w IO-6 hard-gate harness code in
mothership/tests/e2e/e2e_test.go is intentionally NOT included here; it
belongs to the still-open bf-4q5w.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-42cao
2026-07-07 15:16:30 -04:00
jedarden
315dc5a585 docs(bf-f0jee): checkpoint close — IO-6 tracking verified, bead closed on redispatch
Re-verified on redispatch that the bf-f0jee deliverable was already complete
at HEAD (commit c534144): bf-4q5w comment 131 names the five IO-6 functions
and claims them for bf-4q5w; git show 250056c confirms the IO-6 code is
excluded from the bf-qzrmq commit (+17 lines only); bf-4mle6 stays ungated.
Posted closing comment 133 to bf-f0jee and closed it (Completed).

Only .beads/issues.jsonl is staged — the deliberately-uncommitted IO-6
e2e_test.go code (+217 lines) is left in the working tree per the recorded
fate decision (fold into the bf-4q5w Engine.SetNodePosition fix commit).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 12:48:27 -04:00
jedarden
c534144419 docs(bf-4q5w): claim IO-6 hard-gate test code; record working-tree fate (bf-f0jee)
Checkpoint the bead-tracker JSONL after posting comment 131 to bf-4q5w.
The comment explicitly claims the five IO-6 functions
(TestIO6HardGate_WalkerProducesTrackedBlob, CaptureIO6Diagnostics,
GetNodeRecords, GetStatus, io6RunMaxima in mothership/tests/e2e/e2e_test.go)
for bf-4q5w, records the working-tree fate decision (leave uncommitted;
fold into the bf-4q5w Engine.SetNodePosition fix commit so the deliberately
RED gate goes green in the same commit), and cites the git-show(250056c)
proof that the IO-6 code is excluded from the bf-qzrmq commit. Satisfies
bf-qzrmq acceptance criterion #2; bf-4mle6 remains ungated by IO-6.

Only .beads/issues.jsonl is staged — the IO-6 e2e_test.go code is
deliberately left uncommitted per the recorded fate decision.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 12:40:34 -04:00
jedarden
250056cb56 test(e2e): run harness under SPAXEL_MIGRATION_WINDOW_HOURS=0 (bf-qzrmq)
Isolate the window-independence harness change in TestHarness.Start: set
SPAXEL_MIGRATION_WINDOW_HOURS=0 (the strict window, not the open 24h
default) so the e2e no-reject path actually proves token-based admission
regardless of the migration window, instead of silently passing on the
implicit/explicit 24h crutch. A comment documents the bf-4mle6 / bf-qzrmq
policy and why spaxel-sim nodes connect on real HMAC tokens rather than
the window.

This is the commit-hygiene half of bf-qzrmq: it stages ONLY the env-var +
comment (concern A). The unrelated IO-6 hard-gate test code
(TestIO6HardGate_WalkerProducesTrackedBlob, CaptureIO6Diagnostics,
GetNodeRecords, GetStatus, io6RunMaxima + the "strings" import — concern B,
belonging to bf-4q5w) is deliberately left uncommitted in the working tree.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 12:28:10 -04:00
jedarden
9c38cbb9ef fix(bf-1o7qi): bridge X-Spaxel-Token header into hello.Token for validation
The token validator reads only hello.Token (server.go:537), but spaxel-sim and
firmware following the plan.md auth contract present their token solely as the
X-Spaxel-Token upgrade-request header, omitting it from the hello JSON body. The
WS upgrade handler never read that header, so hello.Token was always "" and
every such node fell through to the migration-window branch — the dead
token-supply path.

Fix (approach A, server-side): in HandleNodeWS, after parsing hello and before
the validation check, copy X-Spaxel-Token into hello.Token when the body omits
it. The body token wins when both are present, so existing body-token clients
are unaffected. This does not weaken the validator: an empty/invalid token is
still rejected once the migration window closes (SPAXEL_MIGRATION_WINDOW_HOURS=0
or deadline past).

Test TestTokenValidation_HeaderTokenBridged wires the real
provisioning.Server.ValidateToken (HMAC-SHA256(installSecret, mac)) and mints the
token through the real HandleProvision handler, proving the bridge end-to-end:
valid header token accepted + paired with window closed; empty/invalid rejected;
body-token-wins regression.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 12:28:10 -04:00
jedarden
5b0f4c179c docs(bf-34lwt): close — root-cause re-verified at HEAD 402bb7b
Independently re-verified all 6 REJECT/token root-cause points against the
current source (zero line drift). Posted comment 121 on bf-4iewr with
file:line citations and the two required explicit statements:
  - "no reject" is a 24h-window MASK, not a real fix
  - the token-supply path is currently DEAD (header ignored, hello.Token empty)

go vet ./internal/ingestion/ clean. bf-34lwt closed; the remaining bf-4iewr
children should target making the token contract real (point 4) rather than
re-opening the migration window.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 12:28:10 -04:00
jedarden
1679f00640 docs(bf-34lwt): re-dispatch verification — 6 facts re-confirmed at HEAD 9df77e3, close
Re-dispatch of an already-complete investigation. The authoritative write-up
(docs/notes/token-reject-root-cause.md, 5c6a692) and bf-4iewr comments [110]/[111]
are accurate; re-verified all six citations against current source with zero line
drift and posted verification+correction comment [118] on bf-4iewr (corrects
comment [115]'s scenario.go claim — both sim copies set the X-Spaxel-Token header).
Closed bf-34lwt; checkpointing the comment+close to JSONL so it stops re-dispatching.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 12:28:10 -04:00
jedarden
4b54789302 docs(bf-4th1x): close bead — synthesis comment verified on bf-4iewr
Re-dispatch of a run that posted the synthesized root-cause comment [115]
on the grandparent bf-4iewr but failed at br close (worker_sessions
claimed_at naive-timestamp bug). Re-verified all six file:line citations
against current source, posted closing summary comment [117], repaired the
two naive worker_sessions rows, and closed bf-4th1x.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 07:42:22 -04:00
jedarden
005ae09ae7 docs(bf-4th1x): post synthesized REJECT/token root-cause comment on bf-4iewr
Synthesize the three bf-34lwt children (bf-5ig3e, bf-29wyl, bf-2hdbg) into the
definitive root-cause finding on the grandparent bf-4iewr, with file:line
citations for all six confirmed points. States explicitly that current
'no reject' is a 24h migration-window MASK (not a real fix) and that the
token-supply path is currently dead (X-Spaxel-Token header ignored,
hello.Token always empty).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 07:35:17 -04:00
jedarden
8116dc8fc2 docs(bf-2hdbg): migration window (not tokens) accepts tokenless sim nodes
Third link of the bf-34lwt split. Pins down that the bf-3hji 'no REJECT,
sim nodes accepted' state is a side effect of the default 24h migration
window a fresh boot opens — not a token/auth fix. Tokens never enter the
admission decision: sim nodes are tokenless where checked (hello.Token
body field is empty; the X-Spaxel-Token header they set is unread per
bf-29wyl). The sole acceptor is the migration deadline.

Verified against current source:
- config.go:139 MigrationWindowHours defaults to 24 (config.go:43 field
  comment '0 = disabled'); main.go:4495-4497 installs deadline=now+24h
  only when >0, so a fresh boot opens the window.
- server.go:510-518 acceptance branch gates on server.go:515
  !deadline.IsZero() && time.Now().Before(deadline) — tokenOK is
  irrelevant here; tokenless node admitted as Unpaired.
- server.go:519-526 reject branch (else of 515) hits server.go:525
  sendReject(conn,"invalid_token") + conn.Close(); sendReject writes
  {"type":"reject","reason":"invalid_token"} (server.go:841-845).

Conclusion: 'no reject' is a 24h-window MASK. REJECT returns when
SPAXEL_MIGRATION_WINDOW_HOURS=0 (deadline never set -> IsZero() ->
reject) or uptime > 24h (Before(deadline) false -> reject). Real fix is
on the token/supply side, not the window.

Carries the adjacent bf-3hji section it builds on ('the bf-3hji no-reject
finding above'), also uncommitted docs. No Go code touched; build + unit
tests + go vet clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 07:24:19 -04:00
jedarden
4dcc06af2b docs(bf-29wyl): confirm token-supply path dead — header-only, unread
Verify the sim/header-side half of the tokenless-sim REJECT root cause
against current source (second link of the bf-34lwt split). Three facts
re-checked against current source:

- Sim supplies its token ONLY as the X-Spaxel-Token HTTP header on the
  WS dial — cmd/sim/main.go:313, mothership/cmd/sim/main.go:633,
  mothership/cmd/sim/scenario.go:318 (all three connect paths).
- The hello JSON map the sim sends has NO token field —
  cmd/sim/main.go:348-359, mothership/cmd/sim/main.go:652-665,
  mothership/cmd/sim/scenario.go:331-342. Only "--token" hits are CLI
  flag defs, never a JSON body key.
- The mothership NEVER reads that header on the WS path —
  grep 'X-Spaxel-Token' mothership/ excluding _test.go and cmd/sim/
  returns zero hits. HandleNodeWS (server.go:455-469) passes r straight
  to Upgrade; the only Header ref on the path is the outbound
  w.Header().Set response header.

Conclusion: header ignored -> hello.Token (message.go:22) empty ->
tokenOK (server.go:513) always false. The "have the sim supply valid
tokens" option is currently DEAD. Companion to bf-5ig3e (validator half);
both roll up into bf-34lwt.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-29wyl
2026-07-07 06:55:56 -04:00
jedarden
3c5b79b791 docs(bf-5ig3e): confirm validator wired unconditionally + hello.Token-only check
Verify the validator-side half of the tokenless-sim REJECT root cause
against current source (first link of the bf-34lwt split):

- mothership/cmd/mothership/main.go:4494 — SetTokenValidator(...) is a
  bare statement with no env/build guard; provSrv is created
  unconditionally at :4492. 'validator configured?' is always YES.
- mothership/internal/ingestion/server.go:513 — tokenOK :=
  hello.Token != "" && validator(hello.MAC, hello.Token) depends
  solely on the deserialized hello JSON body field (message.go:22),
  not any HTTP header. No X-Spaxel-Token read exists on this path.

Conclusion: validator always configured; accept/reject hinges entirely
on hello.Token.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 06:38:33 -04:00
jedarden
5c6a6924c1 docs(bf-34lwt): root-cause tokenless-sim REJECT in WS hello path
Definitive root-cause for the bf-4iewr split's first child. Confirms
against current source (with file:line citations) that:

- The token validator is wired UNCONDITIONALLY (main.go:4494)
- It reads hello.Token only (server.go:513); the sim never populates it
- The sim sends its token only as the X-Spaxel-Token header (cmd/sim
  main.go:633/scenario.go:318), and the mothership never reads that
  header (HandleNodeWS upgrades unconditionally at server.go:469) -> the
  token-supply path is currently DEAD
- The default 24h migration window (config.go:139) is the only thing
  accepting sim nodes -> current 'no reject' is a window MASK, not a fix
- REJECT fires only when the window closes (SPAXEL_MIGRATION_WINDOW_HOURS=0
  or uptime > 24h)

Corroborated by the e2e harness comment (tests/e2e/e2e_test.go:86-94)
which pins the window open and documents exactly this.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 06:19:29 -04:00
jedarden
8014ad1768 docs(bf-3zll): document hardware-free mothership build + boot to /healthz
Verified the mothership builds from a clean checkout (mothership/ module root,
plain 'go build', no -tags=embed needed locally) and boots to /healthz with
no ESP32 hardware, reaching status=ok in ~230ms with no panic/crash in stderr.
Records the exact commands for the next child in the bf-40hc chain; canonical
path is TestHarness.Start in mothership/tests/e2e/e2e_test.go.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 00:37:30 -04:00
jedarden
3b00ccfbd4 test(bf-2aqf): hard-assert >=1 blob in TestFullE2EIntegration
Replace the minimum-count dodge in TestFullE2EIntegration with two hard
assertions: AssertBlobObserved on the WatchDashboardWS blobCounts (captured
concurrently during the run via a goroutine alongside AssertDuringRun) and
AssertDetectionEventsObserved on the /api/events "detection" result. The
"we verify the API responds correctly rather than asserting a minimum
count" dodge comment is gone. 4-node/2-walker/30s config and AssertDuringRun
kept. Expected RED until upstream bf-4q5w lands — deliberately not weakened.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 20:41:29 -04:00
jedarden
a8d0759c21 test(bf-16c1): assert >=1 detection event in TestDetectionEvents
Replace the nil-only events check with a hard assertion that
len(events.Events) >= 1, using the AssertDetectionEventsObserved
helper added in bf-2330. An empty detection-event list now fails
with an actionable message naming the count observed and the likely
upstream cause (fusion/tracking produced no blobs). The 4-node /
2-walker / 15s RunSimulator config and timeout handling are
preserved. This is intentionally strict and expected to stay RED
until upstream bf-4q5w (fusion SetNodePosition wiring) lands.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 20:32:19 -04:00
jedarden
06460fb197 test(bf-2330): add hard-fail blob/detection assertion primitives to e2e harness
AssertDuringRun now returns a descriptive error (elapsed time + counts
observed) when the fusion pipeline produces zero blobs AND zero detection
events across the whole run window, instead of silently returning nil.
The health==ok and nodes-online==expected assertions are left intact.

Add reusable AssertBlobObserved(blobCounts) and
AssertDetectionEventsObserved(*EventsResponse) helpers plus a GetBlobCount
poller for TestDetectionEvents / TestFullE2EIntegration / the IO-6
hard-gate test (children of bf-5jeo) to reuse. Table-driven unit tests
cover pass/fail/empty/nil cases and the non-nil failure contract.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 20:22:36 -04:00
jedarden
7e0bf66ddd fix(bf-56uk): make blob-identity TypeScript verification compile-clean
Add the TypeScript tooling and compile-time check that verifies the blob
identity fields (personName/assignedColor/identityResolved + deprecated
aliases) declared on the Blob interface are TypeScript-compliant:

  - dashboard/tsconfig.json: strict tsc config, noEmit, includes types/**
  - dashboard/package.json: add `typescript` devDep and `npm run typecheck`
  - dashboard/types/blob-identity.check.ts: pure compile-time assertions

The check file is the TypeScript mirror of the Go identity-field
serialization suites (bf-5151/bf-2ibc/bf-5v3q/bf-3wkz/bf-4qto): a zero-value
blob (no identity set), a resolved-identity blob (camelCase keys the Go
dashboard projection emits), and the identityResolved tri-state must all
satisfy the Blob interface with no missing-field or type-mismatch errors.

Fix the one compilation error the check surfaced: the HasField helper used
`NonNullable<T[K]> extends NonNullable<V>`, which TypeScript does not
reliably evaluate for an indexed type parameter inside a nested conditional —
it produced a spurious TS2344 `false` for the optional `boolean`
identityResolved field. Rewrite it as `V extends T[K]` (concrete V on the
left, indexed T[K] on the right), which resolves correctly in every case and
still catches missing keys and type mismatches.

Verified: `npm run typecheck` exits 0; `go vet`/`go build`/`go test ./...`
on mothership all pass (no Go touched).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 19:32:31 -04:00
jedarden
ca7a0f21f3 fix(bf-4qto): propagate canonical identity fields through all blob projections
The three canonical identity fields (PersonName, AssignedColor,
IdentityResolved) had been added to every identity-bearing blob type
(bf-5151/bf-2ibc/bf-5v3q/bf-3wkz) but three projection sites in the
fusion loop were silently dropping them when converting the source
sigproc.TrackedBlob:

  - explainability.BlobSnapshot  (cmd/mothership/main.go)
  - automation.TrackedBlob       (cmd/mothership/main.go)
  - volume.BlobPos               (cmd/mothership/main.go)

The api.Track (tracks.go) and dashboard.blobJSON (hub.go) projections
already propagated them, so this left default-handling inconsistent
across projection contexts. Propagate the three fields from the source
blob at all three sites so the canonical fields flow through every
projection instead of being zeroed.

The fields stay at their Go zero values for existing blobs (identity is
not yet resolved at the signal layer): empty strings and a nil *bool all
carry omitempty, so they serialize as omitted (undefined in JS), which is
the consistent default the acceptance criteria require. The BLE identity
sidecar that actually populates them is a separate follow-up bead.

Also add the companion serialization-consistency suites that were missing
for the four types whose fields landed across the split beads:
tracker.Blob, tracking.Blob, volume.BlobPos, explainability.BlobSnapshot.
Each asserts the three task-required properties: zero-value blobs omit
all three fields (undefined), a resolved identity emits the camelCase
keys matching dashboard/types/spaxel.d.ts, and the *bool tri-state
(nil=omitted, &true=emitted, &false still emits identityResolved:false).

Verified: gofmt clean, go vet ./... clean, go test ./... passes
(incl. the four new suites and the existing api/automation/signal ones).

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-4qto
2026-07-06 19:12:57 -04:00
jedarden
d0ac1b27b0 test(bf-5151): guard canonical identity-field serialization on blob types
Add table-driven serialization tests for the three bf-5151 canonical
identity fields (PersonName/AssignedColor/IdentityResolved) on the
tracked-blob types whose fields were added in a612584 + 01a415d and
propagated through the JSON projections:

  - internal/signal/identity_fields_test.go      -> signal.TrackedBlob
    (the canonical type /api/blobs serializes; api.TrackedBlob aliases it)
  - internal/automation/identity_fields_test.go  -> automation.TrackedBlob
    (Tier-1 #1 type that previously carried NO identity fields)
  - internal/api/tracks_identity_test.go         -> api.Track via the real
    GET /api/tracks handler, asserting propagation from signal.TrackedBlob

Each suite asserts the three task-required properties:
  1. zero-value blobs omit all three fields (omitempty -> undefined in JS),
     satisfying "fields set to undefined for existing blobs";
  2. a resolved identity emits the camelCase keys matching
     dashboard/types/spaxel.d.ts;
  3. the *bool tri-state: nil=omitted, &true=emitted, &false still emits
     "identityResolved":false (resolution attempted, failed).

Verified: gofmt clean, go vet ./... clean, go test ./... passes
(50 packages ok, including the 3 new suites).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 18:44:12 -04:00
jedarden
1871a8a42c docs(bf-1mwy): correct coverage-gate HEAD ref to real ancestor 908adf2
The committed bf-1mwy coverage gate (7f697a3) cited HEAD `37d628e`, a
pre-rebase dangling SHA not in history (the same class of stale-hash error
the doc's own §6 warns about for 1446ccf/248d0e0). Re-verified at the real
code HEAD `908adf2` (confirmed ancestor of HEAD): both inventory greps
return the documented 5 + 15 hits, all 8 in-scope blob types carry the
three canonical identity fields, and gofmt/vet/test are green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 18:24:50 -04:00
jedarden
7f697a388d docs(bf-1mwy): coverage gate proving every blob-creation site accounted for
Re-run the bf-1q3m §1.1 inventory greps at HEAD 37d628e and map every
production blob-creation site (5 primary + 16 projection = 21) to either
"has the 3 canonical identity fields" or "out-of-scope with explicit
reason" in notes/bf-5151-coverage.md. This is the coverage gate closing
bf-5151's "No blob creation code is missed" acceptance criterion.

All 8 in-scope types confirmed carrying PersonName/AssignedColor/
IdentityResolved at zero values: signal.TrackedBlob, tracker.Blob,
tracking.Blob, automation.TrackedBlob, api.Track, dashboard.blobJSON,
explainability.BlobSnapshot, volume.BlobPos. All 15 out-of-scope sites
enumerated by reason (pre-identity peak C1; synthetic replay/sim P8-P10;
anon falldetect input P3; lifecycle events P11/P12; trigger-state
volume.BlobState P13-P16; rendering fallbacks P4-P6; distinct-name
falldetect.BlobSnapshot P7; dead api.BlobPos) -- none silently dropped.

Also notes the task-cited SHAs 1446ccf/248d0e0 are pre-rebase dangling
commits superseded by a612584/01a415d in HEAD.

gofmt/vet/test gate: 7 files carried pre-existing gofmt drift unrelated
to the identity work (verified unmodified by this bead before the format
pass); fixed mechanically with gofmt -w to satisfy the clean-gate
requirement. gofmt -l empty, go vet ./... exit 0, go test ./... all ok.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 18:15:22 -04:00
jedarden
908adf2049 docs(bf-3wkz): record independent re-verification of dashboard identity surface
Second pass (retry) re-confirmed all 8 Go types emit the canonical camelCase trio
(personName/assignedColor/identityResolved), state.js:288-294 surfaces server identity
via Object.assign, websocket.js dead-reckoning cache is identity-free by design.
go vet + go test green. No source change needed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 17:58:29 -04:00
jedarden
730956411c fix(bf-3wkz): align volume.BlobPos identity JSON tags with dashboard camelCase
volume.BlobPos was the sole outlier carrying the canonical identity trio
(personName/assignedColor/identityResolved) under snake_case JSON tags
(person_name/assigned_color/identity_resolved); the other 7 blob-shaped
types and dashboard/types/spaxel.d.ts all use camelCase (bf-5151).

The tags were inert today (volume.BlobPos is consumed in Go by
Store.Evaluate, which returns fired-trigger IDs; it is never
JSON-marshalled to the dashboard), but would leak snake_case keys
inconsistent with the TS interface the moment it is ever surfaced.
Aligning removes the latent drift.

Verified (Tier-4 dashboard track of notes/bf-1q3m-consolidated.md §6):
- spaxel.d.ts Blob interface declares personName/assignedColor/
  identityResolved (+ deprecated personLabel/personColor aliases)
- dashboard.blobJSON, api.Track, explainability.BlobSnapshot already match
- state.js:290 surfaces server identity via Object.assign (no change needed)
- websocket.js:167 dead-reckoning cache is intentionally identity-free
Cross-check documented in notes/bf-3wkz-dashboard-identity-surface.md.
go vet ./... and go test ./... both pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 17:52:34 -04:00
jedarden
29a114c961 feat(bf-5v3q): add identity fields to volume.BlobPos type
Add the three canonical identity fields — PersonName, AssignedColor,
IdentityResolved — to volume.BlobPos (the Tier-1 #3 type gap from the
bf-1q3m/bf-5cgc handoff). BlobPos is the trigger-evaluation blob type
constructed at cmd/mothership/main.go:2326; it carried only PersonID.

Mirrors commit 1446ccf (bf-5151), which added the same three fields to
the four dashboard-facing tracked-blob types — but here the JSON tags
are snake_case (person_name, assigned_color, identity_resolved) matching
the volume package's internal convention, versus 1446ccf's camelCase
keys that mirror the dashboard Blob TS interface.

Per the task scope, the fields are DECLARATIONS ONLY: left at zero values
(empty strings, nil *bool) with omitempty, so they serialize as omitted
(undefined in JS). The production literal at main.go:2326 needs no change
— Go zero values handle it. A separate downstream bead populates them
from identityMatcher.GetMatch(blob.ID); that one-line populate is out of
scope here.

Verified: gofmt clean, go vet ./... clean, go test ./... passes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 17:42:33 -04:00
jedarden
c3f4b0dd4a feat(bf-2ibc): add identity fields to explainability.BlobSnapshot
Add the three canonical identity fields — PersonName, AssignedColor,
IdentityResolved — to explainability.BlobSnapshot (handler.go), the only
tracked-blob view that still lacked them (Tier-1 #2 type gap from the
bf-1q3m/bf-5cgc handoff).

The camelCase JSON keys (personName, assignedColor, identityResolved) and the
*bool tri-state choice mirror exactly how commit 1446ccf added them to
signal.TrackedBlob / tracker.Blob / tracking.Blob / automation.TrackedBlob, and
match the dashboard Blob type in dashboard/types/spaxel.d.ts.

Per scope, the fields are DECLARATIONS ONLY: the production creation literal at
mothership/cmd/mothership/main.go:2206 (and the handler_test.go literals) set
only {ID,X,Y,Z,Confidence}, so the new fields stay at zero values — empty
strings and a nil *bool all carry omitempty and serialize as omitted (undefined
in JS). Populating them from the ble.IdentityMatcher sidecar and folding in the
parallel identityMap (main.go:2216) is a separate downstream bead.

Verified: gofmt clean, go vet ./... clean, go build ./... clean,
go test ./... passes.

Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-2ibc
2026-07-06 17:37:06 -04:00
jedarden
01a415dd5c feat(bf-5151): propagate canonical identity fields to blob JSON projections
Complete the identity-field surface started by 1446ccf (which added
PersonName/AssignedColor/IdentityResolved to the four primary tracked-blob
structs). Add the same three canonical fields to the two struct types that
serialize tracked blobs for external consumers, and propagate them from the
source structs:

  - api.Track (internal/api/tracks.go)        -> /api/tracks JSON; sourced
                                                 from signal.TrackedBlob in
                                                 listTracks
  - dashboard.blobJSON (internal/dashboard/hub.go) -> /ws/dashboard feed;
                                                 sourced from tracking.Blob in
                                                 BroadcastLocUpdate

/api/blobs needs no change: it serializes signal.TrackedBlob directly
(pm.GetTrackedBlobs), which gained the fields in 1446ccf.

Per the task scope the fields are left at their zero values for existing
blobs (empty strings + nil *bool, all omitempty -> undefined in JS). A
follow-up bead wires the BLE identity sidecar to populate them. The
camelCase keys match the dashboard Blob interface in
dashboard/types/spaxel.d.ts.

The Tier-1 leak-fix targets from bf-1q3m (explainability.BlobSnapshot,
volume.BlobPos) are intentionally untouched: their identity flows via a
separate mechanism (the identityMap BLEMatch side-channel / PersonID), and
adding blob-struct identity fields there is step (a) of a cohesive
add+populate+fold fix that belongs to the follow-up bead (bf-1q3m §6),
not this fields-as-undefined bead.

Verified: gofmt clean, go vet ./... clean, go test ./... passes (api,
dashboard, signal, tracker, tracking, automation re-run with -count=1).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 17:37:06 -04:00
jedarden
a612584841 feat(bf-5151): add canonical identity fields to blob creation types
Add the three new canonical identity fields — PersonName, AssignedColor,
IdentityResolved — to the four primary tracked-blob struct types that are
constructed at the blob-creation sites catalogued in bf-1q3m:

  - signal.TrackedBlob   (internal/signal/processor.go)   — canonical type;
                                                            api.TrackedBlob is
                                                            a type alias of it
  - tracker.Blob (3D)    (internal/tracker/tracker.go)
  - tracking.Blob (2D)   (internal/tracking/tracker.go)
  - automation.TrackedBlob (internal/automation/engine.go)

The camelCase JSON keys (personName, assignedColor, identityResolved) match
the dashboard Blob interface in dashboard/types/spaxel.d.ts, which viz3d.js
reads directly. The legacy snake_case PersonLabel/PersonColor fields are
retained as deprecated aliases for backward compatibility.

Per the task, the fields are left at their zero values for existing blobs:
empty strings and a nil *bool all carry omitempty, so they serialize as
omitted (undefined in JS). A follow-up bead populates them from the BLE
identity sidecar. IdentityResolved is *bool to faithfully represent the
tri-state semantics (nil=unattempted, &true=resolved, &false=failed).

fusion.Blob is intentionally untouched: it is the pre-identity Fresnel peak
type ({X,Y,Z,Confidence} only) and is out of scope for identity per bf-1q3m.

Verified: gofmt clean, go vet ./... clean, go test ./... passes; marshaling
check confirms zero-value blobs omit the fields (undefined) while set blobs
emit the camelCase keys.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 17:37:06 -04:00
jedarden
a1e76e0c8d docs(bf-4bhd): re-verify blob-creation inventory current at HEAD 63f0c87
bf-4bhd ("Find all blob creation code paths") was auto-re-dispatched after
the task was already fully delivered by the closed chain
(bf-1q3m -> bf-1m2x -> bf-5cgc). This re-dispatch confirms currency rather
than re-doing discovery (the documented anti-pattern).

Empirical greps at HEAD 63f0c87 match the consolidated report exactly:
5 primary literals, 15 non-test projection literals, 15 blob types
(14 structs + 1 alias), 2 JS production sites. Zero Go/JS/firmware source
drift across f144aad..HEAD (every commit since is docs/.beads only).

New finding: SHA provenance correction. The prior notes cite unreachable
dangling SHAs (1a26c12 is actually bf-1bmg, not bf-1q3m; 0ae0c03 is an
amended-away copy of the bf-1q3m commit, real one is f144aad) because
history was rewritten after those notes were written. Inventory is
unaffected; mapped to real reachable SHAs so a future agent doesn't trust
dead citations.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-06 16:50:22 -04:00