utils/focus_project
Krzysztof kuhy Rudnicki 17e300af0a
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
focus-project pre-commit / pre-commit (push) Waiting to run
focus-project tests / test (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
focus_project: pin backing edge cases and pay
Tests only: sql_drills/leetcode beats back, other-project and browser
beats don't, submissions back only leetcode projects, the cutover
midnight, a tutor chain leaves earlier reading unbacked; earned_time
pays 0 units for a timer-only post-cutover day (real focus_project).

Todo: fda71ed1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 16:38:48 +01:00
..
focus_project focus_project: pin backing edge cases and pay 2026-11-10 16:38:48 +01:00
systemd focus_project: minutes, ledger, harvester 2026-11-02 08:19:01 +01:00
.pre-commit-config.yaml focus_project: registry, stands_down, activity 2026-11-02 08:13:52 +01:00
install_harvest.sh focus_project: minutes, ledger, harvester 2026-11-02 08:19:01 +01:00
install.sh focus_project: registry, stands_down, activity 2026-11-02 08:13:52 +01:00
pyproject.toml focus_project 0.10.0: timer needs backing beats 2026-11-10 16:35:46 +01:00
README.md focus_project 0.10.0: timer needs backing beats 2026-11-10 16:35:46 +01:00
requirements.txt Add a plain-text multi-voice score format 2026-11-07 08:26:25 +01:00

focus_project

Time-boxed focus projects ("until Thursday I prepare for X"). The active project switches chosen gates off and defines a daily-minutes gate.

import focus_project as fp

fp.stands_down("leetcode", today)   # gate in leetcode|book|reading|tutor; never raises
fp.active(today)                    # Project | None
fp.status(today)                    # dict for i3bar / GUIs
fp.record_activity("timer", "start")

Data: $FOCUS_PROJECT_DATA or ~/.local/share/focus_project/ (projects.json, activity.jsonl). minutes.py is owned by the minutes/harvester agent. Time rules: minutes_for clips every interval at "now" (an event stamped ahead of the clock earns only once the clock reaches it); record_activity rejects a ts more than 120 s ahead (ValueError). Timer segments (start -> checkin ... -> stop) lapse after a 600 s gap; a tutor/heartbeat is a beat too (0.8.0: a graded check-in card extends the open segment, never opens one). end_project(id, day) rejects a past day (ValueError); today or later is fine. The harvester pays every project day of the last 31 days, so late-synced events for older days are credited (idempotent). Overtime carries over: own minutes past a day's target bank (max 60) and pay on the next project day; the bank never crosses the project's end. minutes_for(day) is the capped pay figure, status(day)["minutes"] the real count (121/120) with carried_in, carry_out, own_minutes, counted_minutes. Carry ledger rows: <project>-<YYYYMMDD>-carry (detail.carry), never over daily_minutes together with own rows. Edits only ratchet (0.7.0): update_project/apply_update(id, today=...) never lowers today's obligation. A lower daily_minutes/unlock_minutes, or a gate added to replaces, on a started project holds from TOMORROW (a history row ends today with the old values, replaces_on(day)/daily_on/unlock_on read it); a mixed edit defers as a whole. Raises, gates removed and edits to a project that has not started hold from today. apply_update(...).effective_from says which. end cannot be before today, so an edit never uncovers today. A NEW project starting today may replace gates and set any targets: it is a new obligation, not a reduction (but see the floor). Floor (0.7.1, coordinator decision for kuhy): daily_minutes >= 30 x replaced gates (min 30; book = reading), enforced on create and update (daily_floor(replaces)), so a day cannot be made cheap. Backed timer (0.10.0, days from BACKING_FROM = 2026-11-08): a timer span earns only if a same-project tutor/sql_drills/leetcode beat (or accepted submission) is in it, 5 min before it, or bridges it (beats <= 20 min apart). unbacked_minutes_for(day) / status()["unbacked_minutes"] show the rest. A beat backs only the spans it touches (the tutor chain makes a graded card back the whole stretch). Known gap: any coach turn backs. Open the work: python3 -m focus_project.open [--no-site] (tutor focus screen, work_url). Install: ./install.sh (stdlib only, editable user install for /usr/bin/python3).

Interview prep (kind="interview", 0.9): create_project(..., kind="interview", test_at=, invite_url=, trips=, weekend_answers=); end must be the test day. Budget: 30 min weekdays / 120 Sat-Sun (daily_on), exempt from daily_floor, unlock clamped. Milestones are generated, never stored: milestones(project, now) -> rules, trip_ask:<sat>, phone:<trip start>, mock, slot with states pending/done/missed/late_done/unknown. owed_lock(now) = earliest overdue uncompleted one (never on test day; wire it after focus-guard's sick/free-day stand-down). status(day) gains milestones and missed. Evidence is append-only (evidence/<id>.jsonl + copied files): record_evidence(id, milestone, kind, value, file=, now=) (file, tap, score, slot, slot_confirm, tutor's phone_check). add_trip / answer_weekend only get stricter (ValueError). run_checks(id) runs the phone source (reads the latest phone_check row) and latches changes; register_source swaps it.