fix(roadtrip): a flight day ends at the stay it lands at, and no drive runs where nobody drives

On a day a flight, train, ferry, cruise or bus leaves and lands, tonight's stay was seated
by its check-in time. A hotel in Munich checked into from 15:00 stood ahead of a 15:15 flight
out of Hamburg, so the drive went from the Munich hotel to Hamburg airport and reported the
flight as nine hours late. A check-in is the earliest arrival, not an appointment: such a
stay now waits behind the landing when it lies nearer the arrival than the departure, and
its check-in only holds it when the ride lands before the room is ready. A time typed on the
stop keeps its seat.

With the stays at both ends of a day switched on:
- a day with nothing but the next stay's check-in and a ride booked across it gets no drive
  from one hotel to the other, the same rule the day plan follows since #2476;
- a stay is never joined by road to a terminal more than 2000 km away. The check moved to
  shared/roadtrip so Days and the road trip read one copy.

A departure terminal's pin is dated: reaching it more than half a day late no longer moves
the flight to the next day without a word, it is a late warning with the full minutes.
A check-in evening also carries the stay's check-in time, as a label only.

calculate_roadtrip runs the same code and its description says all of the above.
This commit is contained in:
Maurice
2026-09-24 10:12:01 +02:00
parent 3062df153f
commit e64ade010f
18 changed files with 784 additions and 52 deletions
@@ -263,8 +263,11 @@ export class RoadtripPlanService {
});
// A day after a booked night starts at the stay and a day before one ends there, by
// the rule the planner runs in the browser, and only while the trip (or the preview's
// own settings) has it switched on. Everything below reads the seated days.
const plan = hotelBookendsOn(preferences) ? seatNightBookends(stored, context.days, context.stays) : stored;
// own settings) has it switched on. The rides go in with or without their stations,
// the way the browser hands over its bookings. Everything below reads the seated days.
const plan = hotelBookendsOn(preferences)
? seatNightBookends(stored, context.days, context.stays, context.carriers)
: stored;
const allLegs: Record<string, RoutedLeg> = {};
const snapByDay: Record<number, Record<string, SnappedWaypoint>> = {};
const missedByDay: Record<number, RouteAvoidClass[]> = {};
@@ -49,7 +49,7 @@ export class RoadtripPlanningMcp {
@Tool({
name: 'calculate_roadtrip',
description:
'Calculate a saved trip without an open browser, using the same daily scheduling and vehicle-range rules as the planner. Returns derived days, arrivals, departures, driving limits, fuel warnings, automatic pauses and routing failures. A flight, train, ferry, cruise or bus booking with located terminals is a seam: its terminals appear as stops carrying `carrier` (role departure or arrival), the road ends at the departure terminal and resumes at the arrival one, and the leg between them is the ride, in the mode of the booking type, with its timetable minutes and no distance. A hire car booking with a located pick-up desk puts that desk on the road as a stop carrying `carrier` with role pickup, and its return desk with role return when the booking names a return day and place; the road runs through both. A flight, train, ferry, cruise or bus booking that is on none of the days of the trip (saved without a date, or on a day the trip no longer has) is not on the drive at all, although its terminals are located: undatedRides lists each one (id, type, title) so it can be given a date within the trip with the reservation tools, after which the road ends at its terminal. With roadtrip_hotel_bookends switched on (missing means off), a day after a booked night starts at that stay and a day before one ends there. Such stops carry `bookend` {phase, accommodationId, reservationId, checkingOut, checkingIn, checkOut}, stopType hotel, the real placeId of the stay and a synthetic negative assignmentId that belongs to no assignment: they cannot be reordered, edited or given via points, and `failures` may name them. The check-out is a label, never a departure time. A day between two nights in the same stay with no visit has no drive. Pass settings {roadtrip_hotel_bookends:true} to preview the drive from and to the stays without saving it. Fixed visit times retain priority. The end time of a visit is when the drive leaves it, in place of its stay; reaching the visit after it is reported as a missedLeave warning. No accommodation or place is created. Optional settings are a read-only preview, not saved. Distances are metres, durations seconds, dwell times minutes. A failed leg or conflicting window means the output is incomplete: never present its totals as a complete route. Up to 150 legs; requests are paced for public routing servers and can take time. Existing place/assignment tools edit stops, stays and ordering, update_roadtrip_settings saves preferences, set_assignment_end_day and set_day_boundary set exceptions. Recalculate after changes. Plugin profiles follow installed route-provider permissions.',
'Calculate a saved trip without an open browser, using the same daily scheduling and vehicle-range rules as the planner. Returns derived days, arrivals, departures, driving limits, fuel warnings, automatic pauses and routing failures. A flight, train, ferry, cruise or bus booking with located terminals is a seam: its terminals appear as stops carrying `carrier` (role departure or arrival), the road ends at the departure terminal and resumes at the arrival one, and the leg between them is the ride, in the mode of the booking type, with its timetable minutes and no distance. The departure terminal is pinned a check-in ahead of the timetable departure (an hour for a flight); reaching it after that is a late warning with the full minutes, however many, and the ride keeps its own day, so the drive after the arrival terminal goes on from the timetable arrival. On a day a ride leaves and lands on, the stop of a stay checked into that day comes after the arrival terminal when the stay lies nearer to it than to the departure terminal: a check-in is the earliest arrival, not a time that seats the stay ahead of the ride, and it pins the stay only when the ride lands before it. A time set on the stop itself keeps its seat. A hire car booking with a located pick-up desk puts that desk on the road as a stop carrying `carrier` with role pickup, and its return desk with role return when the booking names a return day and place; the road runs through both. A flight, train, ferry, cruise or bus booking that is on none of the days of the trip (saved without a date, or on a day the trip no longer has) is not on the drive at all, although its terminals are located: undatedRides lists each one (id, type, title) so it can be given a date within the trip with the reservation tools, after which the road ends at its terminal. With roadtrip_hotel_bookends switched on (missing means off), a day after a booked night starts at that stay and a day before one ends there. Such stops carry `bookend` {phase, accommodationId, reservationId, checkingOut, checkingIn, checkOut, and checkIn on the evening a stay begins when it has a check-in time}, stopType hotel, the real placeId of the stay and a synthetic negative assignmentId that belongs to no assignment: they cannot be reordered, edited or given via points, and `failures` may name them. The check-out and check-in are labels, never a departure or arrival time. A day between two nights in the same stay with no visit has no drive. Nor does a day with nothing on it but the check-in of the next stay when a flight, train, ferry, cruise or bus is booked on that day, with or without located terminals: the booking is the move between the two stays. A stay is never joined by road to a terminal more than 2000 km away in a straight line. Pass settings {roadtrip_hotel_bookends:true} to preview the drive from and to the stays without saving it. Fixed visit times retain priority. The end time of a visit is when the drive leaves it, in place of its stay; reaching the visit after it is reported as a missedLeave warning. No accommodation or place is created. Optional settings are a read-only preview, not saved. Distances are metres, durations seconds, dwell times minutes. A failed leg or conflicting window means the output is incomplete: never present its totals as a complete route. Up to 150 legs; requests are paced for public routing servers and can take time. Existing place/assignment tools edit stops, stays and ordering, update_roadtrip_settings saves preferences, set_assignment_end_day and set_day_boundary set exceptions. Recalculate after changes. Plugin profiles follow installed route-provider permissions.',
inputSchema: roadtripPlanRequestSchema.shape,
annotations: TOOL_ANNOTATIONS_READONLY,
access: { group: 'trips', mode: 'read' },