commit d29d9336f44a69125c7f6c73e5871be6f66290c6 Author: Jay Date: Sat Jul 11 17:52:31 2026 -0400 Add concept board, v1 brief audit, and .gitignore Co-Authored-By: Claude Fable 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b5006f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store + +# Android (for when the app module lands) +*.iml +.gradle/ +local.properties +.idea/ +build/ +captures/ +.externalNativeBuild/ +.cxx/ +*.keystore diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..a9015ec --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,99 @@ +# 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 1–2 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` 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. diff --git a/resources/Bubble Level Concepts.dc.html b/resources/Bubble Level Concepts.dc.html new file mode 100644 index 0000000..7a12c78 --- /dev/null +++ b/resources/Bubble Level Concepts.dc.html @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + +
+ +
+
+
TURN 1
+

Bubble level — four directions

+
+

Same instrument, four personalities. All share the language: near-black glass, a warm spirit-fluid bubble, and a lime lock when the surface reads flat. Pick a lane (or mix) and I'll refine it.

+ +
+ + +
+
+
1a
+
Bullseye
+
Circular target for flat surfaces — the classic bull's-eye vial, reimagined.
+
+ +
+
+
+
BULLSEYE
+
LIVE
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
X-AXIS
+
1.2°
+
+
+
Y-AXIS
+
0.4°
+
+
+
+
+
+ + +
+
+
1b
+
Cross vials
+
Two spirit tubes in a cross — the honest, tool-like read every builder knows.
+
+ +
+
+
+
SURFACE
+
calibrated · 0.0 ref
+
+
2.6°
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
PITCH
+
2.4°
+
+
+
ROLL
+
0.9°
+
+
+
+
+
+ + +
+
+
1c
+
Angle-first
+
Numbers lead. Editorial, oversized readout with the bubble reduced to a hairline.
+
+ +
+
+
TILT
+
HOLD TO ZERO
+
+
+
+ 8.6 + ° +
+
+
+
+
+
+
+
+
+
+
PITCH
+
8.6°
+
+
+
ROLL
+
2.1°
+
+
+
SLOPE
+
15%
+
+
+
+
+
+ + +
+
+
1d
+
Immersive horizon
+
Full-bleed plane that tilts with the phone. Shown in its lime "locked" state.
+
+ +
+
+
+
+
+
+
+
+ LEVEL +
+
+
+
+
+
+
+
0.0°
+
+
+
+
Surface is flat
+
TAP TO CAPTURE
+
+
+
+
+
+ +
+
+
+
+ +