Adds the diagnostic substrate the rest of the hardening work builds on. A
6am failure should be reconstructable afterwards instead of re-debugged.
Two sinks, deliberately. stderr -> journal is unchanged. Alongside it, one
JSON object per line in ~/.local/state/wake_alarm/events.jsonl, so a morning
can be replayed with jq.
The hard constraint is that _trigger is a fresh process every 60 seconds, so
an in-memory rate limiter would reset on every tick and suppress nothing.
_log_dedupe therefore persists the last emitted fingerprint to disk and
flushes a suppressed_repeats count onto the next distinct record. Verified
across separate processes: 4 invocations logging the same routine event
produce 1 record plus an accurate count of 3.
That is what satisfies the standing "do not raise the level of _sync's
degraded-read paths" rule by construction, rather than by keeping levels
artificially low and losing the signal.
The correlation id is derived, never generated: both halves compute
wake-YYYYMMDD-HHMM from the resolved alarm instant, so the PC and the phone
agree with no handshake and no uuid exchange -- which matters precisely when
sync is down.
Logging never raises. An unwritable sink degrades to journal-only.
_alarm.py and _sync.py keep the default component= for now; tagging them
means splitting them for the 250-line gate, which belongs in the phases that
actually restructure those files.
389 tests pass, 100% branch coverage held.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh