wake-alarm/DOCS-architecture.md
Krzysztof kuhy Rudnicki 82abbefa01 Add the continuation prompt for the remaining hardening phases
The PC shut down mid-session. This is the entry point for picking the work
back up without the transcript: what is done, what is left in what order, the
decisions already settled, and the traps that actually cost time.

Points at ~/.claude/plans/wake-alarm-still-does-witty-toucan.md for detail
rather than duplicating it, so the two cannot drift apart.

Every factual claim in it was re-checked against the tree, not written from
memory -- including the one that matters most: _reactivate.py is finished and
tested but is called by NOTHING (it appears only in a _startup.py docstring),
so item 6 is not actually fixed at runtime yet. That is step 1 for the next
session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
2026-08-27 20:05:14 +02:00

46 lines
2.2 KiB
Markdown

# wake_alarm architecture
Reference for the module layout after the 2026-08-26 split, and the two
failure modes that motivated it.
## Where behaviour lives
`_alarm.py` owns the window and the dismiss flow and nothing else. When looking
for behaviour, start here:
| Question | Module |
|---|---|
| Should the alarm run at all today? | `_gating` (no time gate — that is `_trigger`) |
| What should it sound like right now? | `_escalation` (pure), driven by `_beep_loop` |
| Why did it go quiet? | `_escalation` — the failsafe at `FAILSAFE_SILENCE_SECONDS` |
| Why is there no sound on HDMI? | `_sinks` + `_reactivate` (see below) |
| Challenge presentation / rounds | `_rounds`, `_countdown`, `_dismiss_view` |
| Recurring timers | `_alarm_timers` (a mixin; they reschedule via `root.after`) |
| Screen flash | `_screen_flash` (~1.3 Hz, deliberately under the 3 Hz threshold) |
| Startup hardware | `_startup` (display, brightness, plug + pre-roll) |
| Structured logs | `_logging_setup`, `_log_sink`, `_log_dedupe`, `_correlation` |
### The audio failure this fixed
`_activate_all_outputs()` ran **once**, at startup, and `_parse_profile_line`
drops any profile reporting `available: no`. At wake time the monitor has not
finished its HDMI handshake, so the profile is skipped — and while a card's
profile is `off` its sink **does not exist**, so `_play_on_all_sinks`'s
per-cycle re-enumeration can never find it. `_reactivate` re-runs activation on
a decaying schedule and logs the enumerated sink list each pass.
A **powered-off speaker is not detectable from software** — the analog sink is
present and `IDLE` either way. That is a hardware problem; see
`DOCS-speaker-recommendation.md`.
### Timing constants that must not collide
`FAILSAFE_SILENCE_SECONDS` (300) silences **audio only**; the lock, challenge
and workout gate persist. Two invariants are locked in by tests in
`test_escalation.py`, because breaking either silently guts the alarm:
- `PHASE_MEDIUM_END * 60 < FAILSAFE_SILENCE_SECONDS` — otherwise the escalation
never leaves its softest rung before going quiet.
- `ESCAPE_OFFER_AFTER_SECONDS` (900) `> FAILSAFE_SILENCE_SECONDS` — otherwise
the escape hatch appears exactly as the alarm stops being annoying.