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.

Quality

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

block_codec.pyskills/drive-engine/

"""Unified drive-block codec — machine-state writer and multi-shape reader.

One codec for every drive block the engine writes, replacing the two legacy codecs
(flight-assist `<!--fadrive:-->` and drive-planner `<!--dp:-->`).

Where the machine state lives
-----------------------------
State rides in the event's `extendedProperties.private` (the #178 target): a flat
`dengine_*`-namespaced string map, the only value type the field accepts, one key
per state field. `build_extended_properties` is the schema's source of truth and
what `calendar_apply` writes on create/patch. The human line stays in the
description on purpose — it is what the operator sees in the calendar UI; only the
machine state lives in `extendedProperties`.

Leg identity follows #156 C1 / G4 and keys on the CANONICAL flight identity, never
the designator:

    airport_departure / airport_arrival : <dep>-<arr>-<sched_dep_utc>   (+ kind)
    airport_transfer                    : <arr-flight-key>|<dep-flight-key>
    meeting                             : the calendar meeting id

so a codeshare tracked under two designators still maps to one block. `kind`
disambiguates the departure vs arrival block on the same flight.

Multi-shape reader (#156 R4 cutover): `parse_block` reads the current unified state
from `extendedProperties.private` AND recognizes both legacy description shapes,
tagging each with its generation so the reconcile can converge prior-gen blocks
(adopt-under-new-identity + delete legacy) and delete orphans, never double-stamp.
Malformed / unrecognized events parse to None (never raise), so one bad event
cannot abort a sweep.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from datetime import datetime

from chain import LegKind
from flight_identity import MergedFlight
from leg_anchor import ConcreteLeg

# --- schema + generations ---------------------------------------------------

UNIFIED_BLOCK_SCHEMA_VERSION = 1

GEN_UNIFIED = "unified"
GEN_LEGACY_FADRIVE = "legacy_fadrive"
GEN_LEGACY_DP = "legacy_dp"

# --- alert values -----------------------------------------------------------

ALERT_GROWTH = "growth"
ALERT_LEAVE_NOW = "leave_now"
_ALERT_VALUES = (ALERT_GROWTH, ALERT_LEAVE_NOW)

# --- extendedProperties.private state (the #178 migration target) -----------

# `extendedProperties.private` is a flat string→string map shared with any other
# tool that tags the event, so every key is `dengine_`-namespaced to avoid
# clobbering a neighbour's tag. Values are always strings (the only type the field
# accepts) — ints and datetimes are stringified on write and parsed back on read.
# The version key is spelled out per `coding-policy: stateful-artifacts`, which
# requires every record to carry an auditable `schema_version` field by that name;
# the namespace prefix keeps it collision-safe in the shared map.
_EXT_KEY_VERSION = "dengine_schema_version"
_EXT_KEY_LEG = "dengine_leg"
_EXT_KEY_KIND = "dengine_kind"
_EXT_KEY_BASELINE = "dengine_b"
_EXT_KEY_ANCHOR = "dengine_a"
_EXT_KEY_WINDOW_END = "dengine_we"
_EXT_KEY_ORIGIN = "dengine_o"
_EXT_KEY_DESTINATION = "dengine_d"
_EXT_KEY_ALERTED = "dengine_al"

# --- legacy markers (read-only, for cutover convergence) --------------------

_LEGACY_FADRIVE_MARKER_RE = re.compile(
    r"\[flight-assist:flight=(?P<id>[^:\]]+):dir=(?P<dir>[^:\]]+)\]"
)
_LEGACY_FADRIVE_STATE_RE = re.compile(r"<!--fadrive:(?P<json>\{.*?\})-->", re.DOTALL)
_LEGACY_DP_MARKER_RE = re.compile(r"\[drive-planner:meeting=(?P<id>[^:\]]+):dir=(?P<dir>[^:\]]+)\]")
_LEGACY_DP_STATE_RE = re.compile(r"<!--dp:(?P<json>\{.*?\})-->", re.DOTALL)


# --- leg identity -----------------------------------------------------------


def canonical_flight_key(flight: MergedFlight) -> str:
    """Stable per-flight identity string: route + scheduled-departure UTC instant.

    Excludes the designator (#156 W3): two codeshare records for one physical
    flight share this key. Minute precision — the identity tolerance lives in the
    merge, so by here the flight is already one canonical instant.
    """
    stamp = flight.scheduled_dep.strftime("%Y%m%dT%H%MZ")
    return f"{flight.dep_airport}-{flight.arr_airport}-{stamp}"


def leg_identity(leg: ConcreteLeg) -> str:
    """The marker identity for a concrete airport leg (#156 C1 / G4).

    Departure / arrival key on the single flight; transfer keys on the ordered
    pair `<arrival-flight>|<departure-flight>` so the between-flights block has its
    own identity distinct from either flight's own legs.
    """
    if leg.kind is LegKind.AIRPORT_TRANSFER:
        if leg.partner_flight is None:
            raise ValueError("transfer leg missing partner_flight for identity")
        return f"{canonical_flight_key(leg.flight)}|{canonical_flight_key(leg.partner_flight)}"
    return canonical_flight_key(leg.flight)


# --- alerts -----------------------------------------------------------------


def serialize_alerted(alerted: frozenset | set) -> str:
    """Serialize an alert set to the stable comma-joined record."""
    return ",".join(value for value in _ALERT_VALUES if value in alerted)


def parse_alerted(raw: object) -> frozenset:
    """Parse the comma-joined alert record into a set. Tolerant: unknown tokens
    dropped, non-string yields empty (a corrupt record must never crash a sweep)."""
    if not isinstance(raw, str):
        return frozenset()
    return frozenset(token.strip() for token in raw.split(",") if token.strip() in _ALERT_VALUES)


# --- build ------------------------------------------------------------------


def build_extended_properties(
    *,
    identity: str,
    kind: str,
    baseline_seconds: int,
    anchor: datetime,
    origin: str,
    destination: str,
    window_end: datetime | None = None,
    alerted: frozenset | set = frozenset(),
) -> dict:
    """The `extendedProperties` field VALUE carrying the block's machine state —
    the #178 migration target and the schema's source of truth.

    Returns `{"private": {...}}`, the value of the event resource's
    `extendedProperties` field — NOT a full event body. A writer nests it under
    the top-level key: `events.insert` / `events.patch` receive
    `{..., "extendedProperties": build_extended_properties(...)}`. The writer owns
    these events exclusively and re-asserts the COMPLETE `dengine_*` set every
    write, so whether Calendar merges or replaces the private map on patch does not
    matter. Every value is a string — the only type `extendedProperties.private`
    accepts — so `baseline_seconds` and the datetimes are stringified here and
    parsed back in `parse_block`. `window_end` is emitted only for transfer legs.

    Both the reader (`parse_block`) and the writer (`calendar_apply` create/patch)
    use this. It carries no human line — the description keeps that, since it is
    what the operator sees; only the machine state lives here.
    """
    private: dict[str, str] = {
        _EXT_KEY_VERSION: str(UNIFIED_BLOCK_SCHEMA_VERSION),
        _EXT_KEY_LEG: identity,
        _EXT_KEY_KIND: kind,
        _EXT_KEY_BASELINE: str(baseline_seconds),
        _EXT_KEY_ANCHOR: anchor.isoformat(),
        _EXT_KEY_ORIGIN: origin,
        _EXT_KEY_DESTINATION: destination,
        _EXT_KEY_ALERTED: serialize_alerted(alerted),
    }
    if window_end is not None:
        private[_EXT_KEY_WINDOW_END] = window_end.isoformat()
    return {"private": private}


# --- parse (extendedProperties + legacy-description reader) ------------------


@dataclass(frozen=True)
class ParsedBlock:
    """A drive block read off a calendar event, tagged with its generation.

    `generation` is `unified` for a new block, or a legacy tag the cutover
    reconcile uses to converge/delete. For unified blocks, `identity` + `kind`
    carry the leg identity and the state fields are populated. For legacy blocks,
    `legacy_id` + `legacy_direction` carry the old marker's identity so reconcile
    can match or orphan them; their state fields are best-effort.
    """

    generation: str
    event_id: str | None = None
    identity: str | None = None
    kind: str | None = None
    legacy_id: str | None = None
    legacy_direction: str | None = None
    baseline_seconds: int | None = None
    anchor: datetime | None = None
    window_end: datetime | None = None
    origin: str | None = None
    destination: str | None = None
    alerted: frozenset = field(default_factory=frozenset)


def _event_description(event: object) -> str | None:
    if not isinstance(event, dict):
        return None
    desc = event.get("description")
    return desc if isinstance(desc, str) else None


def _event_id(event: object) -> str | None:
    if isinstance(event, dict):
        eid = event.get("id")
        if isinstance(eid, str):
            return eid
    return None


def _parse_dt(raw: object) -> datetime | None:
    if not isinstance(raw, str):
        return None
    try:
        return datetime.fromisoformat(raw)
    except ValueError:
        return None


def _parse_int(raw: object) -> int | None:
    """Parse a baseline that may be a real int (description JSON) or a numeric
    string (`extendedProperties`, which stores every value as a string)."""
    if isinstance(raw, bool):
        return None
    if isinstance(raw, int):
        return raw
    if isinstance(raw, str):
        try:
            return int(raw)
        except ValueError:
            return None
    return None


def _event_extended_private(event: object) -> dict | None:
    """The event's `extendedProperties.private` map, or None when absent/malformed."""
    if not isinstance(event, dict):
        return None
    ext = event.get("extendedProperties")
    if not isinstance(ext, dict):
        return None
    private = ext.get("private")
    return private if isinstance(private, dict) else None


def _parse_extended_block(private: dict, event_id: str | None) -> ParsedBlock | None:
    """Read a unified block off `extendedProperties.private`, or None to fall back.

    Returns None when the map carries no current-version `dengine_*` state or lacks
    a usable leg identity — the caller then tries the two legacy description readers
    (fadrive / dp). A version other than the current one reads as "no unified state
    here" and returns None too; the writer and reader ship together, so a mismatch
    is only a transient rollout artifact that the next same-version sweep rewrites.
    """
    if private.get(_EXT_KEY_VERSION) != str(UNIFIED_BLOCK_SCHEMA_VERSION):
        return None
    identity = private.get(_EXT_KEY_LEG)
    kind = private.get(_EXT_KEY_KIND)
    if not (isinstance(identity, str) and identity and isinstance(kind, str) and kind):
        return None
    origin = private.get(_EXT_KEY_ORIGIN)
    destination = private.get(_EXT_KEY_DESTINATION)
    return ParsedBlock(
        generation=GEN_UNIFIED,
        event_id=event_id,
        identity=identity,
        kind=kind,
        baseline_seconds=_parse_int(private.get(_EXT_KEY_BASELINE)),
        anchor=_parse_dt(private.get(_EXT_KEY_ANCHOR)),
        window_end=_parse_dt(private.get(_EXT_KEY_WINDOW_END)),
        origin=origin if isinstance(origin, str) else None,
        destination=destination if isinstance(destination, str) else None,
        alerted=parse_alerted(private.get(_EXT_KEY_ALERTED)),
    )


def parse_block(event: object) -> ParsedBlock | None:
    """Read a drive block off a calendar event; extendedProperties + legacy (#178, R4).

    Reads the current unified state from `extendedProperties.private` (the #178
    migration target), falling back to the two legacy description markers. Returns a
    ParsedBlock tagged with its generation, or None for an event that carries no
    unified extended-properties state AND neither legacy description marker (or is
    malformed). Never raises — a single bad event must not abort a sweep.
    """
    eid = _event_id(event)

    private = _event_extended_private(event)
    if private is not None:
        extended = _parse_extended_block(private, eid)
        if extended is not None:
            return extended

    desc = _event_description(event)
    if desc is None:
        return None

    fadrive = _LEGACY_FADRIVE_MARKER_RE.search(desc)
    if fadrive and _LEGACY_FADRIVE_STATE_RE.search(desc):
        return ParsedBlock(
            generation=GEN_LEGACY_FADRIVE,
            event_id=eid,
            legacy_id=fadrive["id"],
            legacy_direction=fadrive["dir"],
        )

    dp = _LEGACY_DP_MARKER_RE.search(desc)
    if dp and _LEGACY_DP_STATE_RE.search(desc):
        return ParsedBlock(
            generation=GEN_LEGACY_DP,
            event_id=eid,
            legacy_id=dp["id"],
            legacy_direction=dp["dir"],
        )

    return None

README.md

tile.json