{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://preig.github.io/atiakit/3c/trip-record.schema.json",
  "title": "Agent-ready trip record",
  "description": "Minimum viable trip state for one traveler party: party as counts and age bands, dates, budget band, preferences, short list, rejected options with reasons, reversible holds, open questions and consent. There are no fields for names, contact details or payment data, and free text that looks like an email, phone, card or ID number is rejected. Teaching scaffold from the ATIA AI Take-Home Kit, tool 3.c (Alaska SBDC). Bay Light Day Cruises, Seward is a fictional teaching business. Schema version 0.1, 2026-10-10.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "record_id",
    "version",
    "updated_at",
    "party",
    "dates",
    "budget_band",
    "preferences",
    "shortlisted",
    "rejected",
    "holds",
    "open_questions",
    "consent"
  ],
  "properties": {
    "$schema": {
      "description": "Optional link to this schema, for editors.",
      "type": "string",
      "maxLength": 200
    },
    "record_id": {
      "description": "Opaque id for this trip record. Never built from a name, email, phone or booking reference.",
      "type": "string",
      "pattern": "^TR-[A-Z0-9]+(-[A-Z0-9]+)*$",
      "maxLength": 40
    },
    "version": {
      "description": "Revision number. It goes up by 1 on every change. A server rejects a write whose version is not the stored version plus 1, so two agents cannot overwrite each other silently.",
      "type": "integer",
      "minimum": 1
    },
    "updated_at": {
      "description": "When this revision was written, as an ISO 8601 date-time with a time zone. A server may replace it with its own clock.",
      "$ref": "#/$defs/dateTime"
    },
    "notes": {
      "description": "Short free text for staff. No personal data.",
      "$ref": "#/$defs/safeText",
      "maxLength": 500
    },
    "party": {
      "description": "Who is traveling, as counts only. No names, no birth dates.",
      "type": "object",
      "additionalProperties": false,
      "required": ["count", "age_bands"],
      "properties": {
        "count": {"type": "integer", "minimum": 1, "maximum": 60},
        "age_bands": {
          "description": "Counts per age band. The counts add up to party.count. Bands are narrow for children because minimum ages decide what fits.",
          "type": "array",
          "minItems": 1,
          "maxItems": 11,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["band", "count"],
            "properties": {
              "band": {"enum": ["0-1", "2-3", "4-5", "6-7", "8-9", "10-11", "12-13", "14-15", "16-17", "18-64", "65+"]},
              "count": {"type": "integer", "minimum": 1, "maximum": 60}
            }
          }
        }
      }
    },
    "dates": {
      "description": "Arrival and departure in the destination. depart is on or after arrive.",
      "type": "object",
      "additionalProperties": false,
      "required": ["arrive", "depart"],
      "properties": {
        "arrive": {"$ref": "#/$defs/date"},
        "depart": {"$ref": "#/$defs/date"},
        "flexible_days": {"type": "integer", "minimum": 0, "maximum": 14}
      }
    },
    "budget_band": {
      "description": "Activities budget for the whole party, in US dollars, as a band. Never a card, an account or an exact spend.",
      "enum": ["not_stated", "under_500_usd", "500_to_1000_usd", "1000_to_2500_usd", "2500_to_5000_usd", "over_5000_usd"]
    },
    "preferences": {
      "description": "What the party wants and wants to avoid. Record questions about the business (for example step-free access) in open_questions, never a person's health details.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "interests": {"type": "array", "maxItems": 10, "items": {"$ref": "#/$defs/safeText", "minLength": 2, "maxLength": 60}},
        "avoid": {"type": "array", "maxItems": 10, "items": {"$ref": "#/$defs/safeText", "minLength": 2, "maxLength": 60}},
        "pace": {"enum": ["relaxed", "moderate", "full"]},
        "time_of_day": {"enum": ["morning", "afternoon", "either"]},
        "has_car": {"type": "boolean"},
        "max_drive_minutes": {"type": "integer", "minimum": 0, "maximum": 600}
      }
    },
    "shortlisted": {
      "description": "Options still in play. Every reason cites at least one approved fact by id.",
      "type": "array",
      "maxItems": 20,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["product_id", "reason", "source_fact_ids"],
        "properties": {
          "product_id": {"$ref": "#/$defs/code"},
          "date": {"$ref": "#/$defs/date"},
          "departure": {"$ref": "#/$defs/time"},
          "reason": {"$ref": "#/$defs/safeText", "minLength": 3, "maxLength": 280},
          "source_fact_ids": {"type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true, "items": {"$ref": "#/$defs/code"}}
        }
      }
    },
    "rejected": {
      "description": "Options set aside, with the reason, so the next visit or the next agent does not offer them again.",
      "type": "array",
      "maxItems": 30,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["product_id", "reason"],
        "properties": {
          "product_id": {"$ref": "#/$defs/code"},
          "date": {"$ref": "#/$defs/date"},
          "departure": {"$ref": "#/$defs/time"},
          "rule": {"enum": ["min_age", "time_conflict", "weather_rule", "season", "availability", "budget", "preference", "other"]},
          "reason": {"$ref": "#/$defs/safeText", "minLength": 3, "maxLength": 280},
          "source_fact_ids": {"type": "array", "maxItems": 10, "uniqueItems": true, "items": {"$ref": "#/$defs/code"}}
        }
      }
    },
    "holds": {
      "description": "Reversible, time-limited holds. A hold is not a booking. It expires on the server even if the agent disappears. Only a person at the business confirms.",
      "type": "array",
      "maxItems": 20,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["hold_id", "product_id", "date", "departure", "seats", "status", "expires_at"],
        "properties": {
          "hold_id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{2,63}$"},
          "product_id": {"$ref": "#/$defs/code"},
          "date": {"$ref": "#/$defs/date"},
          "departure": {"$ref": "#/$defs/time"},
          "seats": {"type": "integer", "minimum": 1, "maximum": 60},
          "status": {"enum": ["active", "pending_staff_confirmation", "confirmed", "released", "expired", "declined"]},
          "placed_at": {"$ref": "#/$defs/dateTime"},
          "expires_at": {"$ref": "#/$defs/dateTime"},
          "confirmation_id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{2,63}$"}
        }
      }
    },
    "open_questions": {
      "description": "Missing facts and decisions, with who should answer. A gap stays a question; nobody fills it with a guess.",
      "type": "array",
      "maxItems": 30,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["question", "for", "status"],
        "properties": {
          "id": {"type": "string", "pattern": "^Q[0-9]{1,3}$"},
          "question": {"$ref": "#/$defs/safeText", "minLength": 3, "maxLength": 280},
          "for": {"enum": ["owner", "partner", "staff", "traveler"]},
          "status": {"enum": ["open", "answered"]},
          "answer": {"$ref": "#/$defs/safeText", "maxLength": 280}
        }
      }
    },
    "consent": {
      "description": "What the traveler agreed to for this record. agent_may_confirm is always false: an agent may hold and ask, and a person at the business confirms.",
      "type": "object",
      "additionalProperties": false,
      "required": ["purpose", "traveler_reviewed", "agent_may_place_holds", "agent_may_confirm", "retention_days"],
      "properties": {
        "purpose": {"enum": ["trip_planning"]},
        "traveler_reviewed": {"type": "boolean"},
        "agent_may_place_holds": {"type": "boolean"},
        "agent_may_confirm": {"const": false},
        "human_contact_offered": {"type": "boolean"},
        "retention_days": {"type": "integer", "minimum": 1, "maximum": 400},
        "recorded_at": {"$ref": "#/$defs/dateTime"}
      }
    }
  },
  "$defs": {
    "date": {
      "type": "string",
      "format": "date",
      "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"
    },
    "time": {
      "description": "Local departure time, 24-hour HH:MM.",
      "type": "string",
      "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$"
    },
    "dateTime": {
      "type": "string",
      "format": "date-time",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}(:[0-9]{2}([.][0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2})$"
    },
    "code": {
      "description": "Product or fact id from the business's own catalog, for example BL-CRUISE-4H or BL-MIN-AGE.",
      "type": "string",
      "pattern": "^[A-Z0-9]+(-[A-Z0-9]+)*$",
      "maxLength": 40
    },
    "safeText": {
      "description": "Free text that does not look like personal data.",
      "type": "string",
      "not": {"$ref": "#/$defs/looksLikePersonalData"}
    },
    "looksLikePersonalData": {
      "description": "Email address, North American phone number, 13 to 19 digit card or account number, or US Social Security number pattern.",
      "anyOf": [
        {"pattern": "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}"},
        {"pattern": "(^|[^0-9A-Za-z])([+]?1[ .-]?)?[(]?[2-9][0-9]{2}[)]?[ .-]?[0-9]{3}[ .-]?[0-9]{4}([^0-9A-Za-z]|$)"},
        {"pattern": "(^|[^0-9])([0-9][ -]?){12,18}[0-9]([^0-9]|$)"},
        {"pattern": "(^|[^0-9])[0-9]{3}-[0-9]{2}-[0-9]{4}([^0-9]|$)"}
      ]
    }
  }
}
