CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/nanoclaw-travel

Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), traffic-aware drive planning for in-person meetings (auto drive blocks + leave-by traffic rechecks), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay plugin.

68

Quality

85%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

engine.pyskills/drive-engine/

"""Engine orchestration — assemble the pure pipeline into a reconcile plan.

Ties the pure modules together: normalized `Flight`s → merged union → per-trip
chains → connection classification → planned legs → §B anchors → routed drives with
GPS-overlaid origins → desired blocks → the reconcile diff against the calendar.

Dependency-injected so the whole flow is testable headless: the caller resolves
everything that needs I/O — airport facts (byAir `get_airport`), routed drive
seconds (`maps_client`), the current calendar blocks, the live-location fix, and
the boarding-block / left-terminal predicates — and passes them in. `main()` wires
the real clients; this function stays pure over its inputs.

Per-leg endpoint resolution (the routing ↔ leave-by ↔ origin interplay of §A/§B):

- departure: dest = departure airport (fixed); origin = `position_at(anchor)`, then
  routed, then GPS-overlaid when imminent, then re-routed from the resolved origin.
  leave_by = arrive_by − routed drive.
- arrival: origin = arrival airport (fixed); dest = `position_at(depart_after)`. The
  drive starts at depart_after; no GPS overlay (the operator is at the airport).
- transfer: both endpoints fixed airports; leave_by = window_end − routed drive.

A leg whose non-fixed endpoint cannot be resolved to an address (`position_at`
returns None off a configured home) is skipped with a diagnostic rather than
routed blind. A trivial routed drive with a boarding block present is suppressed.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta

from block_codec import leg_identity
from chain import LegKind, plan_chain_legs
from chain_builder import build_pair_contexts, group_into_chains
from flight_identity import Flight, MergedFlight, merge_flights
from leg_anchor import AirportFacts, BufferOverrides, ConcreteLeg, resolve_leg_anchor
from position import position_at, resolve_leg_origin
from reconcile import (
    DesiredBlock,
    ParsedBlock,
    ReconcilePlan,
    legacy_keys_for_airport_leg,
    plan_reconcile,
)
from suppression import is_trivial_leg

RouteFn = Callable[[str, str], "timedelta | None"]
BoardingPresentFn = Callable[[MergedFlight], bool]
LeftTerminalFn = Callable[[MergedFlight, MergedFlight], bool]


@dataclass(frozen=True)
class AirportInfo:
    """Resolved byAir facts for one airport, keyed by IATA in the `airport_info` map."""

    flag: str | None = None
    delay_index: str | None = None
    timezone: str | None = None  # IANA tz — the airport block's local display tz


@dataclass(frozen=True)
class EngineResult:
    plan: ReconcilePlan
    skipped: tuple[str, ...] = ()  # human diagnostics for legs that couldn't be built


def _facts_for(flight: MergedFlight, airport_info: dict[str, AirportInfo]) -> AirportFacts:
    dep = airport_info.get(flight.dep_airport, AirportInfo())
    arr = airport_info.get(flight.arr_airport, AirportInfo())
    return AirportFacts(
        dep_flag=dep.flag,
        arr_flag=arr.flag,
        delay_index=dep.delay_index,
        dep_timezone=dep.timezone,
        arr_timezone=arr.timezone,
    )


def _dest_label(planned) -> str:
    """A human label for a resolved drive-home destination: 'home' only when it
    really is the static home, else the lodging/anchor address (never a lie)."""
    if planned.source == "home":
        return "home"
    return planned.address or "destination"


def _summary(leg: ConcreteLeg) -> str:
    if leg.kind is LegKind.AIRPORT_DEPARTURE:
        return f"Drive: → {leg.dest_airport}"
    if leg.kind is LegKind.AIRPORT_ARRIVAL:
        # Arrival's real destination is resolved later; _build_arrival overrides
        # this with the actual place. Fallback label only.
        return f"Drive: {leg.origin_airport} → destination"
    return f"Drive: {leg.origin_airport} → {leg.dest_airport}"


def _legacy_keys(leg: ConcreteLeg) -> frozenset[tuple[str, str, str]]:
    kind = (
        "airport_departure"
        if leg.kind is LegKind.AIRPORT_DEPARTURE
        else "airport_arrival"
        if leg.kind is LegKind.AIRPORT_ARRIVAL
        else "airport_transfer"
    )
    return legacy_keys_for_airport_leg(kind, leg.flight.byair_flight_ids)


def _build_departure(
    leg: ConcreteLeg,
    *,
    schedule: list[dict] | None,
    home_address: str | None,
    now: datetime,
    live_origin: str | None,
    route: RouteFn,
    boarding_present: BoardingPresentFn,
) -> tuple[DesiredBlock | None, str | None]:
    anchor = leg.anchor
    assert anchor is not None and leg.dest_airport is not None
    planned = position_at(schedule, anchor, home_address=home_address)
    if planned.address is None:
        return None, f"departure {leg_identity(leg)}: no origin (position_at unresolved)"
    approx = route(planned.address, leg.dest_airport)
    if approx is None:
        return None, f"departure {leg_identity(leg)}: origin route failed"
    leave_by = anchor - approx
    resolved = resolve_leg_origin(
        planned, now=now, leave_by=leave_by, drive=approx, live_origin=live_origin
    )
    origin = resolved.address or planned.address
    drive = route(origin, leg.dest_airport)
    if drive is None:
        return None, f"departure {leg_identity(leg)}: route failed"
    if is_trivial_leg(drive, presence_block_present=boarding_present(leg.flight)):
        return (
            None,
            f"departure {leg_identity(leg)}: trivial ({int(drive.total_seconds())}s), suppressed",
        )
    leave_by = anchor - drive
    return (
        DesiredBlock(
            identity=leg_identity(leg),
            kind="airport_departure",
            summary=_summary(leg),
            start=leave_by,
            end=anchor,
            origin=origin,
            destination=leg.dest_airport,
            baseline_seconds=int(drive.total_seconds()),
            anchor=anchor,
            timezone=leg.timezone,
            legacy_keys=_legacy_keys(leg),
        ),
        None,
    )


def _build_arrival(
    leg: ConcreteLeg,
    *,
    schedule: list[dict] | None,
    home_address: str | None,
    route: RouteFn,
    boarding_present: BoardingPresentFn,
) -> tuple[DesiredBlock | None, str | None]:
    anchor = leg.anchor
    assert anchor is not None and leg.origin_airport is not None
    planned = position_at(schedule, anchor, home_address=home_address)
    if planned.address is None:
        return None, f"arrival {leg_identity(leg)}: no destination (position_at unresolved)"
    drive = route(leg.origin_airport, planned.address)
    if drive is None:
        return None, f"arrival {leg_identity(leg)}: route failed"
    if is_trivial_leg(drive, presence_block_present=boarding_present(leg.flight)):
        return None, f"arrival {leg_identity(leg)}: trivial, suppressed"
    return (
        DesiredBlock(
            identity=leg_identity(leg),
            kind="airport_arrival",
            summary=f"Drive: {leg.origin_airport} → {_dest_label(planned)}",
            start=anchor,
            end=anchor + drive,
            origin=leg.origin_airport,
            destination=planned.address,
            baseline_seconds=int(drive.total_seconds()),
            anchor=anchor,
            timezone=leg.timezone,
            legacy_keys=_legacy_keys(leg),
        ),
        None,
    )


def _build_transfer(leg: ConcreteLeg, *, route: RouteFn) -> tuple[DesiredBlock | None, str | None]:
    assert leg.origin_airport is not None and leg.dest_airport is not None
    assert leg.window_start is not None and leg.window_end is not None
    drive = route(leg.origin_airport, leg.dest_airport)
    if drive is None:
        return None, f"transfer {leg_identity(leg)}: route failed"
    leave_by = leg.window_end - drive
    return (
        DesiredBlock(
            identity=leg_identity(leg),
            kind="airport_transfer",
            summary=_summary(leg),
            start=leave_by,
            end=leg.window_end,
            origin=leg.origin_airport,
            destination=leg.dest_airport,
            baseline_seconds=int(drive.total_seconds()),
            anchor=leg.window_start,
            window_end=leg.window_end,
            timezone=leg.timezone,
        ),
        None,
    )


def build_reconcile_plan(
    *,
    flights: list[Flight],
    airport_info: dict[str, AirportInfo],
    current_blocks: list[ParsedBlock],
    route: RouteFn,
    schedule: list[dict] | None = None,
    home_address: str | None = None,
    now: datetime,
    live_origin: str | None = None,
    boarding_present: BoardingPresentFn | None = None,
    left_terminal: LeftTerminalFn | None = None,
    overrides: BufferOverrides | None = None,
    extra_desired: list[DesiredBlock] | None = None,
    managed_legacy: frozenset[str] | None = None,
) -> EngineResult:
    """Run the full engine pipeline and return the reconcile plan + diagnostics.

    `route(origin, dest)` returns the drive duration or None on failure (a failed
    route skips the leg with a diagnostic, never a blind block). `boarding_present`
    reports whether a flight's boarding block exists (for trivial suppression);
    default assumes present. `left_terminal` supplies same-airport "did you leave"
    geofence evidence. `extra_desired` folds in blocks from another source (the
    meeting-leg source) so both are diffed against the calendar in one reconcile.
    `managed_legacy`, when given, is passed through to `plan_reconcile` to scope
    which legacy generations the engine may converge / orphan-delete.
    """
    boarding_present = boarding_present or (lambda _flight: True)
    if overrides is None:
        overrides = BufferOverrides()
    merged = merge_flights(flights)
    chains = group_into_chains(merged)

    desired: list[DesiredBlock] = []
    skipped: list[str] = []

    for chain in chains:
        contexts = build_pair_contexts(chain, schedule=schedule, left_terminal=left_terminal)
        for planned in plan_chain_legs(chain, contexts):
            # plan_chain_legs guarantees the right endpoint per kind, but narrow it
            # explicitly here so _facts_for receives a non-None flight (no ignore).
            if planned.kind is LegKind.AIRPORT_TRANSFER:
                earlier, later = planned.from_flight, planned.to_flight
                if earlier is None or later is None:
                    skipped.append("transfer leg missing a flight endpoint")
                    continue
                concrete = resolve_leg_anchor(
                    planned,
                    facts=_facts_for(earlier, airport_info),
                    partner_facts=_facts_for(later, airport_info),
                    overrides=overrides,
                )
                block, diag = _build_transfer(concrete, route=route)
            elif planned.kind is LegKind.AIRPORT_DEPARTURE:
                flight = planned.to_flight
                if flight is None:
                    skipped.append("departure leg missing a flight")
                    continue
                concrete = resolve_leg_anchor(
                    planned, facts=_facts_for(flight, airport_info), overrides=overrides
                )
                block, diag = _build_departure(
                    concrete,
                    schedule=schedule,
                    home_address=home_address,
                    now=now,
                    live_origin=live_origin,
                    route=route,
                    boarding_present=boarding_present,
                )
            else:  # AIRPORT_ARRIVAL
                flight = planned.from_flight
                if flight is None:
                    skipped.append("arrival leg missing a flight")
                    continue
                concrete = resolve_leg_anchor(
                    planned, facts=_facts_for(flight, airport_info), overrides=overrides
                )
                block, diag = _build_arrival(
                    concrete,
                    schedule=schedule,
                    home_address=home_address,
                    route=route,
                    boarding_present=boarding_present,
                )
            if block is not None:
                desired.append(block)
            if diag is not None:
                skipped.append(diag)

    if extra_desired:
        desired.extend(extra_desired)

    reconcile_kwargs = {} if managed_legacy is None else {"managed_legacy": managed_legacy}
    plan = plan_reconcile(desired, current_blocks, **reconcile_kwargs)
    return EngineResult(plan=plan, skipped=tuple(skipped))

README.md

tile.json