HANDOFF NOTE: project "scheduler" (current state after Updates 1-5)

SCOPE
Only the scheduler project is authoritative. All "archived work records" (archive-north/east/south/west-NN) belong to other projects and were discarded. Their values must not be used here.

INTERFACE
evaluate(payload) -> {selected:[id], blocked:[id], rejected:[id]}
payload: now (offset-aware ISO datetime); appointments, each with id, start (offset-aware ISO), duration (positive minutes), priority (integer), cancelled (boolean).
No mutation of input. Use zoneinfo or equivalent for timezone conversion. Do not write code in this note.

CURRENT CONFIGURATION (final values after all updates)
- timezone: America/New_York (Update 1)
- cutoff_hour: 17 (Update 1)
- cutoff_inclusive: true (Update 1)
- lead_minutes: 30 (Update 1)
- adjacency_allowed: true (set false in Update 2, then reset to true in Update 4)
- duration_max: 120 (set 90 in Update 2, then 120 in Update 4)
- cancelled_blocks: true (set false in Update 2, then true in Update 5)
- duplicate_policy: "first" (set "last" in Update 2, then "first" in Update 5)
- capacity: 1 (Update 3)
- report_order: "id" (Update 3)
- blackout_day: "2026-11-02" (Update 5)
- priority_high_first: false (Update 5), so process priority ascending

ALGORITHM CONTRACT
1. Deduplicate by id, preserving the first insertion position of each id. Keep the first row's value (duplicate_policy "first"; "last" would keep the last value). The retained row's original index is the actual original index of the value kept.
2. Process order: priority ascending (priority_high_first is false). Ties are broken by original index. The sort is stable.
3. Time arithmetic uses UTC elapsed minutes, not local wall-clock minutes (matters around DST).
4. Validity checks (any failure puts the id in rejected):
   - duration > 0 and <= 120
   - start >= now + 30 minutes
   - start converted to America/New_York must not fall on local date 2026-11-02 (blackout_day)
   - cutoff = 17:00 on start's local calendar day in New York; end must be <= cutoff (inclusive)
5. Cancelled appointments: cancelled_blocks is true, so a cancelled appointment is not rejected for being cancelled. It can reserve capacity and appears in blocked, not selected. It must still pass the other validity checks and the capacity check; otherwise it goes to rejected.
6. Capacity: accept only if fewer than 1 already-accepted interval overlaps it. This counts intersecting accepted intervals, not peak concurrency. Accepted includes cancelled ones that reserved capacity.
7. Adjacency: adjacency_allowed is true, so intervals sharing only an endpoint do not overlap. (false would mean they do overlap.)
8. All invalid or conflicting ids go to rejected.
9. Output ordering: each list is sorted by id (report_order "id"); "input" would sort by retained original index.

NOTES
- 2026-11-01 is the US DST fall-back date, so local-day and UTC-elapsed handling should be tested around it. The blackout_day is 2026-11-02, not 11-01.
- The ids' sort should be consistent (plain string order unless specified otherwise; not specified).

UNRESOLVED / NOT SPECIFIED
- Whether ids are strings or numbers, which affects id sort comparison.
- Handling of malformed or missing fields (not specified).
- Whether the blackout check uses the start's local date only (as stated) or also the end's date; assume start only.
- No updates beyond Update 5 yet. Later updates replace only the keys they name.