Skip to content

fix(shifts): reject over-long jobs under allow_out_of_hours_depot_travel - #14

Merged
hutchinsp01 merged 2 commits into
masterfrom
fix/out-of-hours-depot-travel-overlong-job
Jul 19, 2026
Merged

fix(shifts): reject over-long jobs under allow_out_of_hours_depot_travel#14
hutchinsp01 merged 2 commits into
masterfrom
fix/out-of-hours-depot-travel-overlong-job

Conversation

@hutchinsp01

@hutchinsp01 hutchinsp01 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Reject over-long jobs under allow_out_of_hours_depot_travel

https://uptickhq.slack.com/archives/C08UUNL1L6R/p1784264505054759

Size: small (2 files, +83/−0 — a one-branch guard in the transport constraint plus a regression test)

Intent Stop the out-of-hours-depot-travel flag from scheduling jobs that overrun the shift
Risks Change is in the core TransportConstraint hot path; guarded so non-out-of-hours actors are unaffected
Focus vrp-core/src/construction/features/transport.rs — the floored arr_time_at_target

Motivation

When allow_out_of_hours_depot_travel is set, a job whose service duration exceeds the whole shift span was being scheduled anyway, finishing well outside the tech's work hours. Example: a tech on a 9–5 (8h) shift, single 12h task — should be unassigned with and without the flag, but the flag let it through.

Root cause. With the flag, fleet_reader relaxes the depot-start window to {earliest: None, latest: None} so the vehicle can leave the depot before the shift starts and reach the first job at the shift start. That relaxation becomes Actor.detail.time.start = 0 in Fleet::new (unwrap_or(0.)). The transport time-window constraint (evaluate_activity) then evaluates insertions from a depot departure of t = 0 — effectively a [0, end.latest] operating window instead of the real [shift_start, end.latest]. The FirstJobArrivalFloor correction that pushes the first-job arrival back to the true shift start is only applied later in apply_first_job_arrival_floor during scheduling — never in the feasibility check — so the shift-end overrun was never detected and the over-long job was accepted.

The standalone checker (checker/limits.rs::check_shift_time) already computes the effective window correctly (first-job arrival → depot return) and flags such a tour as "tour time is outside shift time". So the solver and the checker disagreed; the solver was too permissive.

Changes

  • vrp-core/.../features/transport.rs — in TransportConstraint::evaluate_activity, when prev is the depot and the actor declares a FirstJobArrivalFloor, floor arr_time_at_target to that floor before the shift-end feasibility check. This mirrors the schedule that apply_first_job_arrival_floor will produce, so the constraint sees the true (floored) arrival and rejects jobs that would overrun the shift. Guarded by get_first_job_arrival_floor() being Some (only out-of-hours actors set it) and prev.job.is_none(), so all other problems are unaffected.
  • vrp-pragmatic/.../features/limits/max_duration.rs — regression test allow_out_of_hours_depot_travel_rejects_job_longer_than_shift: a 150-unit job on a [100, 200] (span 100) shift must be unassigned with the flag OFF and ON.

Verification

  • cargo test -p vrp-core --lib709 passed, 0 failed
  • cargo test -p vrp-pragmatic --lib342 passed, 0 failed (9 pre-existing ignored)
  • The two existing out-of-hours tests still pass, confirming the valid out-of-hours behaviour (leaving the depot before shift start to reach the first job on time, and enforcing start.latest) is preserved:
    • allow_out_of_hours_depot_travel_moves_shift_bounds_onto_first_and_last_jobs
    • allow_out_of_hours_depot_travel_still_enforces_shift_start_latest

🤖 Generated with Claude Code

When `allow_out_of_hours_depot_travel` is set, the depot-start window is relaxed to
`{earliest: None, latest: None}` so the vehicle can leave the depot out of hours and
reach the first job at the shift start. That relaxation becomes `Actor.detail.time.start
= 0` in `Fleet::new`, so the transport time-window constraint evaluated insertions from a
depot departure of t=0 — effectively a `[0, end.latest]` operating window instead of the
real `[shift_start, end.latest]`. The `FirstJobArrivalFloor` correction that pushes the
first-job arrival back to the true shift start was only applied later in
`apply_first_job_arrival_floor` during scheduling, never in the feasibility check, so a
job longer than the whole shift (e.g. a 12h task on a 9-5 shift) was wrongly scheduled and
finished outside the tech's work hours.

Floor the first-job arrival to `FirstJobArrivalFloor` inside the constraint's
`evaluate_activity` (only when `prev` is the depot and the floor is set), mirroring the
schedule computed by `apply_first_job_arrival_floor`. The shift-end overrun is now detected
during insertion, so the over-long job is left unassigned — matching behaviour with the
flag off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@adamdickinson adamdickinson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TY for pair!

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hutchinsp01
hutchinsp01 merged commit 9cb9d50 into master Jul 19, 2026
3 checks passed
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.

2 participants