Commit Graph

20 Commits

Author SHA1 Message Date
thejayman77 f8aa27e6cb Doc cleanup: handoff + load-listener comment match shipped bullseye audio
Codex re-audit of 0cb315f was functional-green (81 tests, clean build,
device-green) with two non-runtime doc findings:
- SOUND_DESIGN_HANDOFF.md still described the retired "Level on lock,
  once, then silence" behavior. Now documents immediate bullseye
  alignment (enter <=0.2 / exit >=0.35 spatial hysteresis, no dwell) and
  the looping level.wav, with the velocity lock kept separate for
  lime/label/haptic.
- The SoundPool load listener's comment still said "one-shot Level";
  updated to describe the pending-loop-start path.

No code behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:47:23 -04:00
thejayman77 0cb315fdcc Audio: instant bullseye cue on alignment, decoupled from the held lock
Three-way consensus (Jay/Codex/Claude): one sound, split timing not identity.
The bullseye sound now means 'centered right now' and fires the instant you're
aligned - even a fast pass over center - so the zone that needs the most help
locating always announces itself. It no longer waits on the velocity+dwell
lock, which was the delay near center.

- SurfaceTickPolicy: outside the center zone, proximity ticks (unchanged
  anchor scheduler). Inside, emit ALIGNED every frame -> caller loops level.wav.
  Entry is immediate (no dwell, no velocity gate). Alignment uses SPATIAL
  hysteresis (enter <=0.2, rearm only after leaving >=0.35) - not a time
  debounce, so a legitimate quick re-crossing still announces. Ticks resume
  immediately on exit.
- Alignment error = hypot(stable pitch, stable roll) - the SAME axes that drive
  the bubble, so the sound can't lag the visual (Codex's implementation note).
- Player: level.wav loops while aligned (ensureLevelLooping, idempotent),
  stopped before every restart and on exit; pending-load queue retained.
- The held-level confirmation (persistent lime, label, haptic) stays on the
  velocity-aware LockDetector; the locating sound is fully decoupled from it.

Tests: immediate cue on fast crossing, one start / ticks-suppressed while
inside, stop+resume-ticks on exit, spatial-hysteresis rearm, tick reactivity.
81 tests passing; assembleDebug clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:18:04 -04:00
thejayman77 604d61ebb2 Voice init-race fix + velocity-aware Surface lock
Voice (final P1 from audit): the policy is no longer consumed until TTS is
ready, so a phrase can't be queued and then spoken stale after init. When
ready, the policy evaluates the CURRENT reading (silent if moving, correct
instruction if settled). VoiceSpeaker drops its pending-text queue and just
exposes isReady; the policy is remember(speaker) so each VOICE session starts
fresh. Covers all of Codex's cancellation cases (movement, unlock, invalid
placement, foreground loss, mode change, shutdown).

Velocity-aware Surface lock (Jay's observation + Codex): the fixed 400 ms
dwell made a slowly-arriving bubble wait, delaying the level sound, while a
fast crossing could still lock. LockDetector now takes a movement rate:
- error <=0.2 deg AND rate <=0.3 deg/s -> lock after a short 175 ms confirm;
- faster than that -> dwell never accumulates (a center fly-through never
  locks); slowing to a stop inside the zone starts a fresh confirmation;
- 0.35 deg exit hysteresis and the single shared lock (sound/lime/haptic)
  unchanged.
LevelPipeline feeds SettlingDetector.movementRateDegreesPerSecond (was
discarded); Edge passes null -> classic fixed 400 ms dwell. Simple gated
machine, no adaptive-dwell formula.

Tests: slow-entry-locks-promptly, fast-traversal-never-locks, intermediate-
speed-no-accumulate, slowing-inside-zone-fresh-dwell, exit hysteresis,
feedback debounce, Edge fixed-dwell fallback. 82 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:27:47 -04:00
thejayman77 3ebb36f265 Address Codex audit of ticking (36c0cfa): dynamic + init-race fixes
P1 - Tick cadence now reacts immediately to changing tilt. The due time is
recomputed every update from the last-tick anchor plus the CURRENT interval,
so dropping from 5deg to 0.4deg accelerates to the fast cadence at once
instead of waiting out the stale slow interval. Anchor advances by whole
intervals (no drift) and resyncs to now if a full interval behind (no burst).
Added far->near and near->far transition tests.

P1 - One-shot cues no longer lost to async loading. SonarSoundPool queues a
Level asked for before its sample loads and plays it on load-complete (and
cancels the queued Level on unlock/gate). VoiceSpeaker queues the latest
phrase requested before TTS finishes init and speaks it on ready. The policy
fires these once, so they can't rely on retries.

P2 - Unlock stops the level.wav tail BEFORE playing the resumed tick (order
was reversed, allowing a brief overlap).

P2 - Audio-mode preference write moved from the screen's rememberCoroutineScope
to container.applicationScope, so a quick navigation can't cancel it (matches
the earlier persistence hardening).

