Files
2026-07-11 17:52:31 -04:00

100 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.