mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 21:07:12 +01:00
`adb install -r` preserves app data and phone_deploy already refuses every path that would not, but "preserved" is a property of the happy path -- a failed install or a later wipe leaves unpublished entries existing nowhere else. The log is CRDT-synced precisely so a recoverable copy exists, so the deploy now checks that one does before touching the device. The obvious device-side signal does not work and would be a gate that can never pass: the app records a successful tick through SyncHealth, which writes to SharedPreferences (app-private, unreadable over ADB on a release build), and it only writes to logcat when a sync *fails*. So the check is remote-side -- pull the shared log and assert it holds recent entries, which answers "is there a copy off the device" directly. Advisory by default; SYNC_GATE_STRICT=1 makes it block (exit 38). A network outage must not stop a fix reaching the phone. The shell half lives in ~/.claude/scripts/lib/phone_deploy_sync.sh. Verified against the real phone: passes when a peer is current, skips cleanly for an unsynced or not-yet-installed app, and blocks with exit 38 in strict mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQpCiPaSbyRpwsLkH3dgsX
79 lines
3.2 KiB
Python
Executable File
79 lines
3.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Assert that a peer already holds this device's recent food-log entries.
|
|
|
|
Used by ``phone_deploy.sh``'s pre-install gate. ``adb install -r`` preserves
|
|
app data, and the deploy script refuses every path that would not -- but
|
|
"preserved" is a property of the happy path. Only a copy on a peer survives a
|
|
failed install or a later wipe, and the log is CRDT-synced precisely so such a
|
|
copy exists.
|
|
|
|
The check is deliberately **remote-side**. The obvious device-side signal does
|
|
not work: the app records a successful tick through ``SyncHealth``, which
|
|
writes to SharedPreferences (app-private, unreadable over ADB on a release
|
|
build), and it only writes to logcat when a sync *fails*. Watching logcat for
|
|
a success marker would be a gate that can never pass, which is worse than no
|
|
gate -- it trains you to ignore it.
|
|
|
|
So this pulls the shared log and asserts the merged result actually contains
|
|
recent entries. It answers "is there a recoverable copy off the device", which
|
|
is the question the gate exists to ask.
|
|
|
|
Exit codes: 0 a peer holds recent state | 1 it does not, or the remote could
|
|
not be reached (the caller decides whether that blocks the deploy).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
import importlib
|
|
import logging
|
|
import pathlib
|
|
import sys
|
|
|
|
# Running this as `python3 scripts/sync_freshness.py` puts *scripts/* on
|
|
# sys.path, not the repo root, so `import diet_guard` fails unless the package
|
|
# happens to be installed -- which it is not on a CI runner that only installs
|
|
# requirements.txt. Same reason and same fix as `check_patch_targets.py`.
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
|
|
|
#: How stale the newest synced entry may be before the gate calls it unproven.
|
|
#: Generous: a day with no meals logged yet is normal first thing in the
|
|
#: morning, and this must not fail a deploy for an ordinary quiet morning.
|
|
_MAX_AGE_DAYS = 3
|
|
|
|
_logger = logging.getLogger("sync_freshness")
|
|
|
|
|
|
def main() -> int:
|
|
"""Pull the shared log and report whether it holds recent entries."""
|
|
# Imported here, after the sys.path fix above and behind the entry point,
|
|
# so the module's own imports stay stdlib-only and lint-clean.
|
|
load_log = importlib.import_module("diet_guard._state").load_log
|
|
now_local = importlib.import_module("diet_guard._state").now_local
|
|
pull_peer_logs = importlib.import_module("diet_guard._sync_refresh").pull_peer_logs
|
|
|
|
reason = pull_peer_logs()
|
|
if reason is not None:
|
|
_logger.error("could not pull the shared log: %s", reason)
|
|
return 1
|
|
|
|
log = load_log()
|
|
if not log:
|
|
_logger.error("the merged log is empty; no peer holds this device's state")
|
|
return 1
|
|
|
|
newest = max(log)
|
|
age = (now_local().date() - date.fromisoformat(newest)).days
|
|
if age > _MAX_AGE_DAYS:
|
|
_logger.error("newest synced day is %s (%dd old)", newest, age)
|
|
return 1
|
|
|
|
_logger.info("a peer holds entries through %s (%dd old)", newest, age)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# The caller is a shell gate, so the reason must reach stderr, not vanish.
|
|
logging.basicConfig(level=logging.INFO, format="sync freshness: %(message)s")
|
|
sys.exit(main())
|