Cleanup: AudioAssistMode and LevelPipeline comments now describe ticking, not
the retired radar pings/homing bands; AUDIO_DESIGN_LOG cadence principle
reconciled with the 125 ms continuous-tick near rate.

78 tests passing; assembleDebug clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:01:52 -04:00
thejayman77 36c0cfa5ab Audio: Surface proximity ticking + Voice settled-change; stage Edge staircase
Replaces the hands-free audio assist after extensive on-device iteration
(full journey + rejected approaches in AUDIO_DESIGN_LOG.md; current design
in SOUND_DESIGN_HANDOFF.md, from Jay's sound-design sessions).

Surface (TONES) = continuous proximity ticking: tick.wav repeats faster as
you near level (axis-agnostic warmer/colder, the rate is the message).
- Exponential rate in stable tilt error, clamped 1.5/s (>=~5 deg) to 8/s
  (<=~0.4 deg), hard-capped; constant loudness (acceleration is the signal).
- Deadline scheduler off a monotonic clock (SystemClock.elapsedRealtime):
  advances a next-tick deadline, so no drift and no catch-up bursts; <=1 tick
  per sensor update; immediate tick on re-entry (enable/return FACE_UP/unlock).
- Lock reuses LockDetector.isLocked (dwell + hysteresis, no second audio
  threshold): stop ticking, play level.wav once, silence while held. The
  1.39s level stream is retained and stopped on unlock/disable so a fresh
  tick can't overlap its tail.
- Pure SurfaceTickPolicy (unit-tested: anchors/clamps, monotonic rates,
  gating, immediate re-entry, no catch-up burst, level-once, resume on unlock).

Voice (VOICE) = settled-change announcer: one correction on the dominant
axis, silent while moving, speaks again on settle only if direction changed,
crossed coarse->fine, or reached lock. No periodic repeat. (VoiceGuidancePolicy,
unit-tested.)

Assets: tick.wav + level.wav downsampled to mono 44.1kHz in res/raw (from
Jay's SoundQ-derived 96k masters). AudioAssistMode adds OFF/TONES/VOICE.

Retired the settle-gated two-ding packet scheduler (AudioAssistStateMachine +
its ding assets) - superseded by ticking for Surface; logged as tested/rejected.

Edge staircase (1-D, future): 5 pitch-contour masters staged in
sonar-staircase-v3/ for when Edge mode is built.

76 tests passing; assembleDebug clean; blessed on-device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:43:18 -04:00
thejayman77 e5d15c920f Serialize app-scope writes via Main.immediate (Codex hardening note)
Non-blocking follow-up from the audit: applicationScope ran on
Dispatchers.Default, so two very fast conflicting persistence writes
(recalibrate then immediately Reset) had no ordering guarantee. Main.immediate
launches the coroutine body synchronously in call order, so the DataStore
edits enqueue deterministically; DataStore still does the IO on its own
dispatcher. Only lightweight persistence uses this scope.

64 tests passing; assembleDebug clean; runs on device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:21:57 -04:00
thejayman77 daf19c34b3 Address Codex audit: Tools cards, 4-point validation, durable persistence
P1 - Tools screen no longer exposes dead 'coming with the feature build'
cards. Replaced the stale Calibration and Units/Feedback cards with a
single working Settings card that opens the Settings screen.

P1 - Detailed (4-point) calibration now validates rotation quality, not
just the final mean. A 4-point set holds two independent 180-flip bias
estimates - the (0,180) and (90,270) pairs; a malformed turn makes them
diverge even when the mean stays small (which the 3-degree magnitude
guard can't see). isRotationConsistent rejects divergence beyond 0.4
degrees. The disagreement scales with the surface's true tilt, so it's
lenient near level and strict on a steeper permitted surface. Test
proves a deliberately 10-degree-over-rotated set is rejected and a clean
set passes.

P2 - Calibration persistence moved from the screen's rememberCoroutineScope
to a container-owned applicationScope, so the write survives an immediate
Done/back after 'saved'. Preference writes moved there too.

Cleanup: haptic copy is mode-neutral ('when the level locks'); Settings
no longer keeps the screen awake (calibration still does); calibration
method renamed Thoroughness -> Method with Standard/Detailed labels and
accurate 'Four positions, a quarter-turn apart' / 'Averages more sensor
noise' copy; lime now marks the active-calibration status (confirmed
state), consistent with lime = level/confirmed.

64 tests passing; assembleDebug clean; verified on device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:15:29 -04:00
thejayman77 85a8ce1136 Calibration UX overhaul + Settings screen (Claude, driving)
Fixes a real defect and reworks the calibration/settings flow per Jay's
on-device review.

Bug: the second capture hung on 'capturing'. The completion code wrote
capture=null (a LaunchedEffect key) then awaited the suspend
setSurfaceCalibration BEFORE setting phase=COMPLETE; at the suspension
Compose cancelled the effect mid-write, stranding the transition. The
effect body is now fully synchronous and persistence runs on an
independent scope.launch. Verified end-to-end on device.

Settings screen (new): reached from the Level header gear (was: gear
jumped straight into calibration). Calibration is a sleek button in its
own section; Units, Haptics, and Reduce-motion prefs get a home; Done is
pinned to the bottom while the list scrolls independently.

Calibration screen: two stages. OVERVIEW shows the current correction
(Pitch/Roll offsets), the Thoroughness selector, and Calibrate / Reset
to phone defaults / Done. CAPTURING shows ONLY capture + Cancel; Cancel
restores the prior calibration (saved value is never touched mid-flow).
Instructions centered; outlined buttons given visible borders.

Thoroughness: Simple (2-point, 0/180) or Thorough (4-point,
0/90/180/270). Generalized deriveSurfaceFromSamples averages a symmetric
rotation set - the true tilt sums to zero, so the mean is the device
bias; 4-point also cancels each axis twice and averages more noise. Two
new unit tests (2-point equivalence, 4-point bias recovery).

61 tests passing; assembleDebug clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:37:35 -04:00
thejayman77 721fccd5ce Surface polish: motion fix, instrument redesign, readout typography (Codex + Claude)
Driven by on-device iteration on a Galaxy S24+ (live ADB install/
screenshot loop), gated by a real-device motion pass at animator
scale 1x and frame stats (p95 8ms, 0.85% legacy jank).

Motion (Codex): X/Y axes animate in independent concurrent effects.
The prior sequential animateTo calls starved Y under ~50Hz
retargeting - diagonal movement traced an L and the second axis
caught up seconds late. Reduced-motion/system-disabled paths snap
both axes.

Instrument (Claude, on Codex's base):
- Fluorescent yellow-green fluid (matches real spirit-level dye);
  amber retained for guidance text.
- Bubble as a true void: borderless symmetric gradient orb,
  translucent interior (marks read through it), soft bright
  refraction band, contact shadow, fluorescent halo, window-style
  reflection + glint. Velocity-squash experiment tried and removed -
  shape fidelity beats the flourish.
- Bubble (0.12R) nests inside the 1-degree target ring; ring turns
  lime on VISUAL containment of the drawn bubble (lock label/pulse/
  haptic remain on LockDetector truth).
- Machined bezel, etched cardinal/45 ticks, per-ring degree labels
  via the same SurfaceGuidance mapping. dp-scaled strokes replace
  raw-pixel hairlines; contrast lifted for real screens.
- Red center target dot beneath the translucent bubble (Codex).

Text hierarchy: guidance suppressed while locked (no correction
advice under a 'Flat within' verdict), demoted to titleLarge, and
raised into the empty status line's slot. Primary readout optically
dead-centered via an invisible leading degree twin; degree symbol
.55em at TextDim with cap-height baseline shift (.38 - metrics pair,
resize together).

59 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 07:34:58 -04:00
thejayman77 30417a5f98 Slice 5: Audio Level Assist + fixed guidance slot (Codex)
- AudioAssistStateMachine (core/audio, pure, tested): proximity bands
  with enter/exit hysteresis (close 1.0/1.2, near 3.0/3.4 degrees),
  cadence preserved across band changes (no threshold double-ping),
  lock cue driven solely by LockDetector's fireFeedback transition -
  no second lock threshold, and the haptic debounce paces the ping.
- Silence gates: disabled, backgrounded, settling, invalid placement,
  or outside FACE_UP all reset and mute; all five unit-tested.
- SonarSoundPool: locally synthesized WAV pings via SoundPool with
  USAGE_ASSISTANCE_SONIFICATION; zero audio-focus APIs (verified by
  grep); player exists only while enabled and resumed, released on
  composition disposal.
- Persisted SONAR ON/OFF header control using the existing
  audioCueEnabled preference; off by default; state shown as text,
  never color alone.
- Guidance slot fixed at 56dp so settling/correction/level copy no
  longer shifts the instrument (Slice 4 polish note).

59 tests passing.

Audited-by: Claude (no findings)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:43:07 -04:00
thejayman77 027b9b96b3 Slice 4: actionable adjustment guidance (Codex)
- SurfaceAdjustmentInfo: high-side instruction sourced exclusively from
  SurfaceGuidance.from(displayed pitch, displayed roll).highLabel - the
  deadbanded axes supply the label hysteresis, so wording cannot flap
  at direction boundaries.
- Shared SettlingDetector exposed through the pipeline as the sole
  motion gate; strong guidance defers behind a quiet Settling state.
- RiseRun (core/sensors, pure): tan-based mm/m and in/ft, derived from
  the same deadbanded magnitude as the numeric readout; pinned at
  45 degrees = 1000 mm/m = 12 in/ft.
- Persisted MeasurementUnits preference in core/settings (metric
  default); Tools UI control remains deferred per plan.
- Guidance renders only in face-up Surface mode, hidden at level
  (no direction below the 0.1-degree deadband).

56 tests passing.

Audited-by: Claude (no findings)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:43:42 -04:00
thejayman77 422854de07 Slice 3: tactile bullseye instrument (Codex)
- SurfaceGuidance (core/sensors, pure, tested): the sole source of
  bubble position, high-side direction, and ring scale. Bubble and
  1/2/5-degree etched rings share one 5-degree visual range; the
  clamped target puts a saturated bubble exactly at the rim ring.
- SurfaceBullseye: Compose Canvas glass vial with crosshairs and
  amber bubble. Critically damped (no-overshoot) spring toward the
  pre-clamped target; velocity carries across retargets. Reduced
  motion (in-app pref or ANIMATOR_DURATION_SCALE == 0) snaps directly.
- Lock treatment: brief lime ring pulse on acquisition, persistent
  lime tolerance label while locked, neutral numeric readout. Never
  color-only; no full-screen lime state.
- Pipeline passes stable calibrated pitch/roll to the instrument;
  deadbanded readout remains authoritative. Bullseye renders only in
  the FACE_UP presentation.

53 tests passing.

Audited-by: Claude (1 finding raised and resolved)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 15:30:45 -04:00
thejayman77 d3854ac402 Slice 2: guided Surface calibration flow (Codex)
- SurfaceCalibrationScreen: two-sample guided flow from the Level header.
  Captures read SensorSource.gravity directly - stored calibration is
  never applied to either sample. Exact 180-degree same-plane guidance,
  clear-saved-calibration action, Surface persistence only.
- SettlingDetector (core/sensors, shared): 1.0 deg/s enter, 0.3 deg/s
  exit held 500 ms; mid-band movement resets the quiet dwell without
  leaving Settling (audit finding 1).
- StableSurfaceCapture (core/sensors): continuous 1.5 s settled window
  with the 5-degree calibration-surface guard; restarts on motion.
- SurfaceCalibrationValidation (core/sensors): 3-degree bound on the
  combined two-axis bias magnitude, unit-tested both sides of the
  boundary (audit finding 2).
- Lock feedback suppression is structural: the calibration route removes
  LevelScreen from composition, cancelling its sensor collection.

50 tests passing.

Audited-by: Claude (2 findings raised and resolved)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:51:28 -04:00
thejayman77 8d942a88b4 Slice 1: honest full-range Surface readings, self-gating lock (Codex)
- Surface tilt magnitude stays visible 0-180 degrees; never blanked by
  placement. Lock is self-gating (only near-zero magnitude can enter),
  so the placement gate now drives hints only. Edge gating unchanged.
- Pitch/Roll suppressed as ambiguous at >=80 degrees; explicit
  screen-down state past 90 degrees with the true magnitude retained.
- SurfacePresentationDetector classifies from the same deadbanded
  stable magnitude the UI displays, with 1-degree hysteresis at both
  boundaries and a direct face-up -> screen-down transition.
- Tests: angle sweep (30/80/near-vertical/screen-down), hysteretic
  boundary tests, and a sustained 30-degree run proving a steep phone
  never locks or fires feedback. 43 tests passing.

Audited-by: Claude (2 findings raised and resolved)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:10:05 -04:00
thejayman77 0c197894ff Revise Surface plan per audit: near-vertical spec, calibration guards, shared settling, pure guidance mapping (Codex) 2026-07-13 11:52:52 -04:00
thejayman77 abff397364 Add Surface Level attack plan (Codex) and Gradle daemon JVM pin 2026-07-13 10:29:04 -04:00
thejayman77 99c2b12192 Address Codex measurement-core review
- Calibration: still derived in angle space via the 180-degree flip, but now
  APPLIED in vector space as a reference-orientation rotation (Rodrigues
  alignment for Surface, Z-rotation for Edge), exact away from zero; tests
  at 30 degrees, cross-axis, and edge-polarity cases.
- Angle relative zero: stores the gravity direction vector; relative reading
  is the angle between directions, so cross-axis movement is honest.
- Edge mode: precise geometry documented (either long edge down, gravity
  along +/-X), placement-validity guard so e.g. Edge mode never locks on a
  phone lying flat; UI shows repositioning hints.
- Lock/readout coherence: locked label states the tolerance (exit threshold)
  so the rounded readout can never contradict it; pipeline acceptance tests
  pin the invariant.
- Sensor-vector contract: documented and pinned by SensorContractTest. The
  suggested negation of gravity/accelerometer fallbacks is NOT applied: per
  Android SensorEvent docs, a stationary flat device reads +9.81 on Z (the
  gravity reaction), matching the rotation-vector path as-is. The contract
  tests prove all paths agree.

38 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:45:53 -04:00
thejayman77 2a48d79c77 Scaffold Android project per BRIEF.md
Single-module Kotlin/Compose app with manual DI container. Pure-Kotlin
measurement math (orientation mapping, two-sample per-mode calibration,
EMA smoothing, lock hysteresis, display deadband) fully unit-tested.
Sensor fallback: game rotation vector -> gravity -> low-passed
accelerometer. Live Level (manual Surface|Edge) and Angle (hold-to-zero)
scaffolds; Ruler/Tools placeholders; stub Pro entitlement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:34:57 -04:00
thejayman77 bcda2856f7 Add authoritative v1 product & technical brief (Codex) 2026-07-11 18:25:38 -04:00
thejayman77 d29d9336f4 Add concept board, v1 brief audit, and .gitignore
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 17:52:31 -04:00