Skip to content

fix(breaks): stop a required break interrupting a job - #16

Open
hutchinsp01 wants to merge 1 commit into
masterfrom
fix/required-break-no-job-interruption
Open

fix(breaks): stop a required break interrupting a job#16
hutchinsp01 wants to merge 1 commit into
masterfrom
fix/required-break-no-job-interruption

Conversation

@hutchinsp01

Copy link
Copy Markdown
Collaborator

Problem

A required break is modelled as reserved time rather than as a job, and was applied on top of whatever the vehicle happened to be doing at the break's latest time. When that moment fell inside a job's service, the break duration was simply added to the activity's departure, so the break was reported in the middle of the job:

job1  05:00 -> 10:00
break       07:00 -> 09:00   <-- inside the job's service

Total duration was correct, but the work was shown as interrupted.

Rule

A required break is now taken at the first moment it becomes due without interrupting work:

Vehicle state when the break becomes due Break is taken
Driving On the road, as a transit stop
Idle at a stop (arrived before the time window opens) Immediately, before the service starts
Serving a job Once that service is finished, delaying the departure

If deferring past the service in progress would push the break beyond its latest, the insertion is rejected: the job moves to another vehicle, or is reported as unassigned with TIME_WINDOW_CONSTRAINT. A required break is still never dropped and is always taken inside its window.

The departure time does not depend on where the break sits (service_start + service_duration + break_duration either way), so routes which were already valid keep their schedules and statistics.

Changes

  • place_reserved_time in reserved_time.rs holds the placement rule and returns BeforeService / AfterService / None (not feasible). estimate_departure delegates to it, and None becomes a ControlFlow::Break, which the transport feature already turns into a constraint violation.
  • The reserved-time lookup is keyed on the window's start instead of its end, so earliest acts as a floor rather than being ignored.
  • break_writer.rs derives the break's position from the core route using that same shared function, rather than re-deriving it from the API stop schedules. The old logic that extended the overlapping activity's time.end — which drew the break inside the job — is gone.
  • checker/routing.rs assumed a transit break sat at the very start of a leg and validated each half independently; it now validates the leg as a whole.
  • checker/breaks.rs expected a required break only if its window ended before the tour ended; it now expects one if the break becomes due before the vehicle departs its last stop.

Behaviour changes to be aware of

Breaks move earlier. Honouring earliest means a break is taken at the start of its window rather than pinned to latest. A window of [12:00, 13:00] that previously resolved to 13:00 now resolves to 12:00, and if the vehicle is driving at 12:00 the result is a transit stop where a break at a stop was reported before.

earliest == latest is effectively unusable. Pinned to a single instant there is no slack to defer into, so any job whose service spans that instant becomes infeasible. Required breaks need a real window to be satisfiable around jobs.

Testing

Full workspace suite passes (all 15 targets). New coverage:

  • can_place_reserved_time (5 cases) — the placement rule directly, including both infeasible outcomes
  • can_defer_break_until_service_is_finished — the reported problem: break follows the service instead of splitting it
  • can_take_break_before_service_when_it_is_due_on_arrival
  • can_take_break_during_waiting_time — break absorbed by idle time, with the waiting statistic and cost adjusted
  • can_take_break_on_the_road_when_it_is_due_while_driving
  • can_reject_job_which_cannot_be_served_around_break — job reported unassigned rather than interrupted

Three existing tests encoded the old behaviour and were rewritten; the rest are unchanged.

🤖 Generated with Claude Code

A required break is modelled as reserved time, which was applied on top of
whatever the vehicle happened to be doing at the break's `latest` time. When
that moment fell inside a job's service, the break duration was added to the
activity's departure, so the break was reported in the middle of the job.

Place the break around the service instead. It is now taken at the first
moment it becomes due without interrupting work:

* while the vehicle is idle at a stop, before the service starts
* on the road, when it becomes due while driving
* once the service in progress is finished, delaying the departure

The new `place_reserved_time` holds this rule and is shared by the scheduler
and the solution writer, so the reported break cannot drift from the schedule
the solver computed. `earliest` is honoured as a floor rather than ignored, so
the search is driven by the moment a break becomes due instead of its deadline.

When a break cannot be taken within its time window without interrupting a
service, the insertion is rejected: the job moves to another vehicle or is
reported as unassigned. The departure time does not depend on where the break
sits, so routes which were already valid keep their schedules and statistics.

Also fix the routing checker, which assumed a transit break sat at the very
start of a leg and validated each half of it independently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant