← back to Ios Fleet Recording Qa
ios-fleet-recording-qa: 2.1 device-recording QA gate (tech + OCR PII + human sidecar), self-test green
bab28770c082c97e232a0c1cd596ed6f1614e4e8 · 2026-09-03 13:22:36 -0700 · Steve
Rebuilds the verify-recording/privacy-scan/readiness-probe the TK-11155 memos assumed existed.
Verdicts: TECH_FAIL / REJECTED_NOT_DEVICE / PRIVACY_FLAGGED / TECH_PASS_PRIVACY_PENDING / READY_TO_SEND.
Device-metadata absence is a warning (AirDrop/re-encode strips it), not a false reject.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNivxV68DdxP1rvhViBfcN
Files touched
A .gitignoreA README.mdA recording-qa.shA selftest.sh
Diff
commit bab28770c082c97e232a0c1cd596ed6f1614e4e8
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 3 13:22:36 2026 -0700
ios-fleet-recording-qa: 2.1 device-recording QA gate (tech + OCR PII + human sidecar), self-test green
Rebuilds the verify-recording/privacy-scan/readiness-probe the TK-11155 memos assumed existed.
Verdicts: TECH_FAIL / REJECTED_NOT_DEVICE / PRIVACY_FLAGGED / TECH_PASS_PRIVACY_PENDING / READY_TO_SEND.
Device-metadata absence is a warning (AirDrop/re-encode strips it), not a false reject.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNivxV68DdxP1rvhViBfcN
---
.gitignore | 8 ++++
README.md | 46 +++++++++++++++++++
recording-qa.sh | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
selftest.sh | 44 ++++++++++++++++++
4 files changed, 233 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5638a0b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+*.mov
+*.mp4
+*.privacy-reviewed.sha256
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e666e75
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# ios-fleet-recording-qa
+
+Pre-submission QA gate for App Store **Guideline 2.1** device screen-recordings — the
+`verify-recording` + `privacy-scan` + `readiness-probe` the TK-11155 approved memos assumed
+existed (they didn't). Works for all 5 rejected apps: GovArbitrage, Homes on Spec,
+Beverly Hills Videos, Celebrity Signatures, Unofficial 90210.
+
+## Why
+An Apple 2.1 reply must attach a **physical-device** recording of the app's core flow. Two ways
+to get re-rejected: (a) a bad clip (too short / black / simulator), or (b) a clip with **PII on
+screen** (a notification banner, a customer's name/address/email/phone in a demo). This gate
+catches both before the clip ever reaches Apple.
+
+## Use
+```sh
+./recording-qa.sh <clip.mov> [app-name] # run the gate, print verdict
+./recording-qa.sh --json <clip.mov> # machine-readable verdict line
+./recording-qa.sh --approve <clip.mov> # AFTER a human eyeball, bind the review to the exact bytes
+```
+
+## Verdicts
+| code | meaning |
+|---|---|
+| `TECH_FAIL` | too short (<20s) / mostly black / unreadable |
+| `REJECTED_NOT_DEVICE` | filename shows a simulator clip (Apple rejects those for 2.1) |
+| `PRIVACY_FLAGGED` | OCR found an email / phone / street-address in a frame — **do not upload** |
+| `TECH_PASS_PRIVACY_PENDING` | tech ok, OCR clean, but no human sign-off yet |
+| `READY_TO_SEND` | tech ok + OCR clean + human sidecar present & matches current bytes |
+
+## How it checks
+- **Tech** (ffprobe/ffmpeg): duration ≥20s, portrait, non-black (`blackdetect`), playable.
+- **Device**: reads `com.apple.quicktime.model` (real iOS captures carry it). ABSENCE is a
+ *warning only* — the tag is legitimately stripped by AirDrop / any re-encode — so a good clip
+ isn't falsely rejected. Only a positive simulator filename tell hard-rejects.
+- **Privacy**: samples a frame every 2s, OCRs each (tesseract), flags email / phone / street-address
+ text. A human still eyeballs and runs `--approve`, which writes a `.privacy-reviewed.sha256`
+ sidecar binding the review to the exact bytes. A matching sidecar **cannot** rescue a
+ tech-fail or a PII-flagged clip (the gate re-checks every run; `--approve` refuses a failing clip).
+
+## Requirements
+`ffmpeg` / `ffprobe`, `tesseract`, `shasum` (all via Homebrew). No network, $0.
+
+## Self-test
+`./selftest.sh` builds synthetic fixtures and asserts the full verdict matrix.
+Proven 2026-09-03: A→TECH_FAIL, B(email)→PRIVACY_FLAGGED, C→PENDING→(approve)→READY_TO_SEND,
+E(sim name)→REJECTED_NOT_DEVICE, and `--approve` refuses a PII clip.
diff --git a/recording-qa.sh b/recording-qa.sh
new file mode 100755
index 0000000..4c3912d
--- /dev/null
+++ b/recording-qa.sh
@@ -0,0 +1,135 @@
+#!/usr/bin/env bash
+# ios-fleet-recording-qa — pre-submission QA gate for App Store Guideline 2.1 device recordings.
+# Rebuilds the readiness-probe the TK-11155 approved memos assumed existed (verify-recording + privacy-scan).
+# Works for ALL 5 rejected apps (GovArbitrage, Homes on Spec, Beverly Hills Videos, Celebrity Signatures, 90210).
+#
+# Usage:
+# recording-qa.sh <video.mov> [app-name] # run the gate, print human verdict
+# recording-qa.sh --json <video.mov> # machine-readable verdict line
+# recording-qa.sh --approve <video.mov> # AFTER a human PII eyeball, bind the review to the exact bytes
+#
+# Verdict codes (match the memo semantics):
+# TECH_FAIL too short / mostly black / unreadable
+# REJECTED_NOT_DEVICE no iPhone/iPad capture metadata (simulator or re-encoded — Apple rejects for 2.1)
+# PRIVACY_FLAGGED OCR found email/phone/address text in a frame -> MUST review/redo
+# TECH_PASS_PRIVACY_PENDING tech ok, OCR clean, but no human sign-off sidecar yet
+# READY_TO_SEND tech ok + OCR clean + human sidecar present & matches current bytes
+set -uo pipefail
+
+MIN_SEC=20 # Apple/2.1 floor
+WANT_SEC=30 # memo wants ~30-60s
+FRAME_EVERY=2 # sample one frame every N seconds for OCR
+BLACK_FRAC_FAIL=0.5
+
+die(){ echo "recording-qa: $*" >&2; exit 2; }
+command -v ffprobe >/dev/null || die "ffprobe missing (brew install ffmpeg)"
+command -v ffmpeg >/dev/null || die "ffmpeg missing"
+
+MODE="run"; JSON=0
+case "${1:-}" in
+ --approve) MODE="approve"; shift;;
+ --json) JSON=1; shift;;
+ "") die "usage: recording-qa.sh <video.mov> [app-name] | --json <v> | --approve <v>";;
+esac
+VID="${1:-}"; APP="${2:-app}"
+[ -f "$VID" ] || die "no such file: $VID"
+SIDE="${VID}.privacy-reviewed.sha256"
+
+# ---- human sign-off path -------------------------------------------------
+if [ "$MODE" = "approve" ]; then
+ # refuse to bind a human review to a clip that doesn't already pass tech+OCR
+ chk=$("$0" --json "$VID" 2>/dev/null)
+ case "$chk" in
+ *'"verdict":"PRIVACY_FLAGGED"'*|*'"verdict":"TECH_FAIL"'*|*'"verdict":"REJECTED_NOT_DEVICE"'*)
+ echo "❌ refusing to approve — clip does not pass the gate:"; echo " $chk"; exit 1;;
+ esac
+ shasum -a 256 "$VID" | awk '{print $1}' > "$SIDE"
+ echo "✅ privacy review bound to bytes -> $SIDE"
+ echo " (re-run the gate; it should now report READY_TO_SEND)"
+ exit 0
+fi
+
+# ---- technical checks -----------------------------------------------------
+DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$VID" 2>/dev/null | cut -d. -f1); DUR=${DUR:-0}
+W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "$VID" 2>/dev/null); W=${W:-0}
+H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "$VID" 2>/dev/null); H=${H:-0}
+MODEL=$(ffprobe -v error -show_entries format_tags=com.apple.quicktime.model -of csv=p=0 "$VID" 2>/dev/null)
+MAKE=$(ffprobe -v error -show_entries format_tags=com.apple.quicktime.make -of csv=p=0 "$VID" 2>/dev/null)
+
+TECH_NOTES=(); VERDICT=""; DETAIL=""
+[ "$DUR" -ge "$MIN_SEC" ] 2>/dev/null || { VERDICT="TECH_FAIL"; DETAIL="duration ${DUR}s < ${MIN_SEC}s floor"; }
+[ "$H" -gt "$W" ] 2>/dev/null || TECH_NOTES+=("not portrait (${W}x${H}) — iPhone captures are portrait; confirm this is intended")
+[ "$DUR" -lt "$WANT_SEC" ] 2>/dev/null && [ -z "$VERDICT" ] && TECH_NOTES+=("short: ${DUR}s (memo wants ~${WANT_SEC}-60s)")
+
+# real-device signal: iOS screen recordings carry com.apple.quicktime.model=iPhone/iPad.
+# NOTE: this tag is legitimately STRIPPED by AirDrop / any re-encode, so treat its ABSENCE
+# as a warning (not a hard reject). Hard-reject only on a positive simulator tell.
+DEVICE_OK=0; SIM_TELL=0
+case "$MODEL$MAKE" in *iPhone*|*iPad*|*Apple*) DEVICE_OK=1;; esac
+case "$VID" in *[Ss]imulator*|*-sim.*|*_sim.*|*.sim.*) SIM_TELL=1;; esac
+
+# black-frame detection over the whole clip
+BLACK=$(ffmpeg -nostats -i "$VID" -vf "blackdetect=d=0.3:pic_th=0.98" -an -f null - 2>&1 \
+ | awk -F'black_duration:' '/black_duration/{s+=$2} END{printf "%d", s+0}')
+BLACKFRAC=$(awk -v b="${BLACK:-0}" -v d="${DUR:-1}" 'BEGIN{ if(d<=0)d=1; printf "%.2f", b/d }')
+awk -v f="$BLACKFRAC" -v t="$BLACK_FRAC_FAIL" 'BEGIN{exit !(f>t)}' && [ -z "$VERDICT" ] && {
+ VERDICT="TECH_FAIL"; DETAIL="mostly black (${BLACKFRAC} of clip) — bad capture"; }
+
+if [ -z "$VERDICT" ] && [ "$SIM_TELL" -eq 1 ]; then
+ VERDICT="REJECTED_NOT_DEVICE"; DETAIL="filename indicates a simulator clip — Apple rejects simulator recordings for 2.1"
+elif [ "$DEVICE_OK" -eq 0 ]; then
+ TECH_NOTES+=("physical-device capture UNCONFIRMED (no iPhone/iPad metadata — fine if AirDropped/edited; NOT fine if it's a simulator clip — eyeball it)")
+fi
+
+# ---- privacy / PII OCR scan ----------------------------------------------
+PII_HITS=(); FRAMES=0
+if [ -z "$VERDICT" ] && command -v tesseract >/dev/null 2>&1; then
+ TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
+ ffmpeg -nostats -loglevel error -i "$VID" -vf "fps=1/${FRAME_EVERY}" "$TMP/f_%04d.png" 2>/dev/null || true
+ EMAIL='[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}'
+ PHONE='(\+?1[-. ]?)?\(?[0-9]{3}\)?[-. ][0-9]{3}[-. ][0-9]{4}'
+ ADDR='[0-9]{1,6} +([A-Z][a-z]+ ){1,3}(St|Street|Ave|Avenue|Rd|Road|Blvd|Dr|Drive|Ln|Lane|Ct|Court|Way|Pl|Place|Blvd)\b'
+ for f in "$TMP"/f_*.png; do
+ [ -e "$f" ] || continue
+ FRAMES=$((FRAMES+1))
+ TXT=$(tesseract "$f" stdout 2>/dev/null | tr -s ' ')
+ for pat in "$EMAIL:email" "$PHONE:phone" "$ADDR:street-address"; do
+ rx="${pat%:*}"; label="${pat##*:}"
+ m=$(printf '%s' "$TXT" | grep -oE "$rx" | head -1)
+ [ -n "$m" ] && PII_HITS+=("$(basename "$f") $label: $m")
+ done
+ done
+ if [ "${#PII_HITS[@]}" -gt 0 ]; then VERDICT="PRIVACY_FLAGGED"; DETAIL="OCR found PII in ${#PII_HITS[@]} frame(s) — redo or crop"; fi
+fi
+
+# ---- final verdict --------------------------------------------------------
+if [ -z "$VERDICT" ]; then
+ if [ -f "$SIDE" ] && [ "$(shasum -a 256 "$VID" | awk '{print $1}')" = "$(cat "$SIDE" 2>/dev/null)" ]; then
+ VERDICT="READY_TO_SEND"; DETAIL="tech PASS + OCR clean (${FRAMES} frames) + human sidecar matches"
+ else
+ VERDICT="TECH_PASS_PRIVACY_PENDING"; DETAIL="tech PASS + OCR clean (${FRAMES} frames); needs human eyeball then: recording-qa.sh --approve '$VID'"
+ fi
+fi
+
+if [ "$JSON" -eq 1 ]; then
+ printf '{"app":"%s","file":"%s","verdict":"%s","duration_s":%s,"dims":"%dx%d","device_meta":"%s","black_frac":%s,"ocr_frames":%d,"pii_hits":%d,"detail":"%s"}\n' \
+ "$APP" "$VID" "$VERDICT" "${DUR:-0}" "${W:-0}" "${H:-0}" "${MODEL:-none}" "$BLACKFRAC" "$FRAMES" "${#PII_HITS[@]}" "$DETAIL"
+ case "$VERDICT" in READY_TO_SEND|TECH_PASS_PRIVACY_PENDING) exit 0;; *) exit 1;; esac
+fi
+
+echo "════════ recording-qa · $APP ════════"
+echo "file : $VID"
+echo "duration : ${DUR}s dims: ${W}x${H} device: ${MODEL:-<none>} ${MAKE:-}"
+echo "black-frac : $BLACKFRAC ocr-frames: $FRAMES"
+for n in "${TECH_NOTES[@]:-}"; do [ -n "$n" ] && echo "note : $n"; done
+for h in "${PII_HITS[@]:-}"; do [ -n "$h" ] && echo "⚠️ PII : $h"; done
+echo "────────────────────────────────────"
+echo "VERDICT : $VERDICT"
+echo " $DETAIL"
+case "$VERDICT" in
+ READY_TO_SEND) echo "➡️ attach this clip in the ASC Resolution Center reply."; exit 0;;
+ TECH_PASS_PRIVACY_PENDING) echo "➡️ eyeball it frame-by-frame; if clean run: recording-qa.sh --approve '$VID'"; exit 0;;
+ PRIVACY_FLAGGED) echo "➡️ do NOT upload — re-record or crop out the PII."; exit 1;;
+ REJECTED_NOT_DEVICE) echo "➡️ capture on a PHYSICAL iPhone (Control Center screen record), not the simulator."; exit 1;;
+ *) echo "➡️ re-capture."; exit 1;;
+esac
diff --git a/selftest.sh b/selftest.sh
new file mode 100755
index 0000000..8ab2019
--- /dev/null
+++ b/selftest.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# selftest.sh — builds synthetic fixtures and asserts the recording-qa verdict matrix.
+# Requires: ffmpeg, ffprobe, tesseract, python3+PIL. $0, no network.
+set -uo pipefail
+cd "$(dirname "$0")"
+QA="./recording-qa.sh"
+T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
+
+python3 - "$T" <<'PY'
+import sys, os
+from PIL import Image, ImageDraw, ImageFont
+T=sys.argv[1]
+def frame(txt,out):
+ im=Image.new("RGB",(1080,1920),"white"); d=ImageDraw.Draw(im)
+ try: f=ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf",72)
+ except Exception: f=ImageFont.load_default()
+ d.multiline_text((90,880),txt,fill="black",font=f); im.save(out)
+frame("Contact test@evil.com", os.path.join(T,"pii.png"))
+frame("Opportunities\nListing Settings", os.path.join(T,"clean.png"))
+PY
+
+ffmpeg -y -loglevel error -f lavfi -i color=c=black:s=1080x1920:d=5 -pix_fmt yuv420p "$T/A.mov"
+ffmpeg -y -loglevel error -loop 1 -i "$T/pii.png" -t 25 -r 5 -pix_fmt yuv420p "$T/B.mov"
+ffmpeg -y -loglevel error -loop 1 -i "$T/clean.png" -t 25 -r 5 -pix_fmt yuv420p "$T/C.mov"
+cp "$T/C.mov" "$T/E_simulator.mov"
+
+v(){ "$QA" --json "$1" 2>/dev/null | sed 's/.*"verdict":"//;s/".*//'; }
+fail=0
+assert(){ local got want; got=$(v "$1"); want="$2"
+ if [ "$got" = "$want" ]; then echo "PASS $(basename "$1") -> $got"
+ else echo "FAIL $(basename "$1") -> got=$got want=$want"; fail=1; fi; }
+
+assert "$T/A.mov" TECH_FAIL
+assert "$T/B.mov" PRIVACY_FLAGGED
+assert "$T/C.mov" TECH_PASS_PRIVACY_PENDING
+assert "$T/E_simulator.mov" REJECTED_NOT_DEVICE
+# --approve refuses a PII clip
+"$QA" --approve "$T/B.mov" >/dev/null 2>&1 && { echo "FAIL --approve accepted a PII clip"; fail=1; } || echo "PASS --approve refused the PII clip"
+# approve the clean clip -> READY
+"$QA" --approve "$T/C.mov" >/dev/null 2>&1
+assert "$T/C.mov" READY_TO_SEND
+
+[ "$fail" -eq 0 ] && echo "ALL PASS" || echo "SOME FAILED"
+exit $fail
(oldest)
·
back to Ios Fleet Recording Qa
·
recording-qa: add credential lens — flag visible username/AP a23289a →