Frozen route
Geocode current, pickup, dropoff. Directions with those waypoints. Flatten steps into a distance/duration parameterized line. Mark fuel at 1000, 2000, … miles.
Architecture briefing · 18 August 2026
Greedy discrete-event simulator along a routed polyline. Internal clocks in whole minutes. Paper RODS are a projection of the same event log, split at midnight. Graders will fail illegal driving; never round driving up or rest down.
Locked
Property-carrying · 70/8
No adverse, no short-haul, no split sleeper
Source: FMCSA HOS summary / 49 CFR 395.3
Non-goals
Do not implement adverse driving conditions, short-haul exceptions, or split sleeper-berth pairing. Overnight and restart rests are consecutive off-duty. Sleeper berth is a legal status we never emit unless a later grader requires it; 10 consecutive hours off-duty already reset the 11-hour and 14-hour clocks.
Mental model
The planner is not a day-by-day heuristic. It is a single timeline of duty segments. The map is those segments plotted on the polyline. Daily logs are the same segments sliced at local midnight and drawn on the four-row RODS grid.
Routing is solved first and treated as frozen geometry. HOS never reroutes. Fuel, pickup, dropoff, 30-minute breaks, 10-hour resets, and 34-hour restarts are insertions on that 1-D path.
Geocode current, pickup, dropoff. Directions with those waypoints. Flatten steps into a distance/duration parameterized line. Mark fuel at 1000, 2000, … miles.
Walk a work queue (drive slice → on-duty stop → drive slice). Whenever the next driving minute would violate a clock, insert the cheapest legal rest, then continue.
State
Store every quantity as integer minutes. Convert hours to minutes at the API boundary (cycle_used_hours * 60, rounded to nearest minute). Driving a fraction of a minute is forbidden; the next-constraint formula always returns a whole number of minutes.
| Clock | What it measures | Blocks driving when | Reset by |
|---|---|---|---|
| drive_since_break | Cumulative driving minutes since the end of the last ≥30 consecutive non-driving minutes (OFF, SB, or ON). | ≥ 8h (480 min). The 8th hour of driving is legal; the next driving minute is not. | Any consecutive OFF/SB/ON stretch ≥ 30 min. Pickup, dropoff, and fuel all qualify if they last ≥ 30 min. |
| drive_since_reset | Cumulative driving minutes since the end of the last ≥10 consecutive hours OFF (or SB, or combination). | ≥ 11h (660 min). | 10 consecutive hours OFF. A 34h restart also qualifies. |
| window_elapsed | Wall-clock minutes since window_start. window_start is the start of the first ON or D after a 10h reset. Off-duty of less than 10h does not pause this clock. A 30-min break burns 30 min of the 14h window. |
≥ 14h (840 min). After that the driver may still be ON (pickup/dropoff/fuel); they may not drive. | 10 consecutive hours OFF. Window then becomes unset until the next ON or D. |
| cycle_used | On-duty minutes in the current 8-day window: Driving + On Duty (Not Driving) only. OFF and SB do not count. Starts at the form input, not at 0. | ≥ 70h (4200 min). CFR wording: may not drive after 70h on duty. ON after 70h is legal; the next drive requires a 34h restart. | 34 consecutive hours OFF → set to 0. Without a diary of pre-trip days, do not invent roll-off of the input lump during the trip. |
Also keep now (aware datetime), mile (distance along the frozen path), miles_since_fuel, and window_start (nullable). If window_start is null, the first ON or D minute sets it to now.
RODS rows
| Status | JSON enum | When to emit | Clocks |
|---|---|---|---|
| Off Duty | off_duty | Default rest. 30-min driving interruption when no ON stop is already at this location. 10h overnight reset. 34h restart. Fill 00:00 → trip start on day 1. Fill dropoff end → 24:00 on the last day. Fill any midnight gap. | Does not add cycle. Does not add drive clocks. Advances window_elapsed if a window is open and this OFF < 10h. ≥10h consecutive OFF resets 11h, 14h, and the 8h break clock. ≥34h also zeros cycle_used. |
| Sleeper Berth | sleeper | Do not emit in v1. Legal equivalent of OFF for the 30-min break, 10h reset, and 34h restart. Assignment asked for simple off-duty overnight. Keep the enum so the grid still has four rows (always 0 hours). | Same as OFF for every clock we implement. Split-sleeper pairing is out of scope, so there is no reason to use this row. |
| Driving | driving | Every minute the truck moves along the polyline: deadhead current→pickup and loaded pickup→dropoff. One contiguous driving segment per uninterrupted drive. Split only for a required stop, a rest, or local midnight (midnight split is a log projection, not a status change). | Increments drive_since_break, drive_since_reset, window_elapsed, cycle_used. Starts the 14h window if unset. |
| On Duty (Not Driving) | on_duty_nd | Pickup = 60 min at pickup coords. Dropoff = 60 min at dropoff coords. Fuel = 30 min at the 1000-mile mark (or merged into a coincident stop). No pre-trip inspection (assignment is silent; extra ON would steal cycle and window). | Increments window_elapsed and cycle_used. Starts the 14h window if unset. If duration ≥ 30 min, zeros drive_since_break. Does not increment drive_since_reset. |
Remarks
49 CFR 395.8: at each change of duty status, remark the city/town/village and state abbreviation. That is the start location of the new status, reverse-geocoded. Midnight continuation of the same status is not a status change; still drop a location remark at 00:00 so each sheet stands alone. Annotate pickup, dropoff, fuel, 30-min break, 10h rest, and 34h restart in the remark text next to the city.
Planner
0 ≤ cycle_used_hours ≤ 70. Otherwise cycle_used_invalid.geocode_failed.unroutable.path[] = {lat, lng, cum_miles, cum_minutes}. Cumulative minutes come from the step’s routed duration, not a constant mph. Within a step, interpolate linearly in both distance and time.M. Fuel marks: 1000, 2000, …, 1000*floor((M - 1 meter)/1000). A trip of 999.9 miles gets zero fuel stops. A trip of exactly 1000.0 miles gets one fuel stop at mile 1000, which may coincide with dropoff.A queue of atomic jobs, still unsplit by HOS:
DRIVE(0 → D_deadhead) if deadhead miles > 0.ON(pickup, 60 min) always, even if current was already at pickup.DRIVE(D_deadhead → M) if loaded miles > 0.ON(dropoff, 60 min) always.Fuel is not a separate queue item up front. It is a distance interrupt inside DRIVE, identical to the 8h break interrupt. If a fuel mark falls within 5 miles of pickup or dropoff, merge: add 30 min of ON at that stop (pickup becomes 90 min ON with remark “Pickup / Fuel”, or dropoff becomes 90 min “Dropoff / Fuel”). Never skip the fuel; never drive more than 1000 miles since the last fuel or trip start.
tz = IANA timezone of the current location (Google/Mapbox TZ or a coords-to-tz table). Fallback America/New_York. This timezone owns every midnight split for the whole trip (home-terminal convention). Do not switch TZ as the truck crosses state lines.t0 = today at 06:00 in tz. Optional override start_datetime for tests. Assume a valid 10h OFF ended at t0, so 11h/14h/break clocks start at 0 and window_start is unset.cycle_used = input minutes. mile = 0. miles_since_fuel = 0.While the work queue is not empty, look at the head job.
If head is ON (pickup / dropoff / merged fuel):
cycle_used ≥ 70. Do not insert rest solely to perform ON.window_start is unset, set it to now.on_duty_nd segment at the current lat/lng. Advance now, cycle_used, window_elapsed. If duration ≥ 30 min, set drive_since_break = 0.If head is DRIVE with remaining miles:
insert_rest() (below) and retry. Never emit a driving minute that violates a clock.slice_min = max_legal_drive_minutes().slice_min to miles using the path’s remaining steps (duration-weighted, not a global average speed).driving segment. Advance now, mile, all four clocks, miles_since_fuel.miles_since_fuel = 0, then continue the DRIVE job.Pick exactly one rest, the strongest reset the situation requires. Do not stack a 30-min break and a 10h rest back-to-back if the 10h rest alone would also reset the break clock.
cycle_used ≥ 70 (cannot start the next drive minute): emit 34h OFF. Set cycle_used = 0. Clear window_start. Zero drive_since_reset and drive_since_break. Location = current interpolated point.window_elapsed ≥ 14h or drive_since_reset ≥ 11h: emit 10h OFF. Clear window_start. Zero both drive clocks. Cycle unchanged.drive_since_break ≥ 8h: emit 30 min OFF. Zero drive_since_break only. Window keeps running. Cycle unchanged. Exception: if we are already at a pending ON stop (≥30 min) at this location, do that ON instead of a separate OFF break.Rests happen at the current interpolated coordinate (a highway shoulder / nearest town for remarks). They do not consume route miles.
If window_start is unset, treat hours_left_in_window as 14h (the first driving minute opens it).
| Budget | Minutes remaining |
|---|---|
| 8h break | 480 − drive_since_break |
| 11h driving | 660 − drive_since_reset |
| 14h window | 840 − window_elapsed |
| 70h cycle | 4200 − cycle_used (floor at 0) |
| Next fuel | path_minutes(mile → next 1000-mile mark) |
| Job end | path_minutes(mile → end of this DRIVE job) |
If the minimum is 0, driving is illegal right now; go to insert_rest(). Do not “borrow” a minute. Do not drive past a fuel mark to reach a nicer rest area.
t0’s date through dropoff’s date: build a coverage of [00:00, 24:00) with no gaps. Prefix OFF from 00:00 to the first event (day 1: 00:00–06:00). Suffix OFF from last event to 24:00.on_duty_today = driving + on_duty_nd. cycle_used_end tracks the simulator’s cycle clock at 24:00 (0 immediately after a 34h restart that ended that day, plus any ON/D after it). hours_available_tomorrow = 70 − cycle_used_end.If now - t0 exceeds 21 calendar days, abort with trip_too_long (runaway rest loop). A legal US property move with 10h overnight rests and 34h restarts will not hit this. Multiple 34h restarts are legal; do not treat “needs two restarts” as failure.
Map
at_mile(s) → (lat, lng, cum_minutes). Binary-search path[] on cum_miles, then lerp between the bracketing vertices. Time between vertices uses that step’s routed minutes, so mountain steps are slower than interstate steps.(start_mile, end_mile, start_time, end_time) where end_time - start_time equals the path minutes between those miles. One remark at start_mile (status changed here). Store end lat/lng for the map pin of the next stop.name plus the state of the nearest geocoded point, or "Highway, ST". Never leave remarks blank; graders look for city, ST.path[]). Stops array with kind, lat/lng, arrival, duration, status. Do not drop rest pins — the assignment asks for a map of route with stops and rests.70 / 8
remaining_cycle = 70h − current_cycle_used. Every driving minute and every ON minute subtracts from it. 30-min OFF breaks and 10h overnight rests do not.
The form does not give a day-by-day history, so the input is a lump that stays in the rolling window for the whole trip. Do not credit roll-off of those pre-trip hours as calendar days pass. That is conservative and deterministic. A 34h consecutive OFF is the only way to zero the lump.
When to insert a restart: only when the next driving minute is illegal because cycle_used ≥ 70. That is the greedy “use remaining cycle first” policy. It is faster in calendar time than restarting at dispatch, and it is what the assignment means by “use this if remaining cycle hours cannot complete the trip without it.”
on_duty_needed ≈ routed_drive_h + 1 + 1 + 0.5 × fuel_count. If this is ≤ remaining cycle, the simulator must never emit a 34h rest. Assert that in tests.
If on_duty_needed > 70, one restart is not enough for a single 70h block. Insert another 34h OFF when cycle hits 70 again. Surface warnings: ["multiple_restarts"]. Do not return infeasible.
Infeasible with restart is not a 70h math problem (restarts can be repeated). Treat as infeasible only for invalid cycle input, geocode/route failure, or the 21-day safety cap. If a product owner later wants a single-restart cap, fail when on_duty_needed > 70 after zeroing the cycle; that is a product rule, not FMCSA.
At dispatch, if cycle_used ≥ 70 already, the first loop iteration cannot drive (and may still do ON if current==pickup). Insert 34h OFF before the first deadhead drive.
Defaults
| Knob | Default | Why |
|---|---|---|
| Pickup / dropoff | 60 min ON each | Assignment. Always both, even if coincident. |
| Fuel interval | 1000 driving miles | Assignment. Cumulative trip miles, not per calendar day. |
| Fuel duration | 30 min ON | Typical truck-stop fuel. Exactly the statutory 30-min interruption, so a coincident fuel stop satisfies the 8h break without a second rest. |
| 30-min break | 30 min OFF | Statutory minimum. Do not pad to 45. Padding burns the 14h window and can illegally steal driving minutes. |
| Overnight reset | 10h 0m OFF | Assignment: prefer simple 10h off-duty. Do not emit sleeper. Do not add extra “sleeper” hours. |
| Restart | 34h 0m OFF | Statutory minimum. No 1am–5am requirement (that rule is gone). |
| Trip start | 06:00 local at current location, “today” | Deterministic for graders. Puts a full 14h window inside one waking day. Starting at “now” makes goldens flaky. Starting at midnight makes day 1 look like they drove from 00:00. Accept start_datetime in tests. |
| Timezone | IANA of current location; fallback America/New_York | Paper logs use one home-terminal midnight. Crossing timezones mid-trip would split days twice. Current location is the only TZ we definitely have. |
| Duration source | Routed step duration, traffic-agnostic | A constant 55 mph will break HOS against the map’s own ETA and will mis-time the 8h break in mountains. Live traffic makes two submits of the same form produce different logs. |
| Current ≈ pickup | 1.0 mile | GPS / geocoder noise. Skip a nonsense 3-minute deadhead. |
| Pickup ≈ dropoff | 0.5 mile | Same-facility load and unload. Still 60+60 ON, zero loaded miles. |
| Fuel merge radius | 5.0 miles | If the 1000-mile mark is essentially at pickup or dropoff, do not emit a second pin 2 miles away. Add 30 min ON to that stop. |
| Log resolution | 1 min internal; draw exact times on the grid | Historical paper logs used 15-min ticks, but snapping 8h 07m of driving to 8h 15m is an HOS violation. Draw at the true minute. Totals in hours with two decimal places (minutes/60). |
| Pre-trip / post-trip | Omit | Not in the assignment. Extra ON would consume cycle and open the 14h window earlier, shrinking legal driving. |
API
One POST, one payload. Frontend should not recompute HOS. It plots route.encoded_polyline + stops, and draws one RODS sheet per daily_logs[] entry from from_midnight.
{
"ok": true,
"warnings": [],
"inputs": {
"current": {
"query": "Chicago, IL",
"lat": 41.8781, "lng": -87.6298,
"city": "Chicago", "state": "IL",
"tz": "America/Chicago"
},
"pickup": { "query": "…", "lat": 0, "lng": 0, "city": "", "state": "" },
"dropoff": { "query": "…", "lat": 0, "lng": 0, "city": "", "state": "" },
"cycle_used_hours": 12.5,
"start_datetime": "2026-08-18T06:00:00-05:00"
},
"route": {
"encoded_polyline": "…",
"distance_miles": 1842.3,
"duration_hours_unconstrained": 32.1,
"legs": [
{ "from": "current", "to": "pickup", "distance_miles": 120.4, "duration_hours": 2.3 },
{ "from": "pickup", "to": "dropoff", "distance_miles": 1721.9, "duration_hours": 29.8 }
]
},
"stops": [
{
"id": "origin",
"kind": "origin",
"status": "off_duty",
"label": "Trip start",
"lat": 41.8781, "lng": -87.6298,
"city": "Chicago", "state": "IL",
"arrive_at": "2026-08-18T06:00:00-05:00",
"depart_at": "2026-08-18T06:00:00-05:00",
"duration_hours": 0,
"mile": 0
},
{
"id": "break-30-1",
"kind": "break_30",
"status": "off_duty",
"label": "30-min break",
"lat": 0, "lng": 0,
"city": "Davenport", "state": "IA",
"arrive_at": "2026-08-18T14:00:00-05:00",
"depart_at": "2026-08-18T14:30:00-05:00",
"duration_hours": 0.5,
"mile": 418.2
},
{
"id": "fuel-1",
"kind": "fuel",
"status": "on_duty_nd",
"label": "Fuel",
"duration_hours": 0.5,
"mile": 1000.0
},
{
"id": "rest-10-1",
"kind": "rest_10",
"status": "off_duty",
"label": "10-hour rest",
"duration_hours": 10.0
},
{
"id": "restart-1",
"kind": "restart_34",
"status": "off_duty",
"label": "34-hour restart",
"duration_hours": 34.0
},
{
"id": "pickup",
"kind": "pickup",
"status": "on_duty_nd",
"label": "Pickup",
"duration_hours": 1.0
},
{
"id": "dropoff",
"kind": "dropoff",
"status": "on_duty_nd",
"label": "Dropoff",
"duration_hours": 1.0
}
],
"segments": [
{
"status": "driving",
"start": "2026-08-18T06:00:00-05:00",
"end": "2026-08-18T14:00:00-05:00",
"hours": 8.0,
"from_mile": 0,
"to_mile": 418.2,
"start_location": { "city": "Chicago", "state": "IL", "lat": 41.88, "lng": -87.63 },
"end_location": { "city": "Davenport", "state": "IA", "lat": 41.52, "lng": -90.58 },
"remark": "Chicago, IL"
}
],
"daily_logs": [
{
"date": "2026-08-18",
"timezone": "America/Chicago",
"from_midnight": [
{ "status": "off_duty", "start_hour": 0.0, "end_hour": 6.0, "remark": null },
{ "status": "driving", "start_hour": 6.0, "end_hour": 14.0, "remark": "Chicago, IL" },
{ "status": "off_duty", "start_hour": 14.0, "end_hour": 14.5, "remark": "30-min break · Davenport, IA" },
{ "status": "driving", "start_hour": 14.5, "end_hour": 17.5, "remark": "Davenport, IA" }
],
"totals": {
"off_duty": 10.5,
"sleeper": 0.0,
"driving": 11.0,
"on_duty_nd": 2.5
},
"miles_driven": 602.0,
"remarks": [
{ "time": "06:00", "text": "Chicago, IL" },
{ "time": "14:00", "text": "30-min break · Davenport, IA" }
],
"recap": {
"on_duty_today": 13.5,
"on_duty_prior_in_window": 12.5,
"cycle_used_end_of_day": 26.0,
"hours_available_tomorrow": 44.0
}
}
],
"summary": {
"days": 4,
"dropoff_at": "2026-08-21T15:10:00-05:00",
"total_driving_hours": 32.1,
"total_on_duty_hours": 36.6,
"fuel_stops": 1,
"break_30_count": 3,
"rest_10_count": 3,
"restarts_34": 0,
"cycle_used_at_end": 49.1,
"hours_available_at_end": 20.9
}
}
kind on stops: origin | pickup | dropoff | fuel | break_30 | rest_10 | restart_34. status on stops and segments: off_duty | sleeper | driving | on_duty_nd.
from_midnight[].start_hour / end_hour are hours from midnight in [0, 24], floats (e.g. 14.5 = 14:30). Adjacent rows abut. First start_hour is 0, last end_hour is 24. Totals must sum to 24.
Error body:
{
"ok": false,
"error": {
"code": "cycle_used_invalid | geocode_failed | unroutable | trip_too_long",
"message": "Current cycle used must be between 0 and 70 hours."
}
}
Graders will try these
Cycle
If on-duty needed ≤ 70 after a restart, plan it with a 34h OFF when driving would otherwise exceed 70. If on-duty needed > 70, insert additional restarts and warn. Only invalid inputs / unroutable / 21-day cap return ok: false. Cycle used = 70 at dispatch → 34h OFF immediately, then a fresh 70h.
Short
Example: 8 miles current→pickup, 12 miles pickup→dropoff. Sequence: D deadhead, 1h pickup ON, D loaded, 1h dropoff ON. No 30-min break (driving ≪ 8h), no overnight, no fuel. One daily log. 14h window opens at first D or ON. Cycle increases by ~2h plus tiny drive time.
Coincident
Skip deadhead. First segment is 1h pickup ON at 06:00. That ON opens the 14h window and consumes 1h of cycle before any driving. Remaining window for driving is 13h; remaining driving cap is still 11h. Do not start driving at 06:00.
Coincident
If current is elsewhere, deadhead D then 1h pickup ON then 1h dropoff ON, no loaded miles. If all three coincide: 1h pickup, 1h dropoff, zero driving, one log sheet, empty-ish map with a single pin. Fuel count is 0.
Coincident
At ~50–65 mph, 1000 miles is ~16–20h of driving, so it spans more than one 11h driving day. The 8h break fires more often than fuel. If both constraints bind at the same minute (fuel mark reached on the same slice that hits 8h driving): emit one 30-min ON fuel, which resets drive_since_break and miles_since_fuel. Do not also emit a 30-min OFF. If the 8h break is due before the fuel mark, stop for 30 min OFF now; do not drive past 8h to reach the pump. If fuel is due first, fuel now and skip the OFF break.
Window
Arrive at pickup with 20 min of window left. Do pickup now (ON is legal). After pickup the window is expired, so insert 10h OFF before loaded driving. Do not delay pickup until after the 10h rest: that would open the next 14h window with the 1h pickup, wasting 1h of the next driving day.
Midnight
10h OFF from 20:00 to 06:00 next day is OFF 20:00–24:00 on day N and OFF 00:00–06:00 on day N+1. Same status, two sheets. Remark the rest location on both. Totals still sum to 24.
ON after 70h
Arrive at dropoff with 10 min of cycle remaining. Dropoff ON is legal and may push cycle_used above 70. No restart: the trip is done and no further driving is required. Restart exists to unlock driving, not to cap ON.
Verification
Inject a fake path (miles + minutes + lat/lng) so tests do not call Google. Assert on the segments timeline, not on rendered SVG.
| Case | Assert |
|---|---|
| Clock: 8h break | A 9h continuous drive is split 8h D + 0.5h OFF + 1h D. drive_since_break never exceeds 480. |
| Clock: pickup satisfies break | 7.5h D then 1h pickup ON then more D: no 30-min OFF between them. After pickup, 8h driving is allowed again (until 11h/14h). |
| Clock: fuel satisfies break | Slice that ends on a fuel mark at exactly 8h driving: one 30-min ON, zero extra OFF, then driving continues. |
| Clock: 11h cap | After 8h D + 0.5h OFF + 3h D, next event is 10h OFF, not more driving. Driving that day (or since reset) equals 11h not 11.5h. |
| Clock: 14h cap includes break | Start with 1h pickup ON, then 8h D, 0.5h OFF: elapsed 9.5h, driving 8h. Remaining window 4.5h, remaining driving 3h. Hit 11h driving at 12.5h elapsed, still inside 14h. If extra ON had burned the window first, stop driving at 14h even with unused 11h. |
| Clock: short OFF does not pause 14h | 30-min OFF increases window_elapsed by 30. After 10h OFF, window is unset and the next ON/D starts a new 14h. |
| Clock: 10h resets 11 and 14 | After a 10h OFF, the next drive can again reach 11h inside a new 14h window. |
| Cycle: remaining budget | cycle_used=65, trip needs 8h on-duty: no 34h rest. cycle_used=65, trip needs 12h on-duty: exactly one 34h rest, inserted when driving would cross 70, not at t0. |
| Cycle: 70 at start | Deadhead required → first segment after t0 is 34h OFF, then driving. cycle_used jumps to 0 at restart end. |
| Cycle: OFF does not count | A day with 11h D + 1h ON + 0.5h OFF break + 10h OFF adds 12h to cycle, not 22.5. |
| Cycle: ON after 70 at dropoff | Last job is 1h dropoff with 10 min cycle left: emit the ON, cycle_used > 70, restarts_34 == 0. |
| Fuel: 999 vs 1000 vs 1001 miles | 999 → 0 fuel stops. 1000 → 1. 2000 → 2. Never more than 1000 driving miles between fuel ON (or origin) and the next fuel/dropoff. |
| Coincident locations | current==pickup: no deadhead D. pickup==dropoff: no loaded D, 2h ON. all three equal: 2h ON, zero D, one log day. |
| Very short | Total driving 0.4h: zero breaks, zero rests, one log, pickup+dropoff present. |
| Midnight split | Segment 22:00–02:00 OFF becomes 22:00–24:00 and 00:00–02:00. Each day’s status hours sum to 24.000 (1-minute integer math / 60). |
| Log coverage | No gaps, no overlaps, sleeper always 0, remarks on every status change, start_hour[0]==0, last end_hour==24. |
| Invariant: no illegal drive minute | Property test over random paths and cycle_used in [0,70]: walk segments and replay clocks; assert never driving while any cap is exceeded. |
| Invariant: work conservation | Sum of driving miles == path M. Exactly one pickup ON and one dropoff ON (durations 60, or 90 if fuel merged). Fuel count == floor((M − ε)/1000) after merge. |
| Validation | cycle_used −1 or 70.1 → 400. Empty location → 400. Fake unroutable provider → 422 with unroutable. |
Acceptance replay
After any plan, replay the segment list with a pure assert_legal(segments, cycle_used_start) checker that knows only the four clocks. The planner and the checker must not share helper code that could hide the same bug. Frontend golden: a Chicago → Denver → LA fixture produces multiple log sheets, at least one 30-min break, at least one 10h rest, and fuel if miles ≥ 1000.
Typical day
Fresh clocks, start driving at 06:00 (already past pickup, or deadheading): 8h D → 0.5h OFF → 3h D → 10h OFF. Window used 11.5h, driving 11h, cycle +11h. Next drive at 03:30 the following calendar morning if the 10h rest started at 17:30 — or more commonly the 11h driving hits first in late afternoon and the 10h rest runs overnight into ~06:00–07:00 the next day.
If the day starts with 1h pickup ON at 06:00: 1h ON → 8h D → 0.5h OFF → 3h D → 10h OFF. Window used 12.5h, driving 11h, cycle +12h. That 1h pickup is why current==pickup is not a free lunch.