Files
on_the_level/AUDIT.md
T
2026-07-11 17:52:31 -04:00

6.9 KiB
Raw Blame History

On the Level — v1 Brief Audit

Auditor: Claude · Date: 2026-07-11 Subject: v1 product/architecture brief (Codex) + concept board (resources/Bubble Level Concepts.dc.html)

Verdict

Approve with amendments. The framework is sound and well-proportioned: single app module, manual DI container, no backend, offline-first, and — the load-bearing decision — the three-value separation of raw sensor → stable measurement → animated display. Findings below are ranked; the first three should be resolved in the brief before code is written.


Blocking findings

1. Default sensor: use TYPE_GAME_ROTATION_VECTOR, not TYPE_ROTATION_VECTOR

TYPE_ROTATION_VECTOR fuses the magnetometer, and a level is used precisely where magnetometers lie: against steel studs, appliances, metal door frames, workbenches with power tools. Heading is irrelevant to a level — only the gravity direction matters.

  • Primary: TYPE_GAME_ROTATION_VECTOR (gyro + accel, no magnetometer)
  • Fallback: TYPE_GRAVITY, then low-passed accelerometer (as the brief already plans)
  • Declare uses-feature android.hardware.sensor.accelerometer (required) in the manifest.

2. Missing concept: flat mode vs. edge mode

A bubble level is used two ways: phone lying on a surface (bullseye/cross — what all four mockups show) and phone held on its long edge against a wall, shelf, or picture frame. The math, the meaningful readout, and the UI all differ, and credible level apps auto-switch based on the gravity vector. Edge mode is arguably the more common real-world use.

  • Add mode detection/auto-switching to v1 scope.
  • This affects the sensor-mapping design, so decide now — it does not retrofit cleanly.

3. Calibration: store a reference orientation per mode, not one scalar offset

Subtracting a single angle baseline is only valid near the orientation where it was captured; a flat-mode calibration silently applied in edge mode makes readings worse.

  • Store the baseline as a reference gravity vector/quaternion, per mode (flat / edge).
  • Strongly recommend the classic two-measurement flow: measure, flip the phone 180°, measure again — true level is the average. It cancels device bias without needing a known-level surface, mirrors how physical levels are calibrated, and is trust-building UX.
  • Invariant: lock detection and the displayed degree readout must be computed from the same calibrated stable value. The lock must never fire while the readout shows 0.1°.

Answers to the brief's audit questions

Sensor source / orientation reliability

Yes, once findings 12 are addressed. One implicit decision to make explicit: sensor axes are device-frame, not display-frame. Cleanest approach is to lock the Activity to portrait (avoids recreation churn mid-measurement) and remap axes / rotate UI affordances yourself from the gravity vector — in which case "normalize for screen rotation" is your own remapping code, not Display.getRotation(). Either approach works; pick one explicitly.

Calibration consistency

Covered by finding 3.

Feedback/animation separated from measurement truth

The three-value separation handles this. Specify the mechanisms so it's testable:

  • Stable value: EMA/low-pass with a defined time constant
  • Display value: critically-damped spring
  • Readout: 0.1° resolution with a display deadband so it doesn't flicker between 0.0° and 0.1°
  • Lock: enter ≤0.2°, exit ≥0.35°, short dwell before feedback (as proposed — good), plus a debounce floor of a few seconds between haptic events

Free tier completeness

Complete and fair as specced. Product concern (non-blocking): the screen ruler is a weak Pro anchor — screen rulers are commodity, ~15 cm, and mediocre by nature. Target-angle alerts and saved angle references are the real Pro value for anyone doing actual work (stair stringers, drainage slope, shelf pitch). Market Pro around those; treat the ruler as a bonus.

Pro boundary containment

The narrow ProEntitlementRepository is right. Make it concrete:

  • Entitlement exposed as a StateFlow<Entitlement> from the container
  • Billing calls confined to core/billing
  • Free path never blocks on billing init (billing client connects lazily/async; level screen renders immediately)
  • Cache entitlement in DataStore — Play Billing's queryPurchases needs healthy Google Play services; a Pro user on an offline job site must not lose the ruler

Accessibility / legibility / reduced motion

Two Android-specific corrections:

  • There is no first-class reduced-motion preference on Android — the signal is Settings.Global.ANIMATOR_DURATION_SCALE == 0 (user disabled animations). Honor that plus an in-app toggle.
  • Haptics: use HapticFeedbackConstants.CONFIRM / VibrationEffect so system haptic settings are respected.
  • Bright-light check the amber-on-graphite and lime-lock palette — outdoor use is core. Keep numeric readouts near-white (as mockup 1c does); consider a high-contrast option later.

Over-complexity

No — architecture is right-sized. If anything, add two small things:

  • FLAG_KEEP_SCREEN_ON while a tool screen is visible (levels are used hands-free; screen timeout mid-measurement is a one-star review)
  • Unregister sensor listeners when backgrounded (battery)

Smaller notes

  • Ruler calibration UX: calibrate against a credit card (ISO width 85.60 mm) rather than asking the user to hold a physical ruler to the screen. Key calibration by Display identity, not just configuration — foldables have two displays that each need their own scale.
  • Percent grade: define as tan(pitch) × 100 and decide display behavior as pitch approaches 90° (cap or show ∞) — otherwise a wall reads a comedy number.
  • Testability, made concrete: all math (axis remapping, calibration application, smoothing, hysteresis) lives in pure Kotlin behind an injected SensorSource fake, with recorded real-device sensor traces as golden fixtures. That makes "trustworthy" verifiable rather than aspirational.
  • Trivia: mockups use iOS device frames for an Android target (fine — UI is fully custom; just handle edge-to-edge insets and back gesture). The concept HTML references ./support.js and ./ios-frame.jsx not present in the repo.

Summary of amendments to the brief

  1. Sensor: TYPE_GAME_ROTATION_VECTOR primary; gravity/accel fallback; manifest uses-feature.
  2. Add flat-mode / edge-mode detection and switching to v1 scope.
  3. Calibration = reference orientation stored per mode; offer 180°-flip calibration; lock and readout share one calibrated value.
  4. Rebalance Pro pitch toward target angles / saved references over the ruler.
  5. Specify smoothing (EMA + damped spring), display deadband, haptic debounce floor.
  6. Entitlement cached in DataStore; billing never blocks the free path.
  7. Reduced motion via ANIMATOR_DURATION_SCALE; system-respecting haptics.
  8. FLAG_KEEP_SCREEN_ON on tool screens; unregister sensors when backgrounded.