mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 20:27:11 +01:00
Eighth batch of the 250-line sweep (over-cap count 28 -> 25). _state.py 366 -> 197 (+ _state_today.py, _state_sync.py) test_state.py 400 -> 221 (+ test_state_today.py, test_state_sync.py) _tk_fakes.py 368 -> 167 (+ _tk_fakes_containers.py, _tk_fakes_canvas.py) _state.py is the file conftest patches FOOD_LOG_FILE on, so the split is constrained rather than free: `FOOD_LOG_FILE` and the two functions that open it stay put, and the derived read model (_state_today) and the sync/undo helpers (_state_sync) reach the file only through _state's private _read_raw_log/_write_log. Callers were rewired to import from the new modules directly rather than via a circular re-export. That constraint is now enforced instead of merely documented. Moving a reader of FOOD_LOG_FILE into a sibling would leave conftest's patch pointing at a name nothing reads: check_patch_targets.py still exits 0 (the path resolves) and the runtime is unchanged (a file write, not a network call), so the suite would go green while writing to the real ~/.local/share/diet_guard and the next sync tick would publish it. test_state_redirect.py asserts both halves -- only _constants and _state may name the constant, and the redirect demonstrably moves a real write off the live path. Verified it fails closed by adding a FOOD_LOG_FILE reader to _state_today: it named that module exactly. _tk_fakes.py keeps _FAKE_TK/_FAKE_TTK and gains an explicit __all__, since the re-exports are load-bearing and ruff would otherwise strip them as unused. The identity canary from the previous commit covers the split: breaking one entry in the assembly (Canvas=object) fails 8 gate tests loudly rather than silently building real widgets. Verified: 706 Python tests / 100% branch coverage, check_patch_targets 0, pre-commit clean, ~/.local/share/diet_guard byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""Raw-log access, re-signing, and undo for the signed food log.
|
|
|
|
Split out of :mod:`._state` to keep both files under the repo's 250-line
|
|
limit. These are the *write*-side and sync-side helpers: unverified reads
|
|
(:func:`read_raw_log`), the merge write-back (:func:`write_raw_log`), the
|
|
per-entry re-sign the sync tick applies to every persisted entry
|
|
(:func:`resign_entry`), and the tombstoning undo.
|
|
|
|
They reach the file only through :mod:`._state`'s private ``_read_raw_log`` /
|
|
``_write_log``, which is where ``FOOD_LOG_FILE`` stays -- ``tests/conftest.py``
|
|
redirects the log by patching ``diet_guard._state.FOOD_LOG_FILE``, so a copy
|
|
of that constant here would be a patch the tests no longer reach. The names
|
|
are re-exported from :mod:`._state` for the existing call sites.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from gatelock.log_integrity import compute_entry_hmac
|
|
|
|
from diet_guard._state import _read_raw_log, _today, _write_log
|
|
|
|
if TYPE_CHECKING:
|
|
from diet_guard._state import DayLog
|
|
|
|
__all__ = [
|
|
"read_raw_log",
|
|
"resign_entry",
|
|
"undo_last_today",
|
|
"write_raw_log",
|
|
]
|
|
|
|
|
|
def read_raw_log() -> DayLog:
|
|
"""Return the log exactly as stored, including tombstoned/invalid entries.
|
|
|
|
Public counterpart of :func:`_read_raw_log`, for the sync orchestration
|
|
(:mod:`diet_guard._sync`), which must see tombstones to merge them (the
|
|
filtered :func:`load_log` drops them) and must not discard an entry that
|
|
fails verification just because a phone-origin copy was never signed.
|
|
"""
|
|
return _read_raw_log()
|
|
|
|
|
|
def write_raw_log(log: DayLog) -> None:
|
|
"""Persist ``log`` verbatim, overwriting the file on disk.
|
|
|
|
Public counterpart of :func:`_write_log`, for :mod:`diet_guard._sync` to
|
|
write back a merged log after re-signing it.
|
|
"""
|
|
_write_log(log)
|
|
|
|
|
|
def resign_entry(entry: dict[str, object]) -> dict[str, object]:
|
|
"""Return a copy of ``entry`` with a freshly computed ``hmac``.
|
|
|
|
Strips any existing signature first, mirroring :func:`undo_last_today`:
|
|
a signature computed on another device (or none, if the phone -- which
|
|
never holds the shared key -- produced this entry) cannot be trusted
|
|
as-is, and recomputing is the only way :func:`_entry_is_valid` will
|
|
accept it back on the next read. A no-op (signature-wise) when no HMAC
|
|
key is available locally, matching :func:`log_meal`'s degrade-gracefully
|
|
behavior.
|
|
|
|
Args:
|
|
entry: A log entry, signed or not.
|
|
|
|
Returns:
|
|
A new dict equal to ``entry`` except for its ``hmac`` field.
|
|
"""
|
|
resigned = dict(entry)
|
|
resigned.pop("hmac", None)
|
|
signature = compute_entry_hmac(resigned)
|
|
if signature is not None:
|
|
resigned["hmac"] = signature
|
|
return resigned
|
|
|
|
|
|
def undo_last_today() -> dict[str, object] | None:
|
|
"""Tombstone today's most recently logged, not-yet-undone entry.
|
|
|
|
Marks the entry ``deleted`` in place and re-signs it, rather than
|
|
physically removing it: a sync merge with another device only ever
|
|
*adds* entries it hasn't seen before, so a physical delete here would be
|
|
silently resurrected the next time that device's stale copy is pulled
|
|
back in. The tombstone travels with the entry instead, and every reader
|
|
(:func:`load_log`, the food-bank rebuild) already skips it.
|
|
|
|
Operates on the raw log so a mistaken entry can always be undone, even
|
|
one that would not pass verification.
|
|
|
|
Returns:
|
|
The tombstoned entry, or None if nothing undoable was logged today.
|
|
"""
|
|
log = _read_raw_log()
|
|
today = _today()
|
|
entries = log.get(today)
|
|
if not entries:
|
|
return None
|
|
for entry in reversed(entries):
|
|
if entry.get("deleted"):
|
|
continue
|
|
entry["deleted"] = True
|
|
entry.pop("hmac", None)
|
|
signature = compute_entry_hmac(entry)
|
|
if signature is not None:
|
|
entry["hmac"] = signature
|
|
_write_log(log)
|
|
return entry
|
|
return None
|