CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

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.

Quality

89%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

meeting_source.pyskills/drive-engine/

"""Meeting-leg source — genuine ground meetings → desired drive blocks — pure.

The unified engine's meeting half (#156 leg table, `meeting` row). It consumes the
proven drive-planner `scan` classification (virtual/declined/past/skip filtering,
identity-based flight exclusion, per-meeting anchor resolution) and turns each
actionable meeting's legs into unified `DesiredBlock`s that feed the SAME reconcile
as the airport legs — so one engine owns both, no two-engine collision.

Two things make these correct where drive-planner's blocks went wrong:

- **Travel-awareness.** The origin comes from `position_at` (via the scan's
  `anchor_for`), so on a trip it resolves to where the operator actually is. A leg
  whose routed drive is implausibly long — the operator is abroad while the meeting
  is at home — is SUPPRESSED rather than invented. This is what stops "drive to
  Tennessee swim practice" appearing while the operator is in Europe.
- **Display timezone.** Each block carries the operator's current IANA zone when
  the core `current-tz` reader has one (#301), the meeting's own zone otherwise —
  so the block and the operator notice read on the clock the operator is looking
  at, never a stale offset a source event dragged home from a trip.

Pure: the caller runs the scan (I/O: calendar fetch) and supplies a `route` fn; a
route failure or unresolved anchor skips that leg with a diagnostic, never a block.
"""

from __future__ import annotations

import sys
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path

_BUNDLE_DIR = Path(__file__).resolve().parent
if str(_BUNDLE_DIR) not in sys.path:
    sys.path.insert(0, str(_BUNDLE_DIR))

from block_codec import parse_block  # noqa: E402
from reconcile import DesiredBlock, HeldLeg  # noqa: E402
from scan import _MARKER_RE, same_place  # noqa: E402

# A drive block's human summary always starts with this.
_DRIVE_SUMMARY_PREFIX = "Drive:"


def exclude_drive_block_events(events: list[dict]) -> list[dict]:
    """Filter the meeting scan input (#156 — calendar-as-output).

    Drops the engine's own drive blocks so a later sweep never treats one as a
    meeting and plans a drive TO the drive block (self-referential duplicate). A
    `Drive:`-prefixed block with an unreadable marker is dropped too, except a
    block the retired drive-planner left on the calendar: `scan` reads its
    marker to bucket that meeting as `has_block`, so no duplicate is planned.
    """
    kept: list[dict] = []
    for event in events:
        parsed = parse_block(event)
        if parsed is not None:
            continue  # the engine's own block — not a meeting
        summary = event.get("summary") if isinstance(event, dict) else None
        if isinstance(summary, str) and summary.strip().startswith(_DRIVE_SUMMARY_PREFIX):
            description = event.get("description")
            if isinstance(description, str) and _MARKER_RE.search(description):
                kept.append(event)  # a retired drive-planner block: scan needs its marker
            continue  # a Drive: block with an unreadable marker — not a meeting
        kept.append(event)
    return kept


RouteFn = Callable[[str, str], "timedelta | None"]

# Above this a meeting drive is a road trip (owner, #377): planned only when
# TripIt has no flight or train that gets him there, and the notice says the
# drive was assumed. Under it, an ordinary meeting drive.
DEFAULT_MAX_REASONABLE_DRIVE = timedelta(hours=3)
# A routed drive past this is a missing booking, not a road trip.
ROAD_TRIP_MAX = timedelta(hours=9)
# A flight or train landing within this before the event (or leaving within
# this after it) is how he gets there (or back).
BOOKING_WINDOW = timedelta(hours=24)

# A zero or at-most-one-minute route is not a useful calendar drive (#356).
# Treat it as a definitive no-drive, so an earlier bogus block is not retained.
MIN_MEETING_DRIVE = timedelta(minutes=1)

# scan directions → unified meeting leg kinds (distinct so reconcile keys uniquely).
_DIRECTION_KIND = {
    "outbound": "meeting_outbound",
    "bridge": "meeting_outbound",
    "return": "meeting_return",
}


@dataclass(frozen=True)
class TripPresence:
    """Where the operator sleeps during a trip they have confirmed they drive to.

    `scan` resolves ONE anchor per meeting, at the event's start, and both of
    that meeting's legs use it. On a trip whose check-in is stamped after the
    first event that anchor is home, so the drive back from that event was
    planned to home — a 4-hour drive out of the middle of a trip, with the next
    morning's drive starting from a hotel nothing ever reached.

    The operator sleeps at the lodging from the first event of the trip until
    the last. So `lodging` replaces a home endpoint except where home is real:
    the drive OUT to the first event (they leave from home) and the drive BACK
    from the last (`lodging_source` plans the actual drive home).
    """

    lodging: str
    is_first: bool
    is_last: bool


def _trip_endpoints(leg, presence: TripPresence | None) -> tuple[str, str]:
    """A leg's endpoints with a mid-trip home rewritten to the trip's lodging.

    Off a driving trip, or where home is the real endpoint, the leg is returned
    unchanged. `leg.origin` / `leg.destination` are non-None at every call site.
    """
    origin, destination = leg.origin, leg.destination
    if presence is None:
        return origin, destination
    if leg.direction == "return":
        # Back from the LAST event, home is real — `lodging_source` plans that
        # drive, and this leg deferring to it keeps one owner for the way home.
        if not presence.is_last and destination != presence.lodging:
            destination = presence.lodging
    elif leg.direction == "outbound" and not presence.is_first:
        # Out to any event but the FIRST, the operator sets off from the room,
        # not the house they left days ago.
        #
        # `outbound` explicitly, never "not a return": a BRIDGE leg runs venue
        # to venue between two tight-gap meetings, and its origin is the prior
        # venue rather than the anchor. Rewriting that to the lodging invents a
        # detour through the hotel between back-to-back events (#243 review).
        if origin != presence.lodging:
            origin = presence.lodging
    return origin, destination


# Scan buckets whose meeting still exists but whose legs cannot be planned now:
# one already under way (its return drive may still be ahead), and one whose
# time did not parse. Their blocks are kept, never orphan-deleted (#334). A
# declined, cancelled, skipped or virtual meeting is deliberately driveless.
_UNKNOWABLE_REASONS = frozenset({"missing or unparseable time"})


def booked_trips(schedule: list[dict] | None) -> list[tuple[datetime, datetime]]:
    """(departure, arrival) of every timed TripIt Flight and Rail record."""
    trips = []
    for record in schedule or []:
        if not isinstance(record, dict) or record.get("type") not in ("Flight", "Rail"):
            continue
        try:
            start = datetime.fromisoformat(str(record.get("start")).replace("Z", "+00:00"))
            end = datetime.fromisoformat(str(record.get("end")).replace("Z", "+00:00"))
        except ValueError:
            continue
        if start.tzinfo is not None and end.tzinfo is not None:
            trips.append((start, end))
    return trips


def _booked(leg, booked: list[tuple[datetime, datetime]]) -> bool:
    """A flight or train gets him to this leg's event, or away from it."""
    if leg.direction == "return":
        after = leg.depart_after
        return after is not None and any(
            after <= dep <= after + BOOKING_WINDOW for dep, _ in booked
        )
    by = leg.arrive_by
    return by is not None and any(by - BOOKING_WINDOW <= arr <= by for _, arr in booked)


def _legs_unknowable(meeting) -> bool:
    bucket = getattr(meeting, "bucket", None)
    return bucket == "past" or (
        bucket == "filtered" and getattr(meeting, "reason", None) in _UNKNOWABLE_REASONS
    )


def _hold(skipped: list[str], held: list[HeldLeg], meeting, leg, kind: str, reason: str) -> None:
    """Record a meeting leg that exists but could not be computed (#334)."""
    skipped.append(reason)
    held.append(
        HeldLeg(
            identity=meeting.meeting_id,
            kind=kind,
            summary=f"Drive: {meeting.summary}",
            when=leg.arrive_by or leg.depart_after,
            reason=reason,
            not_before=leg.depart_after
            or (
                leg.arrive_by - timedelta(seconds=leg.gap_seconds)
                if isinstance(leg.gap_seconds, int) and leg.arrive_by is not None
                else None
            ),
        )
    )


