HANDOFF NOTE: project "scheduler" (only this project; archive-* records are unrelated and discarded)

1. INTERFACE (fixed)
evaluate(payload) -> {selected:[id], blocked:[id], rejected:[id]}
payload: now (offset-aware ISO datetime); appointments: list of {id, start (offset-aware ISO), duration (positive minutes), priority (int), cancelled (bool)}.
No mutation of input. Use zoneinfo or equivalent for timezone conversion.

2. CURRENT CONFIG (after Updates 1-5; later updates replace only the keys they name)
- timezone = "UTC" (Update 1)
- cutoff_hour = 18 (Update 1)
- cutoff_inclusive = false (Update 1)
- lead_minutes = 60 (Update 1)
- adjacency_allowed = false (Update 4; touching endpoints DO overlap)
- duration_max = 90 (Update 4)
- cancelled_blocks = false (Update 5; cancelled appointments are rejected)
- duplicate_policy = "last" (Update 5)
- blackout_day = "2026-11-01" (Update 5)
- priority_high_first = true (Update 5)
- capacity = 2 (Update 3)
- report_order = "input" (Update 3)

3. ALGORITHM CONTRACT
a. Deduplicate by id, keeping the first insertion position of that id. The value used follows duplicate_policy ("last" now: later row's values replace earlier). The retained row's original index is the actual original index of the value kept (for "last", the index of the last occurrence), not the first position's index.
b. Process order: priority descending (priority_high_first = true); ties broken by original index ascending. (If false: priority ascending, same tie rule.)
c. All start/end arithmetic uses UTC elapsed minutes, not local wall minutes. end = start + duration minutes in absolute time.
d. Validation per appointment (failures go to rejected):
 - duration > 0 and <= duration_max (90).
 - start >= now + lead_minutes (60); equality passes.
 - Convert start to the configured timezone (currently UTC); if its local date equals blackout_day (2026-11-01), reject.
 - Cutoff: on start's local calendar day, at cutoff_hour:00 (18:00) in that zone. End must be < cutoff (cutoff_inclusive false); if inclusive, <=. Compare in absolute time.
 - cancelled: if cancelled_blocks is false, reject. If true, it may reserve capacity and is reported in blocked rather than selected (inactive under current config, but implement the branch).
e. Capacity: accept only if the number of already accepted intervals that overlap it is < capacity (2). This counts intersecting accepted intervals, not peak concurrency. Overlap: with adjacency_allowed=true, intervals sharing only an endpoint do not overlap; with false (current), they do overlap (start1 <= end2 and start2 <= end1).
f. Rejected ids include all invalid and conflicting ones. Accepted non-cancelled go to selected; accepted cancelled (only when cancelled_blocks true) go to blocked.
g. Output ordering: per report_order; "input" = by retained original index (current); "id" = sorted by id. Applies to each of the three lists.

4. UNRESOLVED / NOTES
- No explicit spec for whether cancelled appointments still undergo validation (duration, lead, blackout, cutoff) before reserving capacity; sensible default: validate first, apply the cancelled rule only after passing validation. Confirm if clarified.
- Ambiguities not specified: malformed rows, duplicate ids in output ordering ties, naive datetimes; assume inputs conform to the contract.
- Sort for report_order "id" assumes ordinary string comparison on id.
- Awaiting further updates; apply each as key-by-key replacements only.