mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
earned_time 0.20.0: focus day cuts the full 180
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
earned_time pre-commit / pre-commit (push) Waiting to run
earned_time tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sickdays pre-commit / pre-commit (push) Waiting to run
sickdays tests / test (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
earned_time pre-commit / pre-commit (push) Waiting to run
earned_time tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sickdays pre-commit / pre-commit (push) Waiting to run
sickdays tests / test (push) Waiting to run
A focus-project day cuts the whole 180 of gaming base (LeetCode's free hour too); a full day still earns it all back. Shutdown unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWxdzckyBH7fEbPywpr
This commit is contained in:
parent
213537d0ef
commit
98c013ea3f
@ -189,14 +189,14 @@ How far to trust a gate is computed from its ledger, never listed by name:
|
|||||||
`workout_match` counts a `runnerup_tcx` row by `completed_at`, a `rest_day` row
|
`workout_match` counts a `runnerup_tcx` row by `completed_at`, a `rest_day` row
|
||||||
on its `day` only if `detail.declared_at` predates that day (else logged, no).
|
on its `day` only if `detail.declared_at` predates that day (else logged, no).
|
||||||
|
|
||||||
## The focus project (0.19.0)
|
## The focus project (0.19.0, cut 180 since 0.20.0)
|
||||||
|
|
||||||
While `focus_project.active(day)` returns a project, `earners_for` drops the
|
While `focus_project.active(day)` returns a project, `earners_for` drops the
|
||||||
gates it `replaces` (`book` = `reading`) and adds one counted `focus` earner
|
gates it `replaces` (`book` = `reading`) and adds one counted `focus` earner
|
||||||
(`max_units` = the day's `daily_on`) paying what they paid (shutdown by their rungs,
|
(`max_units` = the day's `daily_on`) paying what they paid (shutdown by their rungs,
|
||||||
gaming by `max_gaming_minutes`: 60+60+60 over 120 min = 1.5/min):
|
gaming by `max_gaming_minutes`: 60+60+60 over 120 min = 1.5/min):
|
||||||
`floor(summed minutes * pay / daily_minutes)`, never per row. The penalty runs
|
`floor(summed minutes * pay / daily_minutes)`, never per row. The penalty runs
|
||||||
from the start day, cutting what the replaced gates cost; maturity never delays it. Ledger `~/.local/share/focus_project/ledger.json`:
|
from the start day, cutting all of it, 180 with LeetCode's formerly free hour (0.20.0); maturity never delays it. Ledger `~/.local/share/focus_project/ledger.json`:
|
||||||
signed `credit` rows, `entry_id` `<project>-<YYYYMMDD>-m<n>`, `detail.minutes` (positive int),
|
signed `credit` rows, `entry_id` `<project>-<YYYYMMDD>-m<n>`, `detail.minutes` (positive int),
|
||||||
`detail.ended_at` (else `day`). `focus_project` unusable = no project (logged).
|
`detail.ended_at` (else `day`). `focus_project` unusable = no project (logged).
|
||||||
|
|
||||||
@ -243,7 +243,7 @@ signed `credit` rows, `entry_id` `<project>-<YYYYMMDD>-m<n>`, `detail.minutes` (
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```console
|
```console
|
||||||
pip install --user --break-system-packages --no-deps "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.19.0#subdirectory=earned_time"
|
pip install --user --break-system-packages --no-deps "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.20.0#subdirectory=earned_time"
|
||||||
```
|
```
|
||||||
|
|
||||||
The root `steam-backlog-enforcer.service` sets `HOME=/home/kuhy` and imports from
|
The root `steam-backlog-enforcer.service` sets `HOME=/home/kuhy` and imports from
|
||||||
|
|||||||
@ -31,11 +31,9 @@ _warned: set[str] = set()
|
|||||||
class FocusEarner(Earner):
|
class FocusEarner(Earner):
|
||||||
"""Counted; ``gaming_minutes``/``shutdown_minutes`` pay a FULL day.
|
"""Counted; ``gaming_minutes``/``shutdown_minutes`` pay a FULL day.
|
||||||
|
|
||||||
``cut_gaming_minutes``: the base cut, what the replaced penalised gates cost.
|
The gaming base cut is all of it, LeetCode's free hour too (kuhy 2026-11-02).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cut_gaming_minutes: int = 0
|
|
||||||
|
|
||||||
def gaming_for(self, units: int) -> int:
|
def gaming_for(self, units: int) -> int:
|
||||||
"""Gaming minutes of ``units`` focused minutes, floored."""
|
"""Gaming minutes of ``units`` focused minutes, floored."""
|
||||||
return self.capped(units) * self.gaming_minutes // (self.max_units or 1)
|
return self.capped(units) * self.gaming_minutes // (self.max_units or 1)
|
||||||
@ -73,9 +71,6 @@ def focus_earner(project: Any, dropped: tuple[Earner, ...], day: date) -> Earner
|
|||||||
name="focus",
|
name="focus",
|
||||||
label="Focus",
|
label="Focus",
|
||||||
gaming_minutes=sum(e.max_gaming_minutes for e in dropped),
|
gaming_minutes=sum(e.max_gaming_minutes for e in dropped),
|
||||||
cut_gaming_minutes=sum(
|
|
||||||
e.max_gaming_minutes for e in dropped if e.penalty_from is not None
|
|
||||||
),
|
|
||||||
shutdown_minutes=sum(
|
shutdown_minutes=sum(
|
||||||
shutdown_units_minutes(e, e.max_units or 1, day) for e in dropped
|
shutdown_units_minutes(e, e.max_units or 1, day) for e in dropped
|
||||||
),
|
),
|
||||||
|
|||||||
@ -25,7 +25,6 @@ from datetime import UTC, date, datetime
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from earned_time._focus import FocusEarner
|
|
||||||
from earned_time._ladder import on_ladder, shutdown_ceiling_for
|
from earned_time._ladder import on_ladder, shutdown_ceiling_for
|
||||||
from earned_time._leisure import base_minutes, ceiling_minutes, is_leisure
|
from earned_time._leisure import base_minutes, ceiling_minutes, is_leisure
|
||||||
from earned_time._maturity import confirmed_start, penalty_start
|
from earned_time._maturity import confirmed_start, penalty_start
|
||||||
@ -46,16 +45,6 @@ def _today() -> date:
|
|||||||
return datetime.now(tz=UTC).astimezone().date()
|
return datetime.now(tz=UTC).astimezone().date()
|
||||||
|
|
||||||
|
|
||||||
def _gaming_cut(item: Earner) -> int:
|
|
||||||
"""The gaming base cut of a penalised earner: what it can pay.
|
|
||||||
|
|
||||||
A focus earner cuts only what the gates it replaced cost (LeetCode's was free).
|
|
||||||
"""
|
|
||||||
if isinstance(item, FocusEarner):
|
|
||||||
return item.cut_gaming_minutes
|
|
||||||
return item.max_gaming_minutes
|
|
||||||
|
|
||||||
|
|
||||||
def _penalised(
|
def _penalised(
|
||||||
item: Earner, day: date, first_credits: Mapping[str, date | None] | None
|
item: Earner, day: date, first_credits: Mapping[str, date | None] | None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@ -128,7 +117,7 @@ def _base(
|
|||||||
penalised = [e for e in registry if _penalised(e, target, first_credits)]
|
penalised = [e for e in registry if _penalised(e, target, first_credits)]
|
||||||
return Base(
|
return Base(
|
||||||
gaming_minutes=base_minutes(leisure=on)
|
gaming_minutes=base_minutes(leisure=on)
|
||||||
- sum(_gaming_cut(e) for e in penalised),
|
- sum(e.max_gaming_minutes for e in penalised),
|
||||||
shutdown_minutes=_shutdown_floor(target, registry, penalised),
|
shutdown_minutes=_shutdown_floor(target, registry, penalised),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -92,28 +92,41 @@ def test_many_small_rows_pay_what_one_row_pays(
|
|||||||
def test_resolve_prices_base_terms_and_ceiling(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_resolve_prices_base_terms_and_ceiling(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
activate(monkeypatch, project())
|
activate(monkeypatch, project())
|
||||||
nothing = resolve({"workout": 0, "focus": 0}, DAY, leisure=False)
|
nothing = resolve({"workout": 0, "focus": 0}, DAY, leisure=False)
|
||||||
assert nothing.base.gaming_minutes == 300 - 120
|
assert nothing.base.gaming_minutes == 300 - 180
|
||||||
assert nothing.base.shutdown_minutes == 18 * 60
|
assert nothing.base.shutdown_minutes == 18 * 60
|
||||||
half = resolve({"workout": 0, "focus": 60}, DAY, leisure=False)
|
half = resolve({"workout": 0, "focus": 60}, DAY, leisure=False)
|
||||||
assert half.gaming_minutes == 180 + 90
|
assert half.gaming_minutes == 120 + 90
|
||||||
assert half.shutdown_minutes == 18 * 60 + 90
|
assert half.shutdown_minutes == 18 * 60 + 90
|
||||||
full = resolve({"workout": 1, "focus": 120}, DAY, leisure=False)
|
full = resolve({"workout": 1, "focus": 120}, DAY, leisure=False)
|
||||||
assert full.shutdown_minutes == 23 * 60
|
assert full.shutdown_minutes == 23 * 60
|
||||||
|
|
||||||
|
|
||||||
def test_a_project_day_is_neutral_against_the_gates_it_replaced(
|
def test_a_full_project_day_pays_what_the_replaced_gates_paid(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Shutdown matches the old full day; gaming loses LeetCode's free hour."""
|
||||||
|
activate(monkeypatch, project())
|
||||||
|
done = {"workout": 1, "leetcode": 1, "reading": 1, "tutor": 60, "focus": 120}
|
||||||
|
old = resolve(done, DAY, TUTOR_EARNERS, leisure=False)
|
||||||
|
new = resolve(done, DAY, leisure=False)
|
||||||
|
assert new.shutdown_minutes == old.shutdown_minutes
|
||||||
|
# kuhy 2026-11-02: LeetCode's bonus hour was free on top of the base; the
|
||||||
|
# project day cuts it and pays it back, so the full day nets 60 less.
|
||||||
|
assert new.gaming_minutes == old.gaming_minutes - 60
|
||||||
|
unsolved = resolve({**done, "leetcode": 0}, DAY, TUTOR_EARNERS, leisure=False)
|
||||||
|
assert new.gaming_minutes == unsolved.gaming_minutes
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_idle_project_day_also_cuts_the_free_leetcode_hour(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""kuhy 2026-11-02: a project day cuts all it can pay (180), not 120."""
|
||||||
activate(monkeypatch, project())
|
activate(monkeypatch, project())
|
||||||
zero = {"workout": 0, "leetcode": 0, "reading": 0, "tutor": 0, "focus": 0}
|
zero = {"workout": 0, "leetcode": 0, "reading": 0, "tutor": 0, "focus": 0}
|
||||||
done = {"workout": 1, "leetcode": 1, "reading": 1, "tutor": 60, "focus": 120}
|
old = resolve(zero, DAY, TUTOR_EARNERS, leisure=False)
|
||||||
for answers in (zero, done):
|
new = resolve(zero, DAY, leisure=False)
|
||||||
old = resolve(answers, DAY, TUTOR_EARNERS, leisure=False)
|
assert new.gaming_minutes == old.gaming_minutes - 60
|
||||||
new = resolve(answers, DAY, leisure=False)
|
assert new.shutdown_minutes == old.shutdown_minutes
|
||||||
assert (new.gaming_minutes, new.shutdown_minutes) == (
|
|
||||||
old.gaming_minutes,
|
|
||||||
old.shutdown_minutes,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_the_penalty_is_not_delayed_by_the_first_credit(
|
def test_the_penalty_is_not_delayed_by_the_first_credit(
|
||||||
@ -125,7 +138,7 @@ def test_the_penalty_is_not_delayed_by_the_first_credit(
|
|||||||
plain = resolve(answers, START, leisure=False)
|
plain = resolve(answers, START, leisure=False)
|
||||||
paid = resolve(answers, START, first_credits={"focus": START}, leisure=False)
|
paid = resolve(answers, START, first_credits={"focus": START}, leisure=False)
|
||||||
assert paid.base == plain.base
|
assert paid.base == plain.base
|
||||||
assert plain.base.gaming_minutes == 300 - 120
|
assert plain.base.gaming_minutes == 300 - 180
|
||||||
|
|
||||||
|
|
||||||
def test_maturity_reads_the_focus_ledger(
|
def test_maturity_reads_the_focus_ledger(
|
||||||
|
|||||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "earned-time"
|
name = "earned-time"
|
||||||
version = "0.19.1"
|
version = "0.20.0"
|
||||||
description = "Penalty-then-reward registry: what every gate app earns in gaming time and shutdown time"
|
description = "Penalty-then-reward registry: what every gate app earns in gaming time and shutdown time"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user