android-input-bounds-audit
Use when adding a typed input or form field to an Android/Compose app, before shipping a form, when a derived metric (lean mass, BMR) can show a negative or absurd value without crashing, or when a nu
SKILL.mdskills/managed/android-input-bounds-audit/SKILL.md
GitHubAndroid input bounds audit
Finds the input defects that never crash: a typo reaches derived maths and the app reports nonsense as fact. Ordered cheapest-first.
1. Enumerate every input, don't recall them
python - <<'PY'
import pathlib, re
for p in sorted(pathlib.Path("app/src/main/kotlin/<pkg>/ui").rglob("*.kt")):
s = p.read_text(encoding="utf-8")
for m in re.finditer(r"onValueChange\s*=\s*\{([^}]{0,90})\}", s):
body = " ".join(m.group(1).split())
capped = any(k in body for k in ("take(", "coerce", "filter"))
print(f"{'ok ' if capped else 'RAW'} {p.name}:{s[:m.start()].count(chr(10))+1} {body[:78]}")
PY
Also sweep parsing sites: grep -rn "toDoubleOrNull\|toIntOrNull". A field
validated only as > 0.0 is the headline defect class.
2. Compute what the unbounded value does downstream
Do NOT argue from plausibility — run the formula:
python -c "w=80.0; bf=500.0; lean=w*(1-bf/100); print(lean, 370+21.6*lean)"
# lean mass -320 kg, BMR -6542 kcal -> shown to the user as a fact
If the number is merely large, it may be acceptable. If it is negative or NaN, the field is a defect regardless of how unlikely the typo is.
3. Bound it in ONE place
Create a single limits object with wide, defensible ranges and a comment giving the real-world anchor (tallest human 272 cm, etc.). Wide enough to accept records, narrow enough to catch a slipped decimal or a unit mix-up.
Wire it at three levels:
- the screen's
enabled =gate, - the repository (defence in depth for future callers),
- the import path — but filter implausible rows rather than
require, because an old archive may carry a typo and losing one row beats losing the whole restore.
Never leave the same literal in two files; that duplication is the bug this object exists to prevent.
4. Client caps must equal server CHECK constraints
Pin them with the migration-reading unit test in wire-name-schema-drift-guard
(its limits axis, mutation-proved from both sides). Patterns that match the two
constraint shapes:
Regex("""char_length\((?:trim\()?$column\)?\)\s*<=\s*(\d+)""")
Regex("""char_length\(trim\($column\)\)\s*between\s*(\d+)\s*and\s*(\d+)""")
5. Prove each gate on device
Read enabled off the clickable ancestor, never the button's text node, and
drive the case table in both directions — an absurd value must read
disabled, and a gate that is always disabled passes a one-sided check — per
compose-control-tap-verification.
6. Record what you deliberately left unbounded
Steppers (fixed increment per deliberate tap) usually should NOT be capped — a cap low enough to matter refuses a legitimate heavy lift. Write the reason down or the next audit re-opens it.