deps: earned_time 0.6.1 (tutor cutover; new-gate rungs spare the floor)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-10-10 08:24:48 +02:00
parent 3a2ad0245c
commit 2140ca5180
6 changed files with 42 additions and 21 deletions

View File

@ -134,7 +134,7 @@ repos:
# than merely an old one. # than merely an old one.
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
- earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.0#subdirectory=earned_time - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.1#subdirectory=earned_time
- freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays
- mcp>=2,<3 - mcp>=2,<3
- pydantic - pydantic
@ -155,7 +155,7 @@ repos:
# than merely an old one. # than merely an old one.
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
- earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.0#subdirectory=earned_time - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.1#subdirectory=earned_time
- freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays
- mcp>=2,<3 - mcp>=2,<3
- pydantic - pydantic

View File

@ -10,7 +10,7 @@ dependencies = [
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock", "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock",
"crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync",
"freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays", "freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays",
"earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.0#subdirectory=earned_time", "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.1#subdirectory=earned_time",
] ]
[project.scripts] [project.scripts]

View File

@ -4,7 +4,7 @@ bandit==1.9.4
codespell==2.4.3 codespell==2.4.3
coverage==7.16.2 coverage==7.16.2
crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.0#subdirectory=earned_time earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.6.1#subdirectory=earned_time
freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
mcp==2.3.0 mcp==2.3.0

View File

@ -199,8 +199,14 @@ class TestProjectionFollowsTheCut:
def base(day: date) -> int: def base(day: date) -> int:
# From the sleep ladder (Sat 2026-10-10) the registry prices the # From the sleep ladder (Sat 2026-10-10) the registry prices the
# day itself; before it, the cut from Tuesday holds. # day itself; before it, the cut from Tuesday holds.
# The registry in force that day (the tutor cutover switches it). # The registry in force that day (the tutor cutover switches it),
return earned_time.base_for(day, _earned.registry(day)).shutdown_minutes # and from today on its first credits: a never-paid tutor spares
# the floor (earned_time 0.6.1).
earners = _earned.registry(day)
starts = _earned.first_credits(earners, day)
return earned_time.base_for(
day, earners, first_credits=starts
).shutdown_minutes
week = [date(2026, 10, 5 + i) for i in range(7)] week = [date(2026, 10, 5 + i) for i in range(7)]
assert [base(d) for d in week[1:4]] == [lowered] * 3 assert [base(d) for d in week[1:4]] == [lowered] * 3

View File

@ -17,7 +17,7 @@ import earned_time
import pytest import pytest
from screen_locker._day import today_str from screen_locker._day import today_str
from screen_locker._earned import flat_earners, hhmm, registry from screen_locker._earned import first_credits, gate_earners, hhmm, registry
from screen_locker._shutdown_base import ( from screen_locker._shutdown_base import (
_apply_flat_bonus, _apply_flat_bonus,
apply_flat_bonuses_if_new, apply_flat_bonuses_if_new,
@ -70,12 +70,14 @@ class TestBaseHour:
minus every earner's first unit, not 20:00 minus the cuts, so the day minus every earner's first unit, not 20:00 minus the cuts, so the day
is priced by the registry and only the "stays lowered" part is ours. is priced by the registry and only the "stays lowered" part is ours.
The Anki waiver (``ANKI_WAIVED_FROM``) raises that floor to 19:50, so The Anki waiver (``ANKI_WAIVED_FROM``) raises that floor to 19:50, so
"lowered" means below the uncut 20:00, not at or below 19:00. "lowered" means below the uncut 20:00, not at or below 19:00; a gate
that never paid out (the tutor) spares it (earned_time 0.6.1).
""" """
later = date(2030, 1, 1) later = date(2030, 1, 1)
expected = earned_time.base_for(later, registry(later)).shutdown_minutes starts = first_credits(registry(later), later)
assert base_minutes(later) == expected base = earned_time.base_for(later, registry(later), first_credits=starts)
assert expected < base_minutes(date(2026, 9, 30)) assert base_minutes(later) == base.shutdown_minutes
assert base.shutdown_minutes < base_minutes(date(2026, 9, 30))
def test_default_is_the_local_today(self) -> None: def test_default_is_the_local_today(self) -> None:
assert base_minutes() == base_minutes(datetime.now().astimezone().date()) assert base_minutes() == base_minutes(datetime.now().astimezone().date())
@ -163,7 +165,7 @@ class TestResetIncludesReading:
reset_to_base_if_new_day(tmp_path / "state.json", _mixin()) reset_to_base_if_new_day(tmp_path / "state.json", _mixin())
base = base_minutes() base = base_minutes()
later = "".join( later = "".join(
f" + 0h {e.label}" for e in flat_earners() if e.name not in _NAMED f" + 0h {e.label}" for e in gate_earners() if e.name not in _NAMED
) )
assert ( assert (
f"Daily base reset: {hhmm(base + 60)} (base {hhmm(base)} + 0h workout + " f"Daily base reset: {hhmm(base + 60)} (base {hhmm(base)} + 0h workout + "

View File

@ -1,9 +1,10 @@
"""The Automation tutor on a simulated cutover: penalty start and grace floor. """The Automation tutor on a simulated cutover: penalty start and grace floor.
``earned_time.TUTOR_FROM`` is a far-future sentinel, so the cutover is ``earned_time.TUTOR_FROM`` is 2026-10-10 since earned_time 0.6.1; the
simulated on a fixed day: the registry's cutover (bound by value in fixture still pins the registry's cutover (bound by value in ``_registry`` and
``_registry`` and ``_ladder``) and the tutor's own ``penalty_from`` move to ``_ladder``) and the tutor's own ``penalty_from`` to :data:`_CUTOVER`, so the
:data:`_CUTOVER`. Nothing is written outside the suite's redirected home. suite does not move if a later release moves the cutover. Nothing is written
outside the suite's redirected home.
""" """
from __future__ import annotations from __future__ import annotations
@ -92,13 +93,25 @@ class TestNeverCredited:
assert wired.resolution.base.gaming_minutes == ( assert wired.resolution.base.gaming_minutes == (
unwired.resolution.base.gaming_minutes + cut unwired.resolution.base.gaming_minutes + cut
) )
# KNOWN GAP (earned_time 0.6.0): on the sleep ladder the shutdown
# floor is the ceiling minus every registered earner's full pay and def test_no_tutor_rung_off_the_shutdown_floor(self) -> None:
# never asks whether a penalty is in force, so a never-credited tutor """earned_time 0.6.1: a gate whose penalty has not started spares the floor.
# still costs its shutdown minutes. A fix there flips this assertion.
Unwired, the tutor's penalty starts at ``penalty_from`` and its full
rung (13+13+12+12) comes off the ladder floor; wired, the never-paid
tutor costs no shutdown either, so the floor stays at 19:50.
"""
answers = {e.name: 0 for e in registry(_CUTOVER)}
starts = first_credits(registry(_CUTOVER), _CUTOVER)
wired = derive(DayInputs(_CUTOVER, answers, first_credits=starts))
unwired = derive(DayInputs(_CUTOVER, answers))
tutor = earned_time.AUTOMATION_TUTOR
rung = tutor.shutdown_for(tutor.max_units or 1, _CUTOVER)
assert rung == 50
assert wired.resolution.base.shutdown_minutes == ( assert wired.resolution.base.shutdown_minutes == (
unwired.resolution.base.shutdown_minutes unwired.resolution.base.shutdown_minutes + rung
) )
assert wired.resolution.base.shutdown_minutes == 19 * 60 + 50
@pytest.mark.usefixtures("tutor") @pytest.mark.usefixtures("tutor")