def meeting_desired_blocks(
    meetings: list,
    *,
    route: RouteFn,
    max_reasonable_drive: timedelta = DEFAULT_MAX_REASONABLE_DRIVE,
    driving_to: dict[str, TripPresence] | None = None,
    display_tz: str | None = None,
    booked: list[tuple[datetime, datetime]] | None = None,
    prep_arrival: Mapping[str, datetime] | None = None,
    prep_unavailable: frozenset[str] = frozenset(),
) -> tuple[list[DesiredBlock], list[str], list[HeldLeg]]:
    """Turn scan `MeetingClass` results into unified meeting `DesiredBlock`s.

    Only actionable meetings carry legs, so pass the scan output directly. Returns
    `(blocks, skipped_diagnostics, held)`. A leg is skipped (never blindly
    blocked) when its anchor is unresolved or its route fails. Each of those is
    also HELD (#334): the leg exists, so its current block is kept rather than
    orphan-deleted. Only a leg whose origin is its destination is dropped.

    A drive over `max_reasonable_drive` is a road trip (#377). With a `booked`
    (departure, arrival) flight or train landing in the BOOKING_WINDOW before
    the event, or leaving in the one after it, the leg is dropped: the airport
    or station drives replace it. Without one it is planned and marked
    `road_trip`, so the notice says the drive was assumed; past ROAD_TRIP_MAX
    it is held as a missing booking instead.

    A leg whose origin IS its destination is skipped too (#301): the scan
    already filters a meeting held at the anchor, but the lodging rewrite below
    can collapse a leg the same way — a venue that is the trip's hotel — and a
    zero-length drive would still land as a degenerate one-minute block. The
    comparison is `scan.same_place`: the schedule's lodging address is only
    stripped, the scan's location is whitespace-collapsed, and either may
    differ in case.

    `driving_to` are the meeting ids the away-suppression must NOT fire on: the
    events of a flight-less trip the operator has confirmed they DRIVE to. The
    suppression reads a long drive as "not positioned to make it — they flew, or
    are elsewhere", and for those meetings that premise is known false. A
    check-in stamped after the trip's first event makes the anchor resolve home,
    so the getting-there drive routes home→venue at its true length and the cap
    deleted the very event the trip exists for (#242).

    For those same meetings a home endpoint is rewritten to the trip's lodging
    unless home is real there — see `TripPresence`.

    `display_tz` is the operator's current IANA zone from the core `current-tz`
    reader, or None when it has none. It is the zone every block is written in
    and the notice renders its "at HH:MM" from (#301). The instant is always the
    source event's; only the wall-clock it is shown on changes. A source event
    can carry a stale offset from a trip (`+02:00` on an `America/Chicago` event
    after Amsterdam), and the scan's offset-derived `Etc/GMT±N` fallback keeps
    that instant right while showing it on the wrong clock; the operator's zone
    is the clock Google Calendar shows them the same event on. None keeps the
    meeting's own zone. A block written in the operator's zone is marked
    `zone_from_operator`, so an existing block in another zone converges to it
    on the next sweep (#309) while a fallback zone never triggers a re-patch.

    `prep_arrival` maps a talk's meeting id to the start of its talk-prep Prep
    hold (#381; read by `prep_window.prep_targets`). The talk's ARRIVING leg
    (outbound or bridge) then arrives at the Prep start instead of the talk
    start, and the block carries `prep_for` = the talk start for the notice.
    A return leg never changes. A bridge leg retargets only when the drive also
    fits before Prep; otherwise it keeps the talk-start target with a skipped
    diagnostic, the one plan that can still be driven (Q1). Its `earliest_start`
    stays the previous meeting's end. `prep_unavailable` are talks whose Prep
    read failed: their arriving leg is HELD (#334), never re-targeted back to the
    talk start on a provider blip.
    """
    presence = driving_to or {}
    prep = prep_arrival or {}
    blocks: list[DesiredBlock] = []
    skipped: list[str] = []
    held: list[HeldLeg] = []

    for meeting in meetings:
        # Recorded attendance is positive no-anchor-drive evidence even for
        # an ongoing/past event. Do not retain a phantom future return through
        # the generic past-event hold; reconcile still preserves drive history.
        if _legs_unknowable(meeting) and getattr(meeting, "attendance_id", None) is None:
            for kind in ("meeting_outbound", "meeting_return"):
                held.append(
                    HeldLeg(
                        identity=meeting.meeting_id,
                        kind=kind,
                        summary=f"Drive: {meeting.summary}",
                        when=(
                            meeting.start
                            if kind == "meeting_outbound"
                            else meeting.end or meeting.start
                        ),
                        reason=f"meeting {meeting.meeting_id}: {meeting.reason}",
                        announce=False,
                        not_before=meeting.end if kind == "meeting_return" else None,
                    )
                )
            continue
        for leg in meeting.legs:
            tag = f"meeting {meeting.meeting_id} {leg.direction}"
            kind = _DIRECTION_KIND.get(leg.direction)
            if kind is None:
                skipped.append(f"{tag}: unknown direction")
                continue

            if leg.origin is None or leg.destination is None:
                _hold(
                    skipped,
                    held,
                    meeting,
                    leg,
                    kind,
                    f"{tag}: {leg.anchor_note or 'unresolved anchor'}",
                )
                continue
            origin, destination = _trip_endpoints(leg, presence.get(meeting.meeting_id))
            if same_place(origin, destination):
                skipped.append(f"{tag}: origin is the destination — no drive")
                continue
            drive = route(origin, destination)
            if drive is None:
                _hold(skipped, held, meeting, leg, kind, f"{tag}: route failed")
                continue
            if drive < timedelta(0):
                _hold(
                    skipped,
                    held,
                    meeting,
                    leg,
                    kind,
                    f"{tag}: negative route duration — check route provider",
                )
                continue
            if drive <= MIN_MEETING_DRIVE:
                skipped.append(f"{tag}: route at or below one minute — no drive")
                continue
            road_trip = drive > max_reasonable_drive and meeting.meeting_id not in presence
            if road_trip:
                minutes = int(drive.total_seconds() // 60)
                if _booked(leg, booked or []):
                    skipped.append(
                        f"{tag}: {minutes}min drive — a TripIt flight or train covers it"
                    )
                    continue
                if drive > ROAD_TRIP_MAX:
                    _hold(
                        skipped,
                        held,
                        meeting,
                        leg,
                        kind,
                        f"{tag}: {minutes}min drive and no flight or train in TripIt "
                        "— a missing booking?",
                    )
                    continue

            if leg.direction != "return" and meeting.meeting_id in prep_unavailable:
                _hold(
                    skipped, held, meeting, leg, kind, f"{tag}: talk-prep Prep window unavailable"
                )
                continue

            # Bridge leg: the drive must fit the gap between two consecutive
            # meetings. A drive longer than the gap can't be made (the "5h drive
            # inside a 45-min gap" case, #85) — suppress rather than create it.
            gap = getattr(leg, "gap_seconds", None)
            if isinstance(gap, int) and drive.total_seconds() > gap:
                _hold(
                    skipped,
                    held,
                    meeting,
                    leg,
                    kind,
                    f"{tag}: {int(drive.total_seconds() // 60)}min drive exceeds the "
                    f"{gap // 60}min gap — suppressed",
                )
                continue

            prep_for: datetime | None = None
            if leg.direction == "return":
                depart_after: datetime | None = leg.depart_after
                if depart_after is None:
                    _hold(
                        skipped, held, meeting, leg, kind, f"{tag}: return leg missing depart_after"
                    )
                    continue
                start, end, anchor = depart_after, depart_after + drive, depart_after
            else:
                arrive_by: datetime | None = leg.arrive_by
                if arrive_by is None:
                    _hold(
                        skipped, held, meeting, leg, kind, f"{tag}: arrival leg missing arrive_by"
                    )
                    continue
                target = arrive_by
                prep_start = prep.get(meeting.meeting_id)
                if prep_start is not None and prep_start < arrive_by:
                    # Arrive by the Prep start (#381). A bridge drive must still
                    # leave after the previous meeting ends.
                    if isinstance(gap, int) and drive > timedelta(seconds=gap) - (
                        arrive_by - prep_start
                    ):
                        skipped.append(
                            f"{tag}: {int(drive.total_seconds() // 60)}min drive from the "
                            "previous meeting can't reach the talk-prep Prep start — "
                            "arriving by the talk start"
                        )
                    else:
                        target, prep_for = prep_start, arrive_by
                start, end, anchor = target - drive, target, target

            blocks.append(
                DesiredBlock(
                    identity=meeting.meeting_id,
                    kind=kind,
                    summary=f"Drive: {meeting.summary}",
                    start=start,
                    end=end,
                    origin=origin,
                    destination=destination,
                    baseline_seconds=int(drive.total_seconds()),
                    anchor=anchor,
                    timezone=display_tz or getattr(meeting, "timezone", None),
                    zone_from_operator=bool(display_tz),
                    road_trip=road_trip,
                    earliest_start=(
                        leg.arrive_by - timedelta(seconds=gap)
                        if isinstance(gap, int) and leg.arrive_by is not None
                        else None
                    ),
                    prep_for=prep_for,
                )
            )

    return blocks, skipped, held

skills

LICENSE

README.md

tile.json