diff --git a/scripts/restore_and_bonus.py b/scripts/restore_and_bonus.py index 9abe3ee..583cb31 100755 --- a/scripts/restore_and_bonus.py +++ b/scripts/restore_and_bonus.py @@ -28,6 +28,7 @@ from __future__ import annotations import argparse from datetime import datetime import json +import logging from pathlib import Path import sys @@ -37,6 +38,12 @@ EXTRA_BENEFITS_FILE = REPO_ROOT / "screen_locker" / "extra_benefits_state.json" DEFAULT_BONUS_HOURS = 2 +# Failures go to the log (stderr) as well as stdout: this script runs as one +# step of finish_2026_08_24.sh, where a bare stdout line scrolls past in a wall +# of other output, and it mutates benefit state — the exact case where a quiet +# failure looks like success. +_logger = logging.getLogger(__name__) + def _report(message: str) -> None: """Write one line to stdout (matches the other scripts in this directory).""" @@ -57,6 +64,7 @@ def _load(path: Path) -> dict: try: return json.loads(path.read_text()) except (OSError, json.JSONDecodeError) as exc: + _logger.warning("Could not read %s: %s — treating as empty", path, exc) _report(f"WARN: could not read {path}: {exc} — treating as empty") return {} @@ -119,6 +127,10 @@ def main() -> int: try: Path(args.benefits_file).write_text(json.dumps(benefits, indent=2) + "\n") except OSError as exc: + _logger.exception( + "Could not write %s — the bonus was NOT banked", + args.benefits_file, + ) _report(f"FAIL: could not write {args.benefits_file}: {exc}") return 1 diff --git a/scripts/revoke_sick_day.py b/scripts/revoke_sick_day.py index acb170d..1dcb0ac 100755 --- a/scripts/revoke_sick_day.py +++ b/scripts/revoke_sick_day.py @@ -24,6 +24,7 @@ from __future__ import annotations import argparse from datetime import datetime, timezone import json +import logging from pathlib import Path import sys @@ -31,6 +32,11 @@ REPO_ROOT = Path(__file__).resolve().parent.parent SICK_HISTORY_FILE = REPO_ROOT / "screen_locker" / "sick_history.json" LOG_FILE = REPO_ROOT / "screen_locker" / "log.json" +# Failures are logged as well as printed: this script rewrites sick-day +# history, so a read/write error that only reaches stdout can leave the +# revocation half-applied while the surrounding script reports success. +_logger = logging.getLogger(__name__) + def _report(message: str) -> None: """Write one line to stdout (matches the other scripts in this directory).""" @@ -112,6 +118,7 @@ def main() -> int: try: history = json.loads(history_path.read_text()) except (OSError, json.JSONDecodeError) as exc: + _logger.exception("Could not read %s — no sick day was revoked", history_path) _report(f"FAIL: could not read {history_path}: {exc}") return 1 @@ -119,6 +126,10 @@ def main() -> int: try: log = json.loads(log_path.read_text()) if log_path.is_file() else {} except (OSError, json.JSONDecodeError) as exc: + _logger.exception( + "Could not read %s — cannot cite a workout id, so nothing was revoked", + log_path, + ) _report(f"FAIL: could not read {log_path}: {exc}") return 1 @@ -133,6 +144,10 @@ def main() -> int: try: history_path.write_text(json.dumps(history, indent=2) + "\n") except OSError as exc: + _logger.exception( + "Could not write %s — the revocation was NOT persisted", + history_path, + ) _report(f"FAIL: could not write {history_path}: {exc}") return 1 diff --git a/scripts/verify_workout_credited.py b/scripts/verify_workout_credited.py index 0aec9ee..8c2622c 100755 --- a/scripts/verify_workout_credited.py +++ b/scripts/verify_workout_credited.py @@ -21,6 +21,7 @@ from __future__ import annotations import argparse import json +import logging from pathlib import Path import sys @@ -38,6 +39,12 @@ COUNTED_WORKOUT_TYPES = frozenset( ) +# This script is a gate: it decides whether the rest of the recovery may +# proceed. A read failure that only reaches stdout can be mistaken for a plain +# "not credited" verdict, so it is logged loudly as well. +_logger = logging.getLogger(__name__) + + def _report(message: str) -> None: """Write one line to stdout (matches the other scripts in this directory).""" sys.stdout.write(f"{message}\n") @@ -58,6 +65,11 @@ def main() -> int: try: log = json.loads(log_path.read_text()) except (OSError, json.JSONDecodeError) as exc: + _logger.exception( + "Could not read %s — cannot tell whether the workout was " + "credited, so this gate fails closed", + log_path, + ) _report(f"FAIL: could not read {log_path}: {exc}") return 1