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>
This commit is contained in:
parent
58faa9ac4a
commit
5d3f3180cc
2 changed files with 294 additions and 0 deletions
|
|
@ -265,6 +265,35 @@ Closes bf-1yr1w criterion 3: the `--ble` matcher recipe is now recorded here, no
|
|||
the fixture note. The matcher → `/api/blobs` identity capstone is **bf-2m534**
|
||||
(`scripts/run-sim-identity.sh`), which depends on this PASS — keep it green.
|
||||
|
||||
### Identity capstone PASS — bf-gdfwx (live `/api/blobs`, 2026-07-08)
|
||||
|
||||
`scripts/run-sim-identity.sh` is the THIRD and final link of the identity chain
|
||||
(matcher-active → match-found → written-onto-served-blob → observable-at-`/api/blobs`).
|
||||
It drives the same hardware-free runtime with `--ble`, seeds the BLE registry with a
|
||||
registered person BEFORE the sim starts (the fix for bf-gdfwx's two prior failures —
|
||||
the early CSI blob window is narrow at walkers=1), then asserts a live `/api/blobs` blob
|
||||
carries non-empty canonical identity.
|
||||
|
||||
Run (reproduce): `./scripts/run-sim-identity.sh` — exit 0. Flags/sequence:
|
||||
`nodes=4 walkers=1 rate=30 duration=25s seed=42 ble=on SIM_PERSON_NAME=Alice
|
||||
SIM_WALKER_MAC=AA:BB:CC:DD:EE:00 SIM_PORT=8088`, fixture applied pre-sim, 0.5s poll cadence.
|
||||
|
||||
Live `/api/blobs` blob (the served, canonical-identity-bearing blob):
|
||||
```json
|
||||
{"ID":7,"X":2.5,"Y":2.3,"Z":1.9,"Weight":0.6923,
|
||||
"person_id":"6960f453-c765-4018-a180-9fbeddfc1b3a","person_label":"Alice",
|
||||
"person_color":"#ec4899","identity_confidence":0.6000597064722052,"identity_source":"ble",
|
||||
"personName":"Alice","assignedColor":"#ec4899","identityResolved":true}
|
||||
```
|
||||
Corroboration from the runtime matcher surface (`GET /api/ble/matches`):
|
||||
```json
|
||||
[{"blob_id":7,"person_name":"Alice","device_addr":"AA:BB:CC:DD:EE:00",
|
||||
"confidence":0.6000597064722052,"is_ble_only":false}]
|
||||
```
|
||||
Satisfies bf-gdfwx acceptance: canonical identity populated **and** observed from the
|
||||
live REST response (`personName="Alice"`, `assignedColor="#ec4899"`,
|
||||
`identityResolved=true`), no panic, evidence recorded here (reused, not re-derived).
|
||||
|
||||
---
|
||||
|
||||
## For later beads (bf-f841 and beyond)
|
||||
|
|
|
|||
265
scripts/run-sim-identity.sh
Executable file
265
scripts/run-sim-identity.sh
Executable file
|
|
@ -0,0 +1,265 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-sim-identity.sh — hardware-free reproduce of NON-EMPTY blob identity (bf-2m534 / bf-gdfwx).
|
||||
#
|
||||
# Sibling of run-sim-local.sh (the canonical bf-40hc "a blob exists" harness) and
|
||||
# the capstone of the bf-5h1t identity chain. It drives the SAME hardware-free
|
||||
# runtime with --ble enabled, seeds the BLE registry with a registered person
|
||||
# BEFORE the sim starts, then asserts that GET /api/blobs serves a tracked blob
|
||||
# whose canonical identity field (personName / assignedColor / identityResolved)
|
||||
# is NON-EMPTY — observed from the live REST surface, with no panic.
|
||||
#
|
||||
# This is the THIRD and final link of the chain:
|
||||
# matcher-active -> match-found -> written-onto-served-blob -> observable-at-/api/blobs
|
||||
# The wire fix (Stage 2b write-back) landed in bf-21v71; this script supplies the
|
||||
# fixture-side preconditions the diagnosis (notes/blob-identity-diagnosis.md)
|
||||
# named as required for a match to fire: --ble advertisements + a registered
|
||||
# person. (The third precondition — the sim's `rssi` vs parser's `rssi_dbm`
|
||||
# field-name mismatch — is fixed at mothership/cmd/sim/main.go so RSSI is no
|
||||
# longer read as 0 and triangulation does not collapse.)
|
||||
#
|
||||
# Ordering (the fix for bf-gdfwx's two prior failures): apply the fixture
|
||||
# BEFORE starting the sim, exactly as run-sim-ble-match.sh (bf-1yr1w, proven
|
||||
# PASS) does. With walkers=1 the CSI blob only lives in the early window
|
||||
# (real t~1-6s; the single walker drifts to a low-signal corner after that),
|
||||
# so the matcher must have the person binding already in place when the first
|
||||
# --ble adv + early blob coincide. Registering the fixture after the sim start
|
||||
# (the prior shape) delayed the poll loop to ~real t=4s and the matcher never
|
||||
# got person+RSSI+nearby-blob together -> empty /api/blobs identity. Polling is
|
||||
# also run at 0.5s cadence (not 1s) to catch that narrow early blob window.
|
||||
#
|
||||
# No hardware, no Docker, no manual IP, no manual token. Reuses the build +
|
||||
# health + token path proven by run-sim-local.sh and the fixture from
|
||||
# run-sim-ble-match.sh.
|
||||
#
|
||||
# Usage: ./scripts/run-sim-identity.sh
|
||||
# Env: SIM_NODES, SIM_WALKERS, SIM_RATE, SIM_DURATION, SIM_SEED, SIM_PORT,
|
||||
# SIM_SPACE, SIM_WALKER_MAC, SIM_PERSON_NAME, SIM_PERSON_COLOR,
|
||||
# SPAXEL_MOTHERSHIP_BIN, SPAXEL_SIM_BIN
|
||||
# Exit: 0 if a live /api/blobs blob carries non-empty canonical identity, 1 otherwise.
|
||||
#
|
||||
# Requires: go, curl, jq.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MS="${SPAXEL_MOTHERSHIP_BIN:-/tmp/spaxel-mothership}"
|
||||
SIM="${SPAXEL_SIM_BIN:-/tmp/spaxel-sim}"
|
||||
PORT="${SIM_PORT:-8088}"
|
||||
|
||||
# walkers=1: one registered person (Alice) -> one identity-bearing blob is the
|
||||
# cleanest evidence. rate=30 is REQUIRED for deterministic blob emission
|
||||
# (hardware-free-runtime.md gotcha #2). seed=42 pins walker-0 to advertise
|
||||
# AA:BB:CC:DD:EE:00 (cmd/sim/main.go: fmt AA:BB:CC:DD:EE:%02X, IDs from 0).
|
||||
SIM_NODES="${SIM_NODES:-4}"
|
||||
SIM_WALKERS="${SIM_WALKERS:-1}"
|
||||
SIM_RATE="${SIM_RATE:-30}"
|
||||
SIM_DURATION="${SIM_DURATION:-25}"
|
||||
SIM_SEED="${SIM_SEED:-42}"
|
||||
SIM_SPACE="${SIM_SPACE:-5x5x2.5}"
|
||||
|
||||
WALKER_MAC="${SIM_WALKER_MAC:-AA:BB:CC:DD:EE:00}"
|
||||
PERSON_NAME="${SIM_PERSON_NAME:-Alice}"
|
||||
PERSON_COLOR="${SIM_PERSON_COLOR:-#4488ff}"
|
||||
|
||||
log() { printf '[run-sim-id] %s\n' "$*"; }
|
||||
die() { printf '[run-sim-id] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
for dep in go curl jq; do
|
||||
command -v "$dep" >/dev/null 2>&1 || die "missing required dependency: $dep"
|
||||
done
|
||||
|
||||
# --- Build both binaries from source (reuse if present; force sim has --ble) ----
|
||||
# Same canonical-source rule as run-sim-local.sh: build ONLY mothership/cmd/sim
|
||||
# (it has --ble/--verify); never the repo-root cmd/sim/ (older, lacks them).
|
||||
log "building canonical binaries from source (reuse if present)..."
|
||||
need_build=0
|
||||
[ -x "$MS" ] || need_build=1
|
||||
[ -x "$SIM" ] || need_build=1
|
||||
if [ -x "$SIM" ] && ! "$SIM" --help 2>&1 | grep -q -- '--ble'; then
|
||||
log "existing $SIM lacks --ble (stale/standalone build); rebuilding..."
|
||||
rm -f "$SIM"; need_build=1
|
||||
fi
|
||||
if [ "$need_build" -eq 1 ]; then
|
||||
( cd "$ROOT/mothership" || exit 1
|
||||
go build -o "$MS" ./cmd/mothership || die "building mothership"
|
||||
go build -o "$SIM" ./cmd/sim || die "building sim"
|
||||
) || exit 1
|
||||
fi
|
||||
|
||||
DATA_DIR=$(mktemp -d -t spaxel-runsimid-data-XXXXXX)
|
||||
MS_PID=""
|
||||
SIM_PID=""
|
||||
|
||||
cleanup() {
|
||||
[ -n "$SIM_PID" ] && kill "$SIM_PID" 2>/dev/null || true
|
||||
[ -n "$MS_PID" ] && kill -INT "$MS_PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
[ -n "$MS_PID" ] && kill -9 "$MS_PID" 2>/dev/null || true
|
||||
wait 2>/dev/null || true
|
||||
rm -rf "$DATA_DIR"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
log "mothership: $MS"
|
||||
log "sim: $SIM"
|
||||
log "params: nodes=$SIM_NODES walkers=$SIM_WALKERS rate=$SIM_RATE duration=${SIM_DURATION}s seed=$SIM_SEED space=$SIM_SPACE port=$PORT ble=on"
|
||||
log "identity fixture: $WALKER_MAC -> person \"$PERSON_NAME\" ($PERSON_COLOR)"
|
||||
|
||||
# --- Start mothership on an ephemeral data dir (identical env to run-sim-ble-match) --
|
||||
# SPAXEL_LOG_LEVEL=info surfaces matcher/fleet INFO lines for evidence capture.
|
||||
SPAXEL_BIND_ADDR="127.0.0.1:$PORT" \
|
||||
SPAXEL_DATA_DIR="$DATA_DIR" \
|
||||
SPAXEL_LOG_LEVEL=info \
|
||||
SPAXEL_MDNS_ENABLED=false \
|
||||
TZ=UTC \
|
||||
"$MS" > "$DATA_DIR/mothership.log" 2>&1 &
|
||||
MS_PID=$!
|
||||
|
||||
ok=""
|
||||
for _ in $(seq 1 50); do
|
||||
if resp=$(curl -s --max-time 2 "http://localhost:$PORT/healthz" 2>/dev/null) && \
|
||||
[ "$(printf '%s' "$resp" | jq -r '.status // empty' 2>/dev/null)" = "ok" ]; then
|
||||
ok=1; break
|
||||
fi
|
||||
sleep 0.3
|
||||
done
|
||||
[ -n "$ok" ] || { log "mothership never became healthy"; tail -30 "$DATA_DIR/mothership.log"; exit 1; }
|
||||
log "mothership healthy"
|
||||
|
||||
# --- Apply the fixture BEFORE starting the sim (bf-gdfwx fix; mirrors ----------
|
||||
# run-sim-ble-match.sh's proven seed_fixture). Register the person + bind the
|
||||
# sim's advertised device addr while the mothership is up but the sim is not yet
|
||||
# streaming, so the device is bound standalone and the binding is in place when
|
||||
# the first --ble adv arrives.
|
||||
seed_fixture() {
|
||||
curl -s -X POST "http://localhost:$PORT/api/ble/devices/preregister" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"mac\":\"$WALKER_MAC\",\"label\":\"$PERSON_NAME\"}" >/dev/null
|
||||
person_id=$(curl -s -X POST "http://localhost:$PORT/api/people" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"name\":\"$PERSON_NAME\",\"color\":\"$PERSON_COLOR\"}" \
|
||||
| jq -r '.id // empty' 2>/dev/null)
|
||||
[ -n "$person_id" ] || die "could not create person \"$PERSON_NAME\" via /api/people"
|
||||
curl -s -X PUT "http://localhost:$PORT/api/ble/devices/$WALKER_MAC" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"person_id\":\"$person_id\",\"label\":\"$PERSON_NAME\"}" >/dev/null
|
||||
SEED_PERSON_ID="$person_id"
|
||||
|
||||
# Sanity: confirm the binding took (reuses the child-1 gate).
|
||||
rec=$(curl -s --max-time 2 "http://localhost:$PORT/api/ble/devices?registered=true" \
|
||||
| jq -c --arg mac "$WALKER_MAC" '[.devices[]|select(.mac==$mac)][0]' 2>/dev/null)
|
||||
pid=$(printf '%s' "$rec" | jq -r '.person_id // empty' 2>/dev/null)
|
||||
[ -n "$pid" ] || { log "fixture FAILED: $WALKER_MAC not bound (child-1 regression)"; exit 1; }
|
||||
}
|
||||
|
||||
log "applying fixture (register \"$PERSON_NAME\" + bind $WALKER_MAC) BEFORE sim..."
|
||||
SEED_PERSON_ID=""
|
||||
seed_fixture
|
||||
log "fixture bound: $WALKER_MAC -> \"$PERSON_NAME\" (id=$SEED_PERSON_ID)"
|
||||
|
||||
# --- Start sim WITH --ble (the fixture precondition run-sim-local.sh omits) ------
|
||||
"$SIM" \
|
||||
--mothership "ws://localhost:$PORT/ws/node" \
|
||||
--nodes "$SIM_NODES" --walkers "$SIM_WALKERS" --rate "$SIM_RATE" \
|
||||
--space "$SIM_SPACE" \
|
||||
--duration "$SIM_DURATION" --seed "$SIM_SEED" --ble \
|
||||
> "$DATA_DIR/sim.log" 2>&1 &
|
||||
SIM_PID=$!
|
||||
log "sim started (pid $SIM_PID, --ble enabled)"
|
||||
|
||||
# --- Observe /api/blobs for non-empty canonical identity over the run window ----
|
||||
# Identity is written by the Stage 2b write-back each fusion tick; the matcher
|
||||
# triangulates from cached RSSI (~5s BLE cadence). Poll at 0.5s cadence over the
|
||||
# full window (real t=0..duration) so the narrow early blob window is not missed.
|
||||
# Capture the first blob that carries identityResolved==true OR personName != ""
|
||||
# as the canonical evidence; also snapshot /api/ble/matches as corroboration.
|
||||
peak_blobs=0
|
||||
identity_blob="" # first /api/blobs blob JSON with non-empty personName
|
||||
identity_blob_t=""
|
||||
match_json=""
|
||||
sample_count=0
|
||||
ticks=$(( SIM_DURATION * 2 )) # 0.5s cadence
|
||||
for i in $(seq 1 "$ticks"); do
|
||||
sleep 0.5
|
||||
sample_count=$((sample_count+1))
|
||||
t_ms=$(( i * 500 ))
|
||||
raw=$(curl -s --max-time 2 "http://localhost:$PORT/api/blobs" 2>/dev/null)
|
||||
n=$(printf '%s' "$raw" | jq 'length' 2>/dev/null || echo 0)
|
||||
[ "$n" -gt "$peak_blobs" ] && peak_blobs=$n
|
||||
|
||||
# identityResolved==true OR personName != "" => resolved canonical identity
|
||||
id_blob=$(printf '%s' "$raw" | jq -c \
|
||||
'[.[] | select((.identityResolved==true) or (.personName // "" | length > 0))][0]' 2>/dev/null)
|
||||
if [ "$id_blob" != "null" ] && [ -n "$id_blob" ]; then
|
||||
identity_blob="$id_blob"
|
||||
[ -z "$identity_blob_t" ] && identity_blob_t="${t_ms}ms"
|
||||
[ -z "$match_json" ] && match_json=$(curl -s --max-time 2 "http://localhost:$PORT/api/ble/matches" 2>/dev/null)
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $(( i % 2 )) -eq 0 ]; then
|
||||
resolved=$(printf '%s' "$raw" | jq '[.[]|select(.identityResolved==true)]|length' 2>/dev/null || echo 0)
|
||||
printf ' t=%2ds blobs=%s peak=%s identityResolved_blobs=%s\n' "$((i/2))" "$n" "$peak_blobs" "$resolved"
|
||||
fi
|
||||
done
|
||||
|
||||
# Stop the sim so the run is bounded, then tally diagnostics.
|
||||
kill "$SIM_PID" 2>/dev/null || true
|
||||
wait "$SIM_PID" 2>/dev/null; SIM_EXIT=$?
|
||||
SIM_PID=""
|
||||
reject_count=$(grep -ciE 'reject|invalid_token|\b401\b|\b403\b' "$DATA_DIR/sim.log" 2>/dev/null)
|
||||
|
||||
# If identity was observed mid-run, take one more stable sample (matcher persists
|
||||
# after sim stop via identity persistence) for a clean evidence capture.
|
||||
if [ -n "$identity_blob" ] && [ "$identity_blob" != "null" ]; then
|
||||
raw=$(curl -s --max-time 2 "http://localhost:$PORT/api/blobs" 2>/dev/null)
|
||||
fresh=$(printf '%s' "$raw" | jq -c \
|
||||
'[.[] | select((.identityResolved==true) or (.personName // "" | length > 0))][0]' 2>/dev/null)
|
||||
[ "$fresh" != "null" ] && [ -n "$fresh" ] && identity_blob="$fresh"
|
||||
fi
|
||||
# Final matcher surface snapshot (corroboration).
|
||||
final_matches=$(curl -s --max-time 2 "http://localhost:$PORT/api/ble/matches" 2>/dev/null)
|
||||
final_summary=$(printf '%s' "$final_matches" \
|
||||
| jq -c '[.[] | {blob_id, person_name, device_addr, confidence, is_ble_only}]' 2>/dev/null)
|
||||
|
||||
echo ""
|
||||
log "---- RESULT ----"
|
||||
log "seeded person: \"$PERSON_NAME\" (id=$SEED_PERSON_ID) device: $WALKER_MAC"
|
||||
log "peak_blob_count=$peak_blobs"
|
||||
log "identity_blob_at=${identity_blob_t:-never}"
|
||||
log "samples=$sample_count (0.5s cadence) nodes=$SIM_NODES walkers=$SIM_WALKERS rate=$SIM_RATE duration=${SIM_DURATION}s seed=$SIM_SEED ble=on"
|
||||
log "sim_exit=$SIM_EXIT reject_in_sim_log=${reject_count:-0}"
|
||||
echo ""
|
||||
log "evidence: /api/blobs blob with non-empty canonical identity:"
|
||||
if [ -n "$identity_blob" ] && [ "$identity_blob" != "null" ]; then
|
||||
printf '%s\n' "$identity_blob" | jq '.' 2>/dev/null || printf '%s\n' "$identity_blob"
|
||||
else
|
||||
log " (none observed)"
|
||||
fi
|
||||
echo ""
|
||||
log "corroboration: /api/ble/matches (runtime matcher surface):"
|
||||
if [ -n "$final_summary" ] && [ "$final_summary" != "null" ]; then
|
||||
printf '%s\n' "$final_summary" | head -40
|
||||
else
|
||||
log " (none observed)"
|
||||
fi
|
||||
echo ""
|
||||
# Persist the raw evidence for the note / audit.
|
||||
printf '%s\n' "$identity_blob" > "$DATA_DIR/identity_blob.json"
|
||||
log "mothership matcher-related log lines (INFO/WARN):"
|
||||
grep -iE 'ble:|identity|triangulat|match' "$DATA_DIR/mothership.log" 2>/dev/null | tail -15 || log " (none)"
|
||||
|
||||
# --- Acceptance gate: a live blob carried non-empty canonical identity ----------
|
||||
if [ -z "$identity_blob" ] || [ "$identity_blob" = "null" ]; then
|
||||
log "FAIL: no /api/blobs blob carried non-empty canonical identity over the run window"
|
||||
log " peak_blobs=$peak_blobs final /api/ble/matches: ${final_summary:-[]}"
|
||||
log " (mothership.log tail for diagnostics:)"
|
||||
tail -20 "$DATA_DIR/mothership.log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
person_val=$(printf '%s' "$identity_blob" | jq -r '.personName // empty' 2>/dev/null)
|
||||
color_val=$(printf '%s' "$identity_blob" | jq -r '.assignedColor // empty' 2>/dev/null)
|
||||
resolved_val=$(printf '%s' "$identity_blob" | jq -r '.identityResolved // empty' 2>/dev/null)
|
||||
log "PASS: live /api/blobs blob has non-empty identity — personName=\"$person_val\" assignedColor=\"$color_val\" identityResolved=$resolved_val"
|
||||
exit 0
|
||||
Loading…
Add table
Reference in a new issue