# POLISH-PLAN — end-goal audit fixes + v1 packaging (2026-07-06)

JIT plan per AGENTS.md. Follows VOICE-PLAN (gate COMPLETE — all four DESIGN.md v1 features
are live on-device). Trigger: the end-goal review — four parallel audits of the shipped app
against DESIGN.md features 1–4, every semantic element verified end-to-end (UI → viewmodel →
JNI → host binary) with file:line evidence. Scope here = closing the audit gaps that are
ours to close, plus the packaging-polish scope decision.

## Audit verdict (2026-07-06)

| Feature | Verdict | Gaps |
|---|---|---|
| 1 Endpoint browser | PARTIAL ×3 | non-reactive "live" refresh; instance state not link-liveness-driven; `last-active`/`description`/`adapter` absent (upstream schema, §W3-8) |
| 2 Endpoint view | SATISFIED (11/11) | none functional; viewmodel glue (gap→resync wiring, send walk) untested in isolation |
| 3 Voice pipe | SATISFIED | `client="ring"` unverified (tolerant parse, fine); KNOWN-HAZARDS "200 unconditionally" wording over-broad vs code |
| 4 Two-way messaging | SATISFIED core | notif tap no deep-link; msg-id-less inbound never dedups; no fsync on host appends; device key = `Build.MODEL` (collision); "on delivery" = 15 s drain poll |

Cursor mechanics (truncate-to-from, overshoot-gap resync, `after_predates_window` wholesale
replace), voice semantics 1–5, QUEUED-is-success, star-per-host, history union + exact
dedup, cross-node pending route: all verified implemented AND tested (JVM twins + Rust
units + cross-process `host_link.rs` + on-device smokes).

## Scope W1 — DESIGN-semantics gaps (app)

1. **Browse liveness** (REQ-BROWSER-LIVE): `BrowseViewModel` collects `supervisor.states`
   (re-list on any host link-state change) + periodic re-list tick while the Browse tab is
   visible; manual Refresh stays. Kills the read-`.value`-once snapshot
   (`BrowseViewModel.kt:56`).
2. **Ruling 9 in the browser** (REQ-ENDPOINT-INSTANCES): the Mobile Gateway endpoint's
   instances take state from device-link liveness (overlay `HostLinkState` on the gateway
   instance rows), not registry `status` alone.
3. **Notification deep-link** (REQ-INBOUND-NOTIFS): PendingIntent carries the sending
   endpoint id; `MainActivity` intent handling opens that endpoint's view directly.

## Scope W2 — robustness (host + app)

4. **Dedup axis always exists**: host mints a uuid msg-id at `route_inbound` ingest when
   the `<EVENT>` carries none — one mint, before history+spool append, so torn-drain
   re-delivery dedups exactly (closes the msg-id-less double-notify/double-log hole;
   REQ-HAZARD-DUP-ROWS host unit).
5. **Durability barrier**: `spool.rs` / `history.rs` appends flush + `sync_data` before
   returning (the enqueue-durable-before-answer story currently stops at the OS cache).
6. **Stable device key**: per-install random suffix persisted in DataStore →
   `MODEL-<uuid8>`; used for new pairings (existing registrations keep working — name only
   enters at pair time).

## Scope W3 — docs, traceability, packaging

7. **KNOWN-HAZARDS §1.1 wording**: scope "200s unconditionally" to the downstream-state
   axis exactly as `WebhookHandler.kt:7-20` argues (401 auth / 400 parse / 413 size / 500
   enqueue-failure enumerated; false 200 on enqueue failure would BE the loss). Doc must
   not be broader than the code it certifies.
8. **Missing Instance fields** (`last-active`, `description`, `adapter`): wire shape was
   pinned by probing the released binary; those fields aren't in the emitted JSON. Upstream
   ask — endpoint-list `--json` schema docs (incl. `local` section semantics) + whether the
   registry Instance row is fully exposed. Recorded in SPT-CORE-NEEDS.md §3 item 4; doyle
   informed with field-level evidence. Render the fields when the binary emits them —
   nothing to build phone-side now.
9. **Packaging polish (scope decision)**: sideload-grade release build, NOT Play. Signing
   config wired to a local keystore (instructions in README, keystore never committed),
   `versionCode`/`versionName` set, R8 minify ON for release with JNI keep rules
   (`LinkNative` + Room), release APK smoke-installed on emulator, README gains an
   install/sideload + Pebble-setup section. Play listing/signing/store assets stay
   long-term.

## Non-goals (audit-accepted as-is)

- Live-delivery push: the 15 s drain IS the delivery path; "on delivery or spool drain"
  collapses to drain by design (ruling 7 defers wake paths). Latency ≤ drain tick, never
  lost — acceptable v1.
- dataSync 6 h/day cap (Android 15 / targetSdk 35): documented watch item, mitigation =
  future wake path (ruling 7).
- `client="ring"` assertion (tolerant parse is correct for an input we don't act on).
- Viewmodel-glue isolation tests: pure parts unit-tested, wire path proven cross-process;
  Android-coupled glue tests deferred until a instrumented-test wave earns its keep.
- Audio forwarding, ntfy/UnifiedPush, camera QR (DESIGN §Long-term).

## Traceability

No new feature REQs — W1/W2 land under existing ids (REQ-BROWSER-LIVE,
REQ-ENDPOINT-INSTANCES, REQ-INBOUND-NOTIFS, REQ-HAZARD-DUP-ROWS, REQ-HAZARD-DICTATION-LOSS
doc wording). New `REQ-RELEASE-PACKAGING` (stages `["doc"]` — Gradle/Kotlin evidence is
scanner-deferred per traceable-reqs#9) for W3-9. Host-side W2 items add Rust unit evidence
(scanner-enforced).

## Gate

`assembleDebug` + `assembleRelease` (minified) green · JVM tests · `cargo test` (rust +
host) · `traceable-reqs check` exit 0 · emulator smoke (both classes) · release APK
installs + links on emulator.
