screen-locker/scripts/verify_workout_credited.py
Krzysztof kuhy Rudnicki f4850670ee Make the recovery scripts say why they failed, not just print it
The three helper scripts were only ever dry-run, so the no-silent-failures hook
never saw them until they were committed. Six handlers reported the reason to
stdout via _report() and nothing else — invisible to the log, and easy to miss
when the script runs as one step of finish_2026_08_24.sh amid a wall of other
output. These mutate benefit and sick-day state, so a quiet failure there reads
as success.

Log at error/warning alongside the stdout line, saying what was not done:
the bonus was not banked, the revocation was not persisted, the credit gate
cannot tell and so fails closed.

Verified by feeding the gate a corrupt log: it now logs the reason with a
traceback and exits 1, instead of printing one line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJmPLyT8NU4DzDJPV1KEX5
2026-08-24 13:10:53 +02:00

102 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail unless a counted workout is on disk for a given date.
A gate, not a report. The 2026-08-24 recovery revokes a sick day and re-arms
enforcement on the strength of "the workout was ingested" -- so that claim has
to be checked by code before the irreversible steps run, not asserted by
whoever is watching the output scroll past.
Exits non-zero when the date has no counted workout, which stops the calling
script before it revokes anything.
Reads ``log.json`` directly rather than importing ``screen_locker``, matching
``add_scheduled_skip.py``: these scripts must keep working even when the
package itself is mid-edit.
Usage:
python3 scripts/verify_workout_credited.py --date 2026-08-24
"""
from __future__ import annotations
import argparse
import json
import logging
from pathlib import Path
import sys
LOG_FILE = Path(__file__).resolve().parent.parent / "screen_locker" / "log.json"
# Mirrors ``_weekly_check.COUNTED_WORKOUT_TYPES``. Duplicated deliberately: a
# gate that imports the code it is checking cannot fail independently of it.
COUNTED_WORKOUT_TYPES = frozenset(
{
"phone_verified",
"runnerup_verified",
"pc_workout_verified",
"manual_workout",
}
)
# 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")
def main() -> int:
"""Report whether ``--date`` holds a counted workout; non-zero if not."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--date", required=True, help="YYYY-MM-DD to check")
parser.add_argument("--log-file", default=str(LOG_FILE))
args = parser.parse_args()
log_path = Path(args.log_file)
if not log_path.is_file():
_report(f"FAIL: no workout log at {log_path}")
return 1
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
entries = log.get(args.date) or []
counted = [
entry
for entry in entries
if (entry.get("workout_data") or {}).get("type") in COUNTED_WORKOUT_TYPES
]
if not counted:
_report(f"FAIL: no counted workout logged for {args.date}.")
_report(f" {len(entries)} entr(ies) exist for that date, none counted.")
_report(" Nothing to revoke a sick day against — stopping here so")
_report(" enforcement is not re-armed against a log that cannot see")
_report(" the workout you actually did.")
return 1
for entry in counted:
data = entry.get("workout_data") or {}
_report(
f"OK: {args.date} has {data.get('type')} "
f"(id={entry.get('workout_id')}, source={data.get('source', 'n/a')})"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())