sickdays: per-date pool, no cap or reason, add/remove today or later only,
import_legacy for the migration. LWW state flip per date (no tombstones),
Firebase RTDB sickdays-sync/devices/<uuid>/sick_days.json, HMAC audit trail.
Readers fail closed (unreadable pool = not sick, logged); writers refuse a
corrupt pool. Free-day cross-check via a lazily imported freedays predicate.
earned_time: resolve(..., sick=True) floors the final shutdown at 20:00
(capped at the ceiling); gaming, terms and base unchanged. Result dataclasses
moved to _resolution.py for the line cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
From TUTOR_QUOTA_FROM (2026-10-12) credit_units scales the tutor's deduped
day minute sum to min(60, minutes * 60 // quota); reward and penalty are
unchanged, every quota <= 60 so the switch is raise-only. Earlier days pay
one unit per minute as before. WORKDAYS is a literal copy of freedays'
(importing freedays pulls crdt-sync; earned_time is stdlib only), guarded by
a drift test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E57caQF4KXwEvqEnKtJiHM
Rows carry no earner name, so only the ledger path moves; the tutor copies
its data dir (old rows included). Pre-cutover AUTOMATION (anki quota),
LADDER and the waiver are unchanged. Same minutes, cap, ladder, TUTOR_FROM
and confirmed_on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YADHKXzZhhePzQ2rNoXFcK
AUTOMATION_TUTOR: 1 gaming min per unit (minute), max_units 60; TUTOR_LADDER
automation Rung(1, extra=1), rungs still sum 250 (23:00 / 18:50).
credit_units sums each row's units (ROW_UNITS: tutor detail.minutes, a row
without it = 15); a repeated entry_id pays once at its smallest count; an
invalid minutes field is refused by tutor_match. An all-block day resolves
exactly as 0.7.0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9cLaZkZaH7Zi2K63uWuoB
TUTOR_FROM = 2026-10-10 (the day after the deploy). The automation tutor
keeps confirmed_on=None, so it stays "new".
Ladder floor: a rung comes off the floor only for a pure bonus (no
penalty_from) or an earner whose penalty has started -- the same
_penalised() rule (first_credits -> penalty_start, confirmed_on fail-closed
fallback, penalty_from without first_credits) the gaming base uses. Before
this, a never-credited tutor would have lowered 2026-10-10's floor by its
full 50-minute rung (19:50 -> 19:00) even with first_credits. Maturity can
only raise time: new property test against the 0.6.0 floor formula.
ruff 0.17.0 (dependency-freshness): CPY001 ignored -- personal repo, no
copyright policy.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdfmsGMht9ypyvxGPmNF1V
CI's earned_time pre-commit flagged tests/_maturity_rows.py: the local
--all-files run predated git tracking of the new files. Same exclude as
steam-backlog-enforcer's tests/_*.py helpers. No package code changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdfmsGMht9ypyvxGPmNF1V
How far to trust a gate is computed from its signed ledger: new / maturing /
mature (>= 14 verified credit days, first >= 21 days old), with a read-only
CLI table. Only signed, non-manual, paying credits count; a gate's own
re-evaluation grant (detail.grant_of, book-guard bonus: rows) counts at
amount 0.
Earner.confirmed_on (hand-set) is new. penalty_start = max(penalty_from, day
after first real credit, day after confirmed_on); resolve/base_for take an
optional first_credits map and can only delay a penalty, never add one.
Omitted, every day resolves exactly as in 0.5.0.
Fail closed: a gate with confirmed_on and no credit on record (unreadable
key/ledger, deleted or emptied ledger) falls back to max(penalty_from, day
after confirmed_on), so deleting a ledger cannot lift a penalty.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdfmsGMht9ypyvxGPmNF1V
From LADDER_FROM (2026-10-10) shutdown is read down from sleep: ceiling =
WAKE_MINUTES (07:00) - 8h = 23:00, floor = ceiling - every earner's rung
(workout 110, LeetCode 50, reading 30, Anki 25, Automation 25) = 19:00.
A second workout earns nothing more. Earlier days resolve exactly as in
0.3.0 (frozen 23:00 ceiling); gaming is unchanged for every input.
WORKOUT gets a signed ledger (~/.local/share/workout_locker/ledger.json,
written by screen-locker) and workout_match; a rest_day row counts only if
declared before its day began. New API: first_credit_at, credit_units,
day_window, shutdown_minutes_for, extra_shutdown_minutes_for,
shutdown_ceiling_for, on_ladder, LADDER/Rung, LADDER_FROM, WAKE_MINUTES.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrjUTo1yhgAsUUasdw4bpG