Participant and Group are now pydantic BaseModels. Group.hosts can form
cycles between groups, so it's kept as a private, non-persisted live list
(set via set_hosts()/add_host()) backed by a serializable host_uuids field,
re-linked via resolve_hosts() after a reload.
A new Plan model (src/tatami/plan.py) bundles groups, the after-party group,
and the event config (course_times, organizer_contacts, info_text,
spreadsheet_id) and supports save()/load() to/from JSON. tatami_masterplan's
__main__ now saves to masterplan.json (PLAN_FILE env var to override) on
first run and loads it on later runs instead of recomputing, so the plan can
be hand-edited (move a member between groups, change a course, fill in
spreadsheet_id) and picked up on rerun without hitting the Routes API again.
spreadsheet_id moves out of .env (GOOGLE_SHEETS_SPREADSHEET_ID) onto the plan
itself, since it's part of the plan rather than a secret.
Lets organizers automatically populate a shared Google Sheet (the same kind
they previously built by hand) with an Overview tab and one tab per group,
instead of printing dicts. Group tabs are named after all their members
(not a single host) and show each group's route, course times, and guests
with allergies. The Overview tab also gets configurable Meal Times, Support
Contacts, and Info sections, passed through as plain data from
tatami_masterplan.py. Export is fully opt-in via GOOGLE_SHEETS_CREDENTIALS_FILE
and GOOGLE_SHEETS_SPREADSHEET_ID; without them, behavior is unchanged.
Document the running-dinner concept, setup (uv + .env API key), CSV input
schema, how to run it (CLI and library), output format, a "what to tweak"
table mapping each knob to its file:line, the algorithm (group building,
rotation topology, annealing), testing (offline + opt-in e2e), limitations,
and development commands.
- test_e2e_api.py: minimal 2-address (4-element) call to the real Google
Routes API; reads the real key from env/.env and skips on a placeholder.
- Marked `e2e` and deselected by default via addopts; run with
`pytest -m e2e`.
- Add python-dotenv and call load_dotenv() in traveltimes.py before reading
GOOGLE_MAPS_API_KEY (exported env vars still take precedence).
- Add .env.example template; the real .env stays gitignored.
Route building / optimization (tatami_masterplan.py):
- fast_total_time was permutation-invariant: it applied get_courses to
group *values* instead of slots and ignored the permutation, so every
ordering scored identically and the annealing optimized nothing. It now
maps each rotation slot to its assigned group via the permutation.
- Replaced the broken next_permutation/simulated_annealing (enumerated n!
orderings per iteration, fed unnormalized Boltzmann weights to
np.random.choice -> ValueError, and returned the last random sample) with
a standard neighbor-swap annealer that tracks and returns the best
solution and handles <2 slots.
- Convert the reduced Timedelta matrix to float seconds before annealing
(np.exp can't operate on Timedelta).
Group building (tatami_masterplan.py):
- assign_courses set each group's hosts (sorted by course) before all
courses were assigned, so hosts whose course was still None got
mis-ordered. Assign all courses first, then wire up hosts.
- get_masterplan no longer mutates the caller's participant list.
classes.py:
- Narrow casts on distance-matrix lookups to satisfy the mypy gate
(pre-existing failures).
Tests:
- Add pytest suite (74 tests) covering the rotation topology, route cost
and optimization, the domain model, the masterplan pipeline, and the
Routes API wrapper (HTTP mocked). The cost cross-check caught the
assign_courses ordering bug above.