mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:07:15 +01:00
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
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
124 lines
4.5 KiB
Python
124 lines
4.5 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""Demo: what the tutor cutover pays, read from a signed temp ledger.
|
|
|
|
Writes tutor block rows in the exact contract the Automation tutor writes,
|
|
signs them with a throwaway key, counts them with the shared reader
|
|
(``credit_units``) and resolves the day. Prints gaming minutes and shutdown
|
|
for the day before ``TUTOR_FROM`` and for ``TUTOR_FROM`` itself with 0-5
|
|
blocks and a tampered row. Touches no real ledger, key or schedule.
|
|
|
|
Run: ``PYTHONPATH=earned_time python3 earned_time/scripts/demo_tutor_cutover.py``
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, time, timedelta
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
|
|
import earned_time
|
|
from earned_time import AUTOMATION_TUTOR, TUTOR_FROM, credit_units, entry_signature
|
|
|
|
_KEY = b"demo-key-not-the-real-one"
|
|
_OTHERS = {"workout": 1, "leetcode": 1, "reading": 1}
|
|
_NONE = {"workout": 0, "leetcode": 0, "reading": 0}
|
|
|
|
|
|
def _say(line: str) -> None:
|
|
sys.stdout.write(line + "\n")
|
|
|
|
|
|
def _hhmm(minutes: int) -> str:
|
|
return f"{minutes // 60:02d}:{minutes % 60:02d}"
|
|
|
|
|
|
def _block(session: str, number: int, day: date) -> dict[str, object]:
|
|
start = datetime.combine(day, time(18)).astimezone()
|
|
ended = (start + timedelta(minutes=15 * number)).timestamp()
|
|
row: dict[str, object] = {
|
|
"kind": "credit",
|
|
"entry_id": f"{session}-b{number}",
|
|
"day": day.isoformat(),
|
|
"created_at": ended + 1,
|
|
"detail": {
|
|
"session_id": session,
|
|
"block": number,
|
|
"active_seconds": 900,
|
|
"ended_at": ended,
|
|
"checks_passed": 3,
|
|
"checks_total": 3,
|
|
},
|
|
}
|
|
return {**row, "hmac": entry_signature(row, _KEY)}
|
|
|
|
|
|
def _units(folder: Path, rows: list[dict[str, object]], day: date) -> int:
|
|
ledger = folder / "ledger.json"
|
|
ledger.write_text(json.dumps({"entries": rows}), encoding="utf-8")
|
|
key = folder / "hmac.key"
|
|
key.write_bytes(_KEY)
|
|
units = credit_units(AUTOMATION_TUTOR, ledger, key, day)
|
|
if units is None:
|
|
msg = "demo ledger unreadable"
|
|
raise RuntimeError(msg)
|
|
return units
|
|
|
|
|
|
def _line(label: str, answers: dict[str, int], day: date) -> str:
|
|
res = earned_time.resolve(answers, day)
|
|
names = {t.earner.name for t in res.terms}
|
|
if "automation" in names:
|
|
term = res.term("automation")
|
|
paid = f"automation +{term.gaming_minutes}g/+{term.shutdown_minutes}s"
|
|
else:
|
|
paid = "automation waived" # ANKI_WAIVED_FROM..TUTOR_FROM
|
|
return (
|
|
f"{label:<44} {res.gaming_minutes:>4} min ({res.gaming_minutes / 60:.2f} h)"
|
|
f" {_hhmm(res.shutdown_minutes)}"
|
|
f" [{paid},"
|
|
f" base {res.base.gaming_minutes}g/{_hhmm(res.base.shutdown_minutes)}]"
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
"""Print the table."""
|
|
eve = TUTOR_FROM - timedelta(days=1)
|
|
_say(f"TUTOR_FROM = {TUTOR_FROM} (no first_credits: an unwired consumer)")
|
|
_say(f"{'case':<44} {'gaming':>15} shutdown")
|
|
for done in (0, 1):
|
|
answers = {**_OTHERS, "anki": done, "automation": done}
|
|
_say(_line(f"{eve} eve: others + anki/automation={done}", answers, eve))
|
|
nothing = {**_NONE, "anki": 0, "automation": 0}
|
|
_say(_line(f"{eve} eve: nothing done", nothing, eve))
|
|
with tempfile.TemporaryDirectory() as raw:
|
|
folder = Path(raw)
|
|
for blocks in (0, 1, 2, 4, 5):
|
|
rows = [_block("s1", n, TUTOR_FROM) for n in range(1, blocks + 1)]
|
|
units = _units(folder, rows, TUTOR_FROM)
|
|
for others, tag in ((_OTHERS, "+ all others"), (_NONE, "tutor only")):
|
|
answers = {**others, "automation": units}
|
|
label = f"{TUTOR_FROM}: {blocks} rows -> {units} units, {tag}"
|
|
_say(_line(label, answers, TUTOR_FROM))
|
|
rows = [_block("s2", n, TUTOR_FROM) for n in range(1, 5)]
|
|
detail = rows[2]["detail"]
|
|
if not isinstance(detail, dict):
|
|
msg = "demo row lost its detail"
|
|
raise TypeError(msg)
|
|
rows[2]["detail"] = {**detail, "active_seconds": 9000} # edited after signing
|
|
units = _units(folder, rows, TUTOR_FROM)
|
|
label = f"{TUTOR_FROM}: 4 rows, 1 tampered -> {units} units"
|
|
_say(_line(label, {**_OTHERS, "automation": units}, TUTOR_FROM))
|
|
anki = {**_OTHERS, "anki": 1, "automation": 4}
|
|
_say(
|
|
_line(
|
|
f"{TUTOR_FROM}: all + a stale anki answer (ignored)", anki, TUTOR_FROM
|
|
)
|
|
)
|
|
_say(f"gaming ceiling {earned_time.GAMING_CEILING_MINUTES} min (8 h)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|