{
  "generated_by": "emit-schema-graph",
  "interfaces": [
    {
      "api_name": "hospitality.interface.approvable",
      "description": "Objects that can be the target of a proposal-0020 approval gate. `request_approval_gate` targets this interface polymorphically; each implementer's `approval_gate_for_<type>` link (declared `binds_to_interface`) is the binding edge registry dispatch resolves, so a new approvable type lands without touching the action or handler. A pure capability marker — the contract is the binding link, not a property shape.",
      "display_name": "Approvable"
    },
    {
      "api_name": "hospitality.interface.expirable",
      "description": "Objects with a wall-clock lapse. The §9.1 expiry sweep (`aerandil-api::workflow_expiry`) enumerates implementers from the registry and transitions overdue live rows through each implementer's declared expire action — implementing this interface is what puts a type on the sweep, and the sweep's behaviour table is test-gated to cover every implementer.",
      "display_name": "Expirable"
    },
    {
      "api_name": "hospitality.interface.retention_bound",
      "description": "Objects held on a storage-limitation clock (GDPR Art. 5(1)(e)). The retention sweep (`aerandil-api::retention_sweep`) enumerates implementers from the registry and, once a live row's `retain_until` has passed, applies `expire_retained_record`: every marked property is cleared and the row is soft-deleted, so it is neither held in readable form nor returned by any read. Implementing this interface is what puts a type on the sweep; an implementer must keep its marked properties optional so the cleared row still validates.",
      "display_name": "Retention-bound"
    }
  ],
  "links": [
    {
      "api_name": "hospitality.link.access_credential_for_stay",
      "cardinality": "N:1",
      "description": "The stay this credential is bound to — the load-bearing edge of tap-to-charge: a tap resolves token → credential → stay → open folio. A stay can hold several credentials (two keys cut for a double room); each credential binds to exactly one stay and is revoked when that stay checks out.",
      "domain": "access",
      "from": "hospitality.access_credential",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.agent_installation_for_registry_entry",
      "cardinality": "N:1",
      "description": "The registry entry an installation instantiates; several installations of one entry coexist only when their scope and enabled-scope/allowed-key hashes differ.",
      "domain": "agents",
      "from": "hospitality.agent_installation",
      "to": "hospitality.agent_registry_entry"
    },
    {
      "api_name": "hospitality.link.agent_thread_message_in_thread",
      "cardinality": "N:1",
      "description": "The thread a turn belongs to (agent-stack design D9: threads persist server-side; clients stay stateless). Written by append_agent_thread_message alongside the thread_ref property; branch structure rides parent_message_ref on the message.",
      "domain": "agents",
      "from": "hospitality.agent_thread_message",
      "to": "hospitality.agent_thread"
    },
    {
      "api_name": "hospitality.link.allergy_alert_for_person",
      "cardinality": "N:1",
      "description": "Whose allergy alert this is; each alert belongs to exactly one person (multi-allergen persons carry one alert per allergen), and the reverse role lets the pre-shift / kitchen surface enumerate every active alert for an arriving guest in one neighbor walk.",
      "domain": "people",
      "from": "hospitality.allergy_alert",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.allergy_alert_references_external_record",
      "cardinality": "N:1",
      "description": "Optional clinical seam for a confirmed allergy: skin-prick / specific-IgE / oral-food-challenge confirmations attach an opaque external-record reference for downstream audit, while self-reported alerts have no record to cite — no clinical payload ever lives in Aerandil.",
      "domain": "people",
      "from": "hospitality.allergy_alert",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.appointment_at_outlet",
      "cardinality": "N:1",
      "description": "The outlet an appointment trades under for revenue reporting; settle_appointment reads its USALI department for the charge bucket (health_club_spa fallback).",
      "domain": "spa",
      "from": "hospitality.appointment",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.appointment_for_person",
      "cardinality": "N:1",
      "description": "The guest the appointment is for; optional — a walk-in may book anonymously at the desk.",
      "domain": "spa",
      "from": "hospitality.appointment",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.appointment_for_service",
      "cardinality": "N:1",
      "description": "The service an appointment books; price and the buffer pair were snapshotted from it at booking time.",
      "domain": "spa",
      "from": "hospitality.appointment",
      "to": "hospitality.spa_service"
    },
    {
      "api_name": "hospitality.link.appointment_in_treatment_room",
      "cardinality": "N:1",
      "description": "The room an appointment occupies over [start_at - pre_time, end_at + cleanup_time]; optional at booking — assignment may come later via reschedule_appointment.",
      "domain": "spa",
      "from": "hospitality.appointment",
      "to": "hospitality.treatment_room"
    },
    {
      "api_name": "hospitality.link.appointment_references_external_record",
      "cardinality": "N:1",
      "description": "The external clinical record a medically-licensed appointment binds to (proposal 0005 structural seam); written by book_appointment when the service's modality carries requires_medical_licensing = true.",
      "domain": "spa",
      "from": "hospitality.appointment",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.appointment_settled_to_folio",
      "cardinality": "N:1",
      "description": "The folio an appointment's aggregated spa charge posted to; set by settle_appointment on the room-charge tender.",
      "domain": "spa",
      "from": "hospitality.appointment",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.appointment_with_provider",
      "cardinality": "N:1",
      "description": "The provider (therapist) delivering the appointment; optional at booking — a provider is single-booked across the buffered window once assigned.",
      "domain": "spa",
      "from": "hospitality.appointment",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.approval_decision_for_gate",
      "cardinality": "N:1",
      "description": "Which gate this decision was cast against; a gate accumulates a decision history and each decision belongs to exactly one gate.",
      "domain": "approvals",
      "from": "hospitality.approval_decision",
      "to": "hospitality.approval_gate"
    },
    {
      "api_name": "hospitality.link.approval_decision_supersedes_decision",
      "cardinality": "N:1",
      "description": "The prior decision this one supersedes on the same gate; a same-approver/same-gate/non-terminal precondition is enforced by the supersedes-same-approver-only validator.",
      "domain": "approvals",
      "from": "hospitality.approval_decision",
      "to": "hospitality.approval_decision"
    },
    {
      "api_name": "hospitality.link.approval_gate_for_automation",
      "cardinality": "N:1",
      "description": "The automation whose staged effect an approval gate guards; one gate per requires_approval firing, registered for approvable-interface dispatch (deep-scan §4.1).",
      "domain": "approvals",
      "from": "hospitality.approval_gate",
      "to": "hospitality.automation"
    },
    {
      "api_name": "hospitality.link.approval_gate_for_federated_analysis_definition",
      "cardinality": "N:1",
      "description": "The definition an approval gate guards; the gate-side pointer paired with federated_definition_reviewed_by_gate, registered for approvable-interface dispatch.",
      "domain": "approvals",
      "from": "hospitality.approval_gate",
      "to": "hospitality.federated_analysis_definition"
    },
    {
      "api_name": "hospitality.link.approval_gate_for_federated_output",
      "cardinality": "N:1",
      "description": "The output an approval gate guards; the gate-side pointer paired with federated_output_reviewed_by_gate, registered for approvable-interface dispatch.",
      "domain": "approvals",
      "from": "hospitality.approval_gate",
      "to": "hospitality.federated_output"
    },
    {
      "api_name": "hospitality.link.approval_gate_for_owner_statement",
      "cardinality": "N:1",
      "description": "The approvable-interface binding edge for owner statements: `request_approval_gate` targeting a statement anchors its gate here, and the gate decision transitions it (submitted → approved / changes_requested).",
      "domain": "approvals",
      "from": "hospitality.approval_gate",
      "to": "hospitality.owner_statement"
    },
    {
      "api_name": "hospitality.link.approval_gate_for_property_budget",
      "cardinality": "N:1",
      "description": "The approvable-interface binding edge for annual budgets: the gate decision transitions the budget (submitted → approved / objected — the PHR HMA's own vocabulary).",
      "domain": "approvals",
      "from": "hospitality.approval_gate",
      "to": "hospitality.property_budget"
    },
    {
      "api_name": "hospitality.link.approval_gate_for_purpose_grant",
      "cardinality": "N:1",
      "description": "The approvable-interface binding edge for purpose grants: `request_approval_gate` targeting a grant anchors its gate here, and the gate decision transitions the grant (requested → active / rejected).",
      "domain": "approvals",
      "from": "hospitality.approval_gate",
      "to": "hospitality.purpose_grant"
    },
    {
      "api_name": "hospitality.link.approval_gate_for_workflow_step_run",
      "cardinality": "N:1",
      "description": "The step run this gate blocks — the step cannot proceed until the gate resolves; optional because gates also guard standalone single-action approvals that have no parent step.",
      "domain": "approvals",
      "from": "hospitality.approval_gate",
      "to": "hospitality.workflow_step_run"
    },
    {
      "api_name": "hospitality.link.assessment_for_membership",
      "cardinality": "N:1",
      "description": "Whose longevity assessment this is; assessments bind to the individual membership, not the account — the spouse's lab work is distinct from the primary's.",
      "domain": "membership",
      "from": "hospitality.longevity_assessment",
      "to": "hospitality.membership"
    },
    {
      "api_name": "hospitality.link.assessment_references_external_record",
      "cardinality": "N:1",
      "description": "The mandatory clinical seam: every longevity assessment points at an external clinical record — an opaque reference, never clinical payload in Aerandil. record_assessment writes this link alongside assessment_for_membership, enforcing presence by construction because object validators cannot see the link graph.",
      "domain": "membership",
      "from": "hospitality.longevity_assessment",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.asset_at_venue",
      "cardinality": "N:1",
      "description": "The venue a piece of equipment stands at — the kitchen's walk-in, the spa's sauna heater. At most one of this and `asset_in_room` (validator-enforced).",
      "domain": "operations",
      "from": "hospitality.asset",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.asset_in_room",
      "cardinality": "N:1",
      "description": "The room a piece of equipment is installed in. At most one of this and `asset_at_venue` (validator-enforced); re-registering the asset re-points it.",
      "domain": "operations",
      "from": "hospitality.asset",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.banquet_order_at_outlet",
      "cardinality": "N:1",
      "description": "The banquet revenue centre the order's charges post under — every line lands in the USALI Food and Beverage department.",
      "domain": "catering",
      "from": "hospitality.banquet_event_order",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.banquet_order_has_line",
      "cardinality": "1:N",
      "description": "The order's lines, in `sequence` order on the sheet.",
      "domain": "catering",
      "from": "hospitality.banquet_event_order",
      "to": "hospitality.banquet_order_line"
    },
    {
      "api_name": "hospitality.link.banquet_order_line_for_service_item",
      "cardinality": "N:1",
      "description": "The catalogue item behind the line, when there is one.",
      "domain": "catering",
      "from": "hospitality.banquet_order_line",
      "to": "hospitality.service_item"
    },
    {
      "api_name": "hospitality.link.banquet_order_line_posted_charge",
      "cardinality": "1:1",
      "description": "The folio charge the line became at posting; absent until the order is posted.",
      "domain": "catering",
      "from": "hospitality.banquet_order_line",
      "to": "hospitality.charge"
    },
    {
      "api_name": "hospitality.link.banquet_order_posts_to_folio",
      "cardinality": "N:1",
      "description": "The folio the charges post to when the order names one; a group event with none named posts to the block's master folio.",
      "domain": "catering",
      "from": "hospitality.banquet_event_order",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.block_allocation_for_room_type",
      "cardinality": "N:1",
      "description": "The room type a grid cell holds rooms of — the other half of the cell's (block, room_type, date) coordinates.",
      "domain": "revenue",
      "from": "hospitality.block_allocation",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.block_allocation_of_block",
      "cardinality": "N:1",
      "description": "The block a grid cell belongs to; the block's allocations are its room-type x date grid.",
      "domain": "revenue",
      "from": "hospitality.block_allocation",
      "to": "hospitality.group_block"
    },
    {
      "api_name": "hospitality.link.block_for_property",
      "cardinality": "N:1",
      "description": "The property whose inventory a block holds. N:1 — a block never spans properties; multi-property groups get one block each, so block scope matches the availability ledger it mutates.",
      "domain": "revenue",
      "from": "hospitality.group_block",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.block_has_master_folio",
      "cardinality": "N:1",
      "description": "The master folio a block's charges settle to. Individual pickups' folios join it through folio_part_of_group_master.",
      "domain": "revenue",
      "from": "hospitality.group_block",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.board_sort_pattern_at_property",
      "cardinality": "N:1",
      "description": "Which property a board sort pattern orders a board at — the house whose handsets and desk read it through `board_sort`.",
      "domain": "operations",
      "from": "hospitality.board_sort_pattern",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.booking_origin_for_reservation",
      "cardinality": "1:1",
      "description": "The guest-originated online booking this IP address and device were captured for.",
      "domain": "booking",
      "from": "hospitality.booking_origin",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.brand_has_portfolio",
      "cardinality": "1:N",
      "description": "Which brand a portfolio operates under; portfolios group the brand's properties into regions, operating companies, or franchisees for reporting and permission rollups.",
      "domain": "organization",
      "from": "hospitality.brand",
      "to": "hospitality.portfolio"
    },
    {
      "api_name": "hospitality.link.brand_has_property",
      "cardinality": "1:N",
      "description": "Which brand a property flies; the edge brand standards (rate plans, room types, policies) flow down. Boutique tenants may have no brand at all.",
      "domain": "organization",
      "from": "hospitality.brand",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.brand_has_rate_plan",
      "cardinality": "1:N",
      "description": "Which brand owns a brand-standard rate plan shared across its properties; bespoke plans attach at the property instead.",
      "domain": "organization",
      "from": "hospitality.brand",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.brand_operator_cost_at_property",
      "cardinality": "N:1",
      "description": "Which property a franchise/management cost entry posts against; the property accumulates the brand & operator ledger subtracted from GOP in its daily rollup.",
      "domain": "brand_operator",
      "from": "hospitality.brand_operator_cost_entry",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.brand_operator_cost_for_company",
      "cardinality": "N:1",
      "description": "The payee company (brand or operator) a cost entry is owed to; optional, since a lump-sum import may name no counterparty row.",
      "domain": "brand_operator",
      "from": "hospitality.brand_operator_cost_entry",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.cancellation_policy_at_brand",
      "cardinality": "N:1",
      "description": "Which brand owns a brand-standard cancellation policy shared across its properties; bespoke policies attach at the property instead.",
      "domain": "booking",
      "from": "hospitality.cancellation_policy",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.cancellation_policy_at_property",
      "cardinality": "N:1",
      "description": "Which property owns a bespoke cancellation policy; brand-standard policies attach at brand instead.",
      "domain": "booking",
      "from": "hospitality.cancellation_policy",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.cashier_at_property",
      "cardinality": "N:1",
      "description": "The desk this till stands at. Sessions inherit the property through their cashier; the night audit's open-session count walks property -> cashiers -> sessions.",
      "domain": "cashiering",
      "from": "hospitality.cashier",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.cashier_session_for_cashier",
      "cardinality": "N:1",
      "description": "Whose shift this is. open_cashier_session enforces at most one OPEN session per cashier per currency through this edge — the till cannot be two people's float at once.",
      "domain": "cashiering",
      "from": "hospitality.cashier_session",
      "to": "hospitality.cashier"
    },
    {
      "api_name": "hospitality.link.cashier_transaction_in_session",
      "cardinality": "N:1",
      "description": "The shift a cash movement belongs to. close_cashier_session folds every transaction on this edge into the expected balance; the append-only contract means the fold is final the moment the session closes.",
      "domain": "cashiering",
      "from": "hospitality.cashier_transaction",
      "to": "hospitality.cashier_session"
    },
    {
      "api_name": "hospitality.link.cashier_transaction_mirrors_payment",
      "cardinality": "N:1",
      "description": "The folio payment this drawer movement mirrors, when it mirrors one — Mews's optional PaymentId shape. The payment stays the ledger truth on the folio; the session entry only reconciles the physical drawer. Free-standing movements (floats, paid-outs, tips out) carry no edge.",
      "domain": "cashiering",
      "from": "hospitality.cashier_transaction",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.channel_commission_period_for_channel",
      "cardinality": "N:1",
      "description": "Which channel a commission period prices; the period rows form the authoritative time-varying ledger behind the convenient `current` scalar on the channel.",
      "domain": "distribution",
      "from": "hospitality.channel_commission_period",
      "to": "hospitality.channel"
    },
    {
      "api_name": "hospitality.link.channel_commission_period_for_property",
      "cardinality": "N:1",
      "description": "Which property the commission period applies at; commission contracts are negotiated per (channel, property) pair, not per channel alone.",
      "domain": "distribution",
      "from": "hospitality.channel_commission_period",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.channel_connection_carries_channel",
      "cardinality": "N:M",
      "description": "The demand channels a conduit fronts. Many-to-many, and the reason a conduit is not itself a channel: one SiteMinder connection carries Booking.com, Expedia and dozens more, each of which keeps its own commission period and its own share of the mix.",
      "domain": "channel_manager",
      "from": "hospitality.channel_connection",
      "to": "hospitality.channel"
    },
    {
      "api_name": "hospitality.link.channel_connection_for_property",
      "cardinality": "N:1",
      "description": "The property whose ARI this conduit publishes and whose reservations it delivers. A property can run several conduits at once — a channel manager for the OTAs, a direct switch for a key account — and each carries the partner's own code for the property in `property_code`.",
      "domain": "channel_manager",
      "from": "hospitality.channel_connection",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.channel_distributes_rate_plan",
      "cardinality": "N:M",
      "description": "Which rate plans a channel is allowed to sell; many-to-many because a plan can be distributed on several channels and a channel carries many plans (replacing the prior single-channel enum on rate plan).",
      "domain": "distribution",
      "from": "hospitality.channel",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.channel_for_property",
      "cardinality": "N:1",
      "description": "Which property this channel connection distributes inventory for; connection credentials and sync state live per property.",
      "domain": "distribution",
      "from": "hospitality.channel",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.channel_mapping_for_connection",
      "cardinality": "N:1",
      "description": "The conduit a code binding belongs to. Codes are per-connection — the same room type is `DBL` to one partner and `RT-004` to the next — so a mapping is meaningless without its conduit.",
      "domain": "channel_manager",
      "from": "hospitality.channel_mapping",
      "to": "hospitality.channel_connection"
    },
    {
      "api_name": "hospitality.link.channel_mapping_for_rate_plan",
      "cardinality": "N:1",
      "description": "Our side of a `rate_plan` or `room_rate` binding. Absent on room-type-only, age-category and extra mappings.",
      "domain": "channel_manager",
      "from": "hospitality.channel_mapping",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.channel_mapping_for_room_type",
      "cardinality": "N:1",
      "description": "Our side of a `room_type` or `room_rate` binding. Absent on rate-plan-only, age-category and extra mappings.",
      "domain": "channel_manager",
      "from": "hospitality.channel_mapping",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.charge_at_outlet",
      "cardinality": "N:1",
      "description": "Which revenue center this charge posts under — the USALI-department anchor for total-revenue reporting. Required: every charge-emitting handler writes it atomically with the charge's `outlet_id` property, and the commit-time validator enforces field-link agreement.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.charge_attributed_to_package_component",
      "cardinality": "N:1",
      "description": "The package component a charge is attributed to; paired with the package_sale link, the pair's presence and the package_attribution_kind enum record the attribution shape.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.package_component"
    },
    {
      "api_name": "hospitality.link.charge_attributed_to_package_sale",
      "cardinality": "N:1",
      "description": "The package sale a charge is attributed to; a package-attributed charge sets both this and the component link, a non-package charge neither.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.package_sale"
    },
    {
      "api_name": "hospitality.link.charge_for_service_item",
      "cardinality": "N:1",
      "description": "The catalog item a charge was posted against, when there is one; system-generated charges (room night, tax line, adjustment) carry a system_charge_kind enum instead.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.service_item"
    },
    {
      "api_name": "hospitality.link.charge_for_stock_movement",
      "cardinality": "N:1",
      "description": "The minibar consumption a charge bills, when the charge was posted after the guest left (post_departed_charge): the floor recorded the movement `uncharged`, and this edge says it has since reached the bill. The movement is never edited; a movement with an incoming edge is posted, and is not posted again.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.stock_movement"
    },
    {
      "api_name": "hospitality.link.charge_has_tax_application",
      "cardinality": "1:N",
      "description": "The individual tax lines applied to a charge; one charge typically carries several (city tax, occupancy tax, VAT, resort fee), each with its own jurisdiction and rate.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.tax_application"
    },
    {
      "api_name": "hospitality.link.charge_reverses_charge",
      "cardinality": "N:1",
      "description": "Ties a reversal charge (negative amount) back to the charge it takes back. Written only by `reverse_charge`: full-amount reversals, one per original, never chained onto another reversal.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.charge"
    },
    {
      "api_name": "hospitality.link.charge_via_credential",
      "cardinality": "N:1",
      "description": "Which credential authorised this posting. Written atomically by `post_charge_by_credential`; the daily-cap check sums the business day's charges along this edge, and the fraud audit walks it from a suspect card to everything it bought.",
      "domain": "finance",
      "from": "hospitality.charge",
      "to": "hospitality.access_credential"
    },
    {
      "api_name": "hospitality.link.check_at_outlet",
      "cardinality": "N:1",
      "description": "The outlet a check rolls up to for revenue reporting; the constant anchor, since takeaway and room-service checks have no table.",
      "domain": "fnb",
      "from": "hospitality.guest_check",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.check_for_stay",
      "cardinality": "N:1",
      "description": "The in-house stay a check is for — written by `open_check` for every room-service order (and for any check opened for a stay), so the runner has the room and the room charge has the stay's folio without the server looking the guest up again.",
      "domain": "fnb",
      "from": "hospitality.guest_check",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.check_line_for_menu_item",
      "cardinality": "N:1",
      "description": "The menu item a check line ordered; the item every item-level report (mix, allergens, recipe COGS) reads through.",
      "domain": "fnb",
      "from": "hospitality.check_line",
      "to": "hospitality.menu_item"
    },
    {
      "api_name": "hospitality.link.check_line_for_variant",
      "cardinality": "N:1",
      "description": "The variant a check line sold — the size or measure whose price the line snapshotted.",
      "domain": "fnb",
      "from": "hospitality.check_line",
      "to": "hospitality.menu_item_variant"
    },
    {
      "api_name": "hospitality.link.check_line_modifies_line",
      "cardinality": "N:1",
      "description": "The line a modifier line modifies (add bacon → the burger); the child is priced and taxed as its own item and follows its parent through fire, bump, void and comp.",
      "domain": "fnb",
      "from": "hospitality.check_line",
      "to": "hospitality.check_line"
    },
    {
      "api_name": "hospitality.link.check_line_on_check",
      "cardinality": "N:1",
      "description": "The check a line belongs to; a check accumulates its ordered lines.",
      "domain": "fnb",
      "from": "hospitality.check_line",
      "to": "hospitality.guest_check"
    },
    {
      "api_name": "hospitality.link.check_line_on_ticket",
      "cardinality": "N:1",
      "description": "The kitchen ticket a fired line landed on; set by fire_course, a line joins at most one ticket (one per station per course, picked deterministically from its course and item's station).",
      "domain": "fnb",
      "from": "hospitality.check_line",
      "to": "hospitality.kitchen_ticket"
    },
    {
      "api_name": "hospitality.link.check_opened_at_table",
      "cardinality": "N:1",
      "description": "The table a dine-in check was opened at; optional — takeaway and room-service checks carry none.",
      "domain": "fnb",
      "from": "hospitality.guest_check",
      "to": "hospitality.service_table"
    },
    {
      "api_name": "hospitality.link.check_payment_on_check",
      "cardinality": "N:1",
      "description": "The check a payment was taken against; a check is paid by one or more payments.",
      "domain": "fnb",
      "from": "hospitality.check_payment",
      "to": "hospitality.guest_check"
    },
    {
      "api_name": "hospitality.link.check_payment_posted_charge",
      "cardinality": "1:N",
      "description": "The folio charges a room-charge payment posted: one per revenue category on the check, and its tip as a gratuity charge.",
      "domain": "fnb",
      "from": "hospitality.check_payment",
      "to": "hospitality.charge"
    },
    {
      "api_name": "hospitality.link.check_payment_to_folio",
      "cardinality": "N:1",
      "description": "The folio a room-charge or charge-to-key payment posted to; absent on every other tender.",
      "domain": "fnb",
      "from": "hospitality.check_payment",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.check_settled_to_folio",
      "cardinality": "N:M",
      "description": "A folio a check's F&B charges posted to — set by every room-charge or charge-to-key payment; a check split between two guests' bills links to both.",
      "domain": "fnb",
      "from": "hospitality.guest_check",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.checklist_result_for_task",
      "cardinality": "N:1",
      "description": "The housekeeping task a checklist was recorded against — the attendant's clean or the supervisor's inspection. A task holds at most one result per template.",
      "domain": "operations",
      "from": "hospitality.checklist_result",
      "to": "hospitality.housekeeping_task"
    },
    {
      "api_name": "hospitality.link.checklist_result_for_template",
      "cardinality": "N:1",
      "description": "The template a result answers. A template revised in place keeps its results; each result's score was derived from the list as it stood when recorded (its checks_total says so).",
      "domain": "operations",
      "from": "hospitality.checklist_result",
      "to": "hospitality.checklist_template"
    },
    {
      "api_name": "hospitality.link.checklist_result_recorded_by",
      "cardinality": "N:1",
      "description": "Who ticked the list: the staff member behind the handset session (or the desk sign-in) that recorded it, taken from the principal exactly as `start_housekeeping_task` self-assigns. Absent when the caller is not a staff member (an agent, the system). Re-pointed when someone else records it again.",
      "domain": "operations",
      "from": "hospitality.checklist_result",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.checklist_template_at_brand",
      "cardinality": "N:1",
      "description": "A brand-standard checklist: it resolves onto the tasks of every house flying the brand that has no list of its own for the task type (and room type).",
      "domain": "operations",
      "from": "hospitality.checklist_template",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.checklist_template_at_property",
      "cardinality": "N:1",
      "description": "The house's own checklist; it outranks the brand's for the same task type.",
      "domain": "operations",
      "from": "hospitality.checklist_template",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.checklist_template_for_room_type",
      "cardinality": "N:1",
      "description": "The one room type a checklist is for (a suite's list is longer than a twin's). Absent, the template is for every room; present, it outranks the every-room list at the same scope. Written once at definition — the room type is part of the template's identity.",
      "domain": "operations",
      "from": "hospitality.checklist_template",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.checkpoint_result_for_asset",
      "cardinality": "N:1",
      "description": "The piece of equipment an asset checkpoint is — the asset the round's checkpoint names. With the asset's work orders, its incoming edges are its inspection history.",
      "domain": "operations",
      "from": "hospitality.checkpoint_result",
      "to": "hospitality.asset"
    },
    {
      "api_name": "hospitality.link.checkpoint_result_for_room",
      "cardinality": "N:1",
      "description": "The room a room checkpoint is — the room the round's checkpoint names. A room's incoming edges are every round's record of it.",
      "domain": "operations",
      "from": "hospitality.checkpoint_result",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.checkpoint_result_in_round",
      "cardinality": "N:1",
      "description": "The round a checkpoint result was recorded in — one result per round and checkpoint position. Every result has one (a cross-link validator refuses a result in no round).",
      "domain": "operations",
      "from": "hospitality.checkpoint_result",
      "to": "hospitality.inspection_round"
    },
    {
      "api_name": "hospitality.link.checkpoint_result_recorded_by",
      "cardinality": "N:1",
      "description": "The staff member who recorded the checkpoint (the latest recording, when it was recorded again), when one did; `recorded_by` names the principal either way.",
      "domain": "operations",
      "from": "hospitality.checkpoint_result",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.coaching_plan_for_membership",
      "cardinality": "N:1",
      "description": "Whose coaching plan this is; like protocols and assessments, the plan attaches to the individual membership rather than the shared account.",
      "domain": "membership",
      "from": "hospitality.coaching_plan",
      "to": "hospitality.membership"
    },
    {
      "api_name": "hospitality.link.coaching_plan_references_protocol",
      "cardinality": "N:M",
      "description": "Which protocols the coaching plan works: a member may run a strength protocol and a sleep protocol concurrently under one plan.",
      "domain": "membership",
      "from": "hospitality.coaching_plan",
      "to": "hospitality.wellness_protocol"
    },
    {
      "api_name": "hospitality.link.commission_agreement_for_company",
      "cardinality": "N:1",
      "description": "The travel agent, tour operator, wholesaler or consortium a commission agreement contracts with. Many agreements over time, one open at any moment.",
      "domain": "commission",
      "from": "hospitality.commission_agreement",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.commission_agreement_for_property",
      "cardinality": "N:1",
      "description": "The property whose commission the agreement governs — the same agent can be on different terms at different houses.",
      "domain": "commission",
      "from": "hospitality.commission_agreement",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.commission_batch_for_property",
      "cardinality": "N:1",
      "description": "The property whose commission a payment run settled.",
      "domain": "commission",
      "from": "hospitality.commission_payment_batch",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.commission_detail_for_reservation",
      "cardinality": "N:1",
      "description": "The completed stay a commission accrual rests on. N:1 because one stay can owe two different payees — an agent booked it through an OTA — and each is settled separately.",
      "domain": "commission",
      "from": "hospitality.commission_detail",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.commission_detail_in_batch",
      "cardinality": "N:1",
      "description": "The payment run that settled this detail. Absent until the row is paid.",
      "domain": "commission",
      "from": "hospitality.commission_detail",
      "to": "hospitality.commission_payment_batch"
    },
    {
      "api_name": "hospitality.link.commission_detail_owed_to_channel",
      "cardinality": "N:1",
      "description": "The channel a commission detail is owed to. Present on rows we expect to reconcile against the channel's own invoice rather than pay out.",
      "domain": "commission",
      "from": "hospitality.commission_detail",
      "to": "hospitality.channel"
    },
    {
      "api_name": "hospitality.link.commission_detail_owed_to_company",
      "cardinality": "N:1",
      "description": "The agent a commission detail is payable to. Present on hotel-initiated payables.",
      "domain": "commission",
      "from": "hospitality.commission_detail",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.comp_authority_policy_at_property",
      "cardinality": "N:1",
      "description": "Which property the comp-authority policy row governs; the recovery handler resolves (staff role, comp category) against the property's rows to find the effective limit.",
      "domain": "people",
      "from": "hospitality.comp_authority_policy",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.company_has_ar_folio",
      "cardinality": "1:N",
      "description": "The company's A/R accounts — one `company_ar` folio per property and currency — that the desk direct-bills guest folios to and `settle_folio_to_ar` targets.",
      "domain": "people",
      "from": "hospitality.company",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.consumption_for_charge",
      "cardinality": "1:1",
      "description": "The folio posting for this consumption; exactly one charge per consumption row, so the reconciliation query walks either direction in O(1).",
      "domain": "revenue",
      "from": "hospitality.package_component_consumption",
      "to": "hospitality.charge"
    },
    {
      "api_name": "hospitality.link.consumption_for_check_payment",
      "cardinality": "1:1",
      "description": "The room-charge payment whose food and soft drinks a package allowance covered: the consumption row and the payment that consumed it, one each, so the check that ate the guest's breakfast is a walk away from the package ledger.",
      "domain": "revenue",
      "from": "hospitality.package_component_consumption",
      "to": "hospitality.check_payment"
    },
    {
      "api_name": "hospitality.link.demand_forecast_for_property",
      "cardinality": "N:1",
      "description": "The house a forecast point is for. Property-level by design: a room-type forecast is a finer artefact this point does not claim to be.",
      "domain": "revenue_management",
      "from": "hospitality.demand_forecast",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.deposit_policy_at_property",
      "cardinality": "N:1",
      "description": "Which property a deposit policy applies at — the house default that reservation creation resolves when no rate-plan override outranks it.",
      "domain": "finance",
      "from": "hospitality.deposit_policy",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.derived_rate_for_rate_per_night",
      "cardinality": "N:1",
      "description": "Back-pointer to the rate_per_night row that contributed the BAR anchor at compute time; pins the exact input the effective rate was derived from.",
      "domain": "distribution",
      "from": "hospitality.derived_rate",
      "to": "hospitality.rate_per_night"
    },
    {
      "api_name": "hospitality.link.derived_rate_for_rate_plan",
      "cardinality": "N:1",
      "description": "Back-pointer from a materialised rate snapshot to the rate plan whose derivation it resolved; supports 'what rate did we sell at that moment' audit reconstruction.",
      "domain": "distribution",
      "from": "hospitality.derived_rate",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.derived_rate_for_room_type",
      "cardinality": "N:1",
      "description": "Back-pointer from a materialised rate snapshot to the room type it was computed for.",
      "domain": "distribution",
      "from": "hospitality.derived_rate",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.design_dna_designed_by_company",
      "cardinality": "N:1",
      "description": "The lead designer-of-record (company with kind = designer) a design DNA is attributed to; N:1 since co-credit lives outside the schema.",
      "domain": "brand_venue",
      "from": "hospitality.design_dna",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.design_dna_has_signature_scent",
      "cardinality": "N:1",
      "description": "The retail line that is a design DNA's signature scent (Le Labo Santal 33); at most one per DNA.",
      "domain": "brand_venue",
      "from": "hospitality.design_dna",
      "to": "hospitality.retail_product_line"
    },
    {
      "api_name": "hospitality.link.desk_session_acting_staff",
      "cardinality": "N:1",
      "description": "Who is at the desk now. Re-pointed by every switch, so the current edge answers \"who is acting\" and the `desk_session.switched` events answer \"who was, and when\".",
      "domain": "people",
      "from": "hospitality.desk_session",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.desk_session_at_property",
      "cardinality": "N:1",
      "description": "The house the shift is at. Every person switched in to the session must work at it (`staff_works_at_property`); a colleague from another property is refused, not moved.",
      "domain": "people",
      "from": "hospitality.desk_session",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.desk_session_opened_by",
      "cardinality": "N:1",
      "description": "The member of staff whose identity-provider sign-in opened the shift. Written once and never re-pointed: the shift's accountable opener, distinct from whoever is at the desk now.",
      "domain": "people",
      "from": "hospitality.desk_session",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.development_pathway_definition_at_brand",
      "cardinality": "N:1",
      "description": "Which brand owns a brand-standard progression-ladder definition shared across its properties; bespoke ladders attach at the property instead.",
      "domain": "people",
      "from": "hospitality.development_pathway_definition",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.development_pathway_definition_at_property",
      "cardinality": "N:1",
      "description": "Which property owns a bespoke progression-ladder definition; brand-standard ladders attach at brand instead.",
      "domain": "people",
      "from": "hospitality.development_pathway_definition",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.device_at_outlet",
      "cardinality": "N:1",
      "description": "The revenue center this device serves — a payment terminal on the pool bar posts that outlet's charges. Optional: back-office hardware (a key encoder at the desk) has no outlet.",
      "domain": "access",
      "from": "hospitality.device",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.device_at_property",
      "cardinality": "N:1",
      "description": "Where this hardware physically lives; every device belongs to exactly one property.",
      "domain": "access",
      "from": "hospitality.device",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.displacement_analysis_for_request",
      "cardinality": "1:N",
      "description": "The displacement analyses run for this request; each quote_group_block mints a new frozen artefact, the latest being the current position and older ones audit history.",
      "domain": "revenue",
      "from": "hospitality.group_request",
      "to": "hospitality.group_displacement_analysis"
    },
    {
      "api_name": "hospitality.link.dispute_for_payment",
      "cardinality": "N:1",
      "description": "The settled payment this chargeback contests. The reverse role is what tells a desk a payment is under dispute at all — the payment's own status stays `settled`, because it was, and the dispute is a separate fact about it.",
      "domain": "finance",
      "from": "hospitality.payment_dispute",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.evidence_packet_for_dispute",
      "cardinality": "N:1",
      "description": "The chargeback this evidence packet defends. At most one is live (draft or staged) at a time; earlier ones are superseded, and a submitted one is final.",
      "domain": "finance",
      "from": "hospitality.dispute_evidence_packet",
      "to": "hospitality.payment_dispute"
    },
    {
      "api_name": "hospitality.link.external_record_reference_for_provider",
      "cardinality": "N:1",
      "description": "The external organisation that holds the referenced record; many references can point at one provider — a clinic holding several patients' records, an audit firm running multiple engagements.",
      "domain": "external_records",
      "from": "hospitality.external_record_reference",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.fault_type_at_brand",
      "cardinality": "N:1",
      "description": "The brand whose catalogue a fault type is in — shared by every house of the brand. A fault type is in exactly one catalogue: this or `fault_type_at_property` (validator-enforced).",
      "domain": "operations",
      "from": "hospitality.fault_type",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.fault_type_at_property",
      "cardinality": "N:1",
      "description": "The house whose own catalogue a fault type is in — the alternative to `fault_type_at_brand`.",
      "domain": "operations",
      "from": "hospitality.fault_type",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.federated_contribution_for_run",
      "cardinality": "N:1",
      "description": "The run a contribution was made to; a run accumulates many participant contributions.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_contribution",
      "to": "hospitality.federated_run"
    },
    {
      "api_name": "hospitality.link.federated_contribution_from_participation",
      "cardinality": "N:1",
      "description": "The participation that produced a contribution; a participation contributes across many runs over time.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_contribution",
      "to": "hospitality.federated_participation"
    },
    {
      "api_name": "hospitality.link.federated_definition_reviewed_by_gate",
      "cardinality": "N:1",
      "description": "The approval gate that approved a definition version (proposal §8.4); optional.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_analysis_definition",
      "to": "hospitality.approval_gate"
    },
    {
      "api_name": "hospitality.link.federated_local_runner_agent_installation",
      "cardinality": "N:1",
      "description": "The installed agent that runs local extraction for this participation; set when local_execution_mode = agent_installation.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_participation",
      "to": "hospitality.agent_installation"
    },
    {
      "api_name": "hospitality.link.federated_output_from_run",
      "cardinality": "N:1",
      "description": "The run that produced an output; a run may emit several outputs across output_kind / schema-version pairs.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_output",
      "to": "hospitality.federated_run"
    },
    {
      "api_name": "hospitality.link.federated_output_reviewed_by_gate",
      "cardinality": "N:1",
      "description": "The approval gate an output cleared; required when the definition sets output_review_required, absent otherwise.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_output",
      "to": "hospitality.approval_gate"
    },
    {
      "api_name": "hospitality.link.federated_participation_for_property",
      "cardinality": "N:1",
      "description": "The property a participation is scoped to; present only when participant_scope selects the property endpoint (tenant and portfolio scopes are participation-internal flags).",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_participation",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.federated_participation_for_venue",
      "cardinality": "N:1",
      "description": "The venue a participation is scoped to; present only when participant_scope selects the venue endpoint.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_participation",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.federated_participation_in_program",
      "cardinality": "N:1",
      "description": "The program a tenant's participation enrols in; a tenant accumulates many participations across programs.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_participation",
      "to": "hospitality.federated_intelligence_program"
    },
    {
      "api_name": "hospitality.link.federated_program_has_cohort",
      "cardinality": "N:1",
      "description": "The program a cohort belongs to; a program reuses many cohort definitions (oriented cohort → program to match the schema's anchor convention).",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_cohort_definition",
      "to": "hospitality.federated_intelligence_program"
    },
    {
      "api_name": "hospitality.link.federated_program_has_definition",
      "cardinality": "N:1",
      "description": "The program a definition version belongs to; a program accumulates definition versions over time, each belonging to exactly one program.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_analysis_definition",
      "to": "hospitality.federated_intelligence_program"
    },
    {
      "api_name": "hospitality.link.federated_run_reserved_from_account",
      "cardinality": "N:1",
      "description": "The privacy-budget account a run's reservation drew from — the graph anchor beside the run's `privacy_budget_reserved` envelope (proposal 0021 §7: reservation precedes collecting). Written by start_federated_run atomically with the reserve ledger event; absent on runs that reserve no budget (no account configured or epsilon_per_run absent).",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_run",
      "to": "hospitality.privacy_budget_account"
    },
    {
      "api_name": "hospitality.link.federated_run_started_by_workflow",
      "cardinality": "N:1",
      "description": "The workflow run that initiated this federated run; optional, the proposal 0020 orchestration seam.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_run",
      "to": "hospitality.workflow_run"
    },
    {
      "api_name": "hospitality.link.federated_run_uses_cohort",
      "cardinality": "N:1",
      "description": "The cohort a run drew participants from; the graph anchor beside the run's inline cohort_snapshot.",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_run",
      "to": "hospitality.federated_cohort_definition"
    },
    {
      "api_name": "hospitality.link.federated_run_uses_definition",
      "cardinality": "N:1",
      "description": "The definition version a run executed; the graph anchor beside the run's inline definition snapshot (its immutable evidence).",
      "domain": "federated_intelligence",
      "from": "hospitality.federated_run",
      "to": "hospitality.federated_analysis_definition"
    },
    {
      "api_name": "hospitality.link.feedback_label_at_property",
      "cardinality": "N:1",
      "description": "The house whose guest-feedback label it is — the house's label catalogue is this edge's incoming side. Every label has exactly one (validator-enforced); define_feedback_label writes it.",
      "domain": "operations",
      "from": "hospitality.feedback_label",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.folio_has_charge",
      "cardinality": "1:N",
      "description": "Which folio a charge posts to; routing rules are applied at post time, so the edge records where the charge actually landed.",
      "domain": "finance",
      "from": "hospitality.folio",
      "to": "hospitality.charge"
    },
    {
      "api_name": "hospitality.link.folio_has_payment",
      "cardinality": "1:N",
      "description": "Which folio a payment applies to; refund legs post as separate negative-amount payment rows on the same folio rather than rewriting the original.",
      "domain": "finance",
      "from": "hospitality.folio",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.folio_part_of_group_master",
      "cardinality": "N:1",
      "description": "Membership of a folio in a group block's master folio, so a block's individual folios can settle to one group account.",
      "domain": "finance",
      "from": "hospitality.folio",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.folio_rebates_folio",
      "cardinality": "N:1",
      "description": "Ties a rebate folio back to the sealed bill it corrects. Written only by open_rebate_folio; reverse_charge and refund_payment route their sealed-folio legs through the open rebate folio this edge names.",
      "domain": "finance",
      "from": "hospitality.folio",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.folio_routes_to_master_folio",
      "cardinality": "N:1",
      "description": "Standing roll-up from a child folio to the master folio that absorbs its routed charges; the bill-to backbone behind per-charge routing rules.",
      "domain": "finance",
      "from": "hospitality.folio",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.folio_routing_rule_at_property",
      "cardinality": "N:1",
      "description": "Which property the routing rule operates at; routing is evaluated against the property's active rules when a charge posts.",
      "domain": "finance",
      "from": "hospitality.folio_routing_rule",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.folio_routing_rule_source",
      "cardinality": "N:1",
      "description": "The folio whose charges the routing rule intercepts at post time; at most one rule binds a given source to a given target, and the lowest priority number wins among a source's active rules.",
      "domain": "finance",
      "from": "hospitality.folio_routing_rule",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.folio_routing_rule_target",
      "cardinality": "N:1",
      "description": "The folio the matched charges are routed to — typically a master or company folio absorbing a contracted share of the source folio's spend.",
      "domain": "finance",
      "from": "hospitality.folio_routing_rule",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.folio_settled_by_company_ar",
      "cardinality": "N:1",
      "description": "Which company A/R folio an outstanding balance was transferred to; settle_folio_to_ar writes the edge idempotently — re-settling refreshes due_at rather than duplicating the link.",
      "domain": "finance",
      "from": "hospitality.folio",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_at_property",
      "cardinality": "N:1",
      "description": "The property an assessment was raised at; the denormalised anchor for tenant-scoped fraud queries.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_chained_action_target",
      "cardinality": "1:1",
      "description": "The object a chained apply-time action wrote (decline-with-hold or cancel path); pinned to reservation as the kernel endpoint pending the Phase 3 polymorphic-target primitive, set at apply.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_consulted_network",
      "cardinality": "N:1",
      "description": "The network answer this assessment was scored with — the evidence behind a `network.*` reason code and the band floor.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.fraud_network_signal"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_escalated_to_staff_member",
      "cardinality": "N:1",
      "description": "The staff member an assessment was escalated to; set only on escalate_via_fraud_assessment, and absent when escalation routes to the chargeback-defence queue rather than a person.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_for_charge",
      "cardinality": "N:1",
      "description": "The charge an assessment scored; a charge accumulates many assessments across its authorisation lifecycle.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.charge"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_for_person",
      "cardinality": "N:1",
      "description": "The person an assessment scored; a person accumulates many assessments over time (KYC / 3DS / post-attempt re-scoring).",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_for_prior_triage",
      "cardinality": "N:1",
      "description": "The prior message_triage whose injection.suspected signal fed this assessment; audit-grade evidence the cross-surface signal was used (0014 → 0015), optional and usually absent.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.message_triage"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_for_reservation",
      "cardinality": "N:1",
      "description": "The reservation an assessment scored; a reservation accumulates many assessments as it is re-scored on fresh evidence, each belonging to exactly one reservation.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_references_external_record",
      "cardinality": "N:1",
      "description": "The vendor raw-payload reference (Radar / Sift / Forter) held for chargeback-dispute defence; optional, since in-house or manual-review assessments carry no vendor payload.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.fraud_assessment_superseded_by",
      "cardinality": "N:1",
      "description": "The fresher assessment that superseded this one on the same (subject, fire_point) after new evidence (3DS, KYC); N:1 so a re-score may skip intermediate generations.",
      "domain": "fraud_detection",
      "from": "hospitality.fraud_risk_assessment",
      "to": "hospitality.fraud_risk_assessment"
    },
    {
      "api_name": "hospitality.link.fraud_network_contribution_at_property",
      "cardinality": "N:1",
      "description": "The house that offered this confirmed fraud outcome to the network. Never shared: the network knows only the digests.",
      "domain": "fraud_network",
      "from": "hospitality.fraud_network_contribution",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.fraud_network_contribution_for_assessment",
      "cardinality": "N:1",
      "description": "The assessment applied as a block that this contribution reports (`confirmed_fraud`).",
      "domain": "fraud_network",
      "from": "hospitality.fraud_network_contribution",
      "to": "hospitality.fraud_risk_assessment"
    },
    {
      "api_name": "hospitality.link.fraud_network_contribution_for_payment",
      "cardinality": "N:1",
      "description": "The payment whose lost fraud dispute this contribution reports (`chargeback_lost_fraud`).",
      "domain": "fraud_network",
      "from": "hospitality.fraud_network_contribution",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.fraud_network_signal_at_property",
      "cardinality": "N:1",
      "description": "The house whose risk read the network answered.",
      "domain": "fraud_network",
      "from": "hospitality.fraud_network_signal",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.function_event_at_property",
      "cardinality": "N:1",
      "description": "The property the event is at.",
      "domain": "catering",
      "from": "hospitality.function_event",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.function_event_contact",
      "cardinality": "N:1",
      "description": "The client's on-site contact for the event.",
      "domain": "catering",
      "from": "hospitality.function_event",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.function_event_for_block",
      "cardinality": "N:1",
      "description": "The group whose event this is — the block holds the bedrooms, its events hold the function space, and the block's master folio is where the events' charges post by default.",
      "domain": "catering",
      "from": "hospitality.function_event",
      "to": "hospitality.group_block"
    },
    {
      "api_name": "hospitality.link.function_event_has_order",
      "cardinality": "1:1",
      "description": "The banquet event order for the event — at most one, revised in place under a revision number rather than re-minted.",
      "domain": "catering",
      "from": "hospitality.function_event",
      "to": "hospitality.banquet_event_order"
    },
    {
      "api_name": "hospitality.link.function_event_in_space",
      "cardinality": "N:1",
      "description": "Where the event is set; the diary conflict check runs over the space's events, its sections' and its combined room's.",
      "domain": "catering",
      "from": "hospitality.function_event",
      "to": "hospitality.function_space"
    },
    {
      "api_name": "hospitality.link.function_space_at_property",
      "cardinality": "N:1",
      "description": "The property whose diary this space is in.",
      "domain": "catering",
      "from": "hospitality.function_space",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.function_space_section_of",
      "cardinality": "N:1",
      "description": "The combined room this space is a section of (Ballroom A of the Grand Ballroom): an event in the combined room occupies every section, and a booked section blocks the combined room.",
      "domain": "catering",
      "from": "hospitality.function_space",
      "to": "hospitality.function_space"
    },
    {
      "api_name": "hospitality.link.group_request_converts_to_reservation",
      "cardinality": "1:N",
      "description": "The reservations a group request converted into; one request fans out to many reservations (one per broken-out block slot, across properties for multi-property blocks).",
      "domain": "revenue",
      "from": "hospitality.group_request",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.group_request_for_property",
      "cardinality": "N:M",
      "description": "The properties a group request books across; many-to-many so a multi-property block (a wedding across two properties) is quoted and decided once.",
      "domain": "revenue",
      "from": "hospitality.group_request",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.group_request_has_block",
      "cardinality": "1:N",
      "description": "The operational blocks a group request produced, one per property. Optional: a block can exist with no request behind it (house allotment, tour series, wholesale allocation).",
      "domain": "revenue",
      "from": "hospitality.group_request",
      "to": "hospitality.group_block"
    },
    {
      "api_name": "hospitality.link.guest_conversation_anchored_to_reservation",
      "cardinality": "N:1",
      "description": "The reservation a conversation is currently anchored to; temporal at the storage layer so a returning guest's thread re-anchors per stay, optional for pre-arrival/general threads.",
      "domain": "messaging",
      "from": "hospitality.guest_conversation",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.guest_conversation_at_property",
      "cardinality": "N:1",
      "description": "The property a conversation is scoped to; the anchor for tenant-scoped conversation queries.",
      "domain": "messaging",
      "from": "hospitality.guest_conversation",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.guest_conversation_for_person",
      "cardinality": "N:1",
      "description": "The guest a conversation is with; a person accumulates many conversations over time, each belonging to exactly one person.",
      "domain": "messaging",
      "from": "hospitality.guest_conversation",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.guest_feedback_entry_by_staff",
      "cardinality": "N:1",
      "description": "The staff member who wrote a follow-up — the principal behind the handset or the desk when it is one. Absent for an agent's or an automation's entry; the entry's event names that principal.",
      "domain": "operations",
      "from": "hospitality.guest_feedback_entry",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.guest_feedback_entry_on_feedback",
      "cardinality": "N:1",
      "description": "The feedback a follow-up entry belongs to; a feedback's entries are its thread.",
      "domain": "operations",
      "from": "hospitality.guest_feedback_entry",
      "to": "hospitality.guest_feedback"
    },
    {
      "api_name": "hospitality.link.guest_feedback_entry_sent_as",
      "cardinality": "1:1",
      "description": "The outbound guest message a guest-facing follow-up was written into the guest's conversation as — the message the delivery lane sends.",
      "domain": "operations",
      "from": "hospitality.guest_feedback_entry",
      "to": "hospitality.guest_message"
    },
    {
      "api_name": "hospitality.link.guest_feedback_for_person",
      "cardinality": "N:1",
      "description": "The guest who gave the feedback. The feedback holds no name or contact of its own; this edge is where the guest is, read under the person's markings. Written from the stay's guest or the message's conversation when not named.",
      "domain": "operations",
      "from": "hospitality.guest_feedback",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.guest_feedback_for_stay",
      "cardinality": "N:1",
      "description": "The stay a piece of guest feedback is about. Optional — a diner or a day visitor gives feedback too — and the room a complaint's work order is raised against when none is named.",
      "domain": "operations",
      "from": "hospitality.guest_feedback",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.guest_feedback_from_message",
      "cardinality": "1:1",
      "description": "The guest message the feedback was recorded from — a triaged complaint becoming a case. One feedback per message: recording the same message again is a replay.",
      "domain": "operations",
      "from": "hospitality.guest_feedback",
      "to": "hospitality.guest_message"
    },
    {
      "api_name": "hospitality.link.guest_feedback_has_label",
      "cardinality": "N:M",
      "description": "A label of the house's a piece of guest feedback is filed under (hubOS's quality labels). Written by set_guest_feedback_labels, which replaces the set; the feedback's `labels` property carries the same codes (validator-enforced), so a board filters by code without a walk.",
      "domain": "operations",
      "from": "hospitality.guest_feedback",
      "to": "hospitality.feedback_label"
    },
    {
      "api_name": "hospitality.link.guest_id_document_for_person",
      "cardinality": "N:1",
      "description": "Whose government-ID record a guest_id_document is — a guest may hold several concurrently (passport + visa), so the person is the one side.",
      "domain": "people",
      "from": "hospitality.guest_id_document",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.guest_id_document_references_external_record",
      "cardinality": "N:1",
      "description": "Optional pointer to the external KYC / IDV vault holding the raw document image / full number that Aerandil deliberately does not store; the referenced record's domain is `identity`.",
      "domain": "people",
      "from": "hospitality.guest_id_document",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.guest_message_in_conversation",
      "cardinality": "N:1",
      "description": "The conversation a message belongs to; a conversation accumulates its message thread in order.",
      "domain": "messaging",
      "from": "hospitality.guest_message",
      "to": "hospitality.guest_conversation"
    },
    {
      "api_name": "hospitality.link.guest_message_references_external_record",
      "cardinality": "N:1",
      "description": "The external messaging record this message shadows (domain = messaging); optional, since a staff-typed call summary may have no upstream record.",
      "domain": "messaging",
      "from": "hospitality.guest_message",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.housekeeping_briefing_at_property",
      "cardinality": "N:1",
      "description": "The house a housekeeping briefing was posted to.",
      "domain": "operations",
      "from": "hospitality.housekeeping_briefing",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.housekeeping_briefing_by",
      "cardinality": "N:1",
      "description": "Who posted a housekeeping briefing — the head housekeeper or supervisor whose instructions they are.",
      "domain": "operations",
      "from": "hospitality.housekeeping_briefing",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.housekeeping_credit_rule_at_property",
      "cardinality": "N:1",
      "description": "Which property a housekeeping credit rule prices tasks at — the house whose task creation and board generation read it.",
      "domain": "operations",
      "from": "hospitality.housekeeping_credit_rule",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_assigned_to",
      "cardinality": "N:1",
      "description": "The staff member the task is currently assigned to; an unassigned task simply lacks the edge.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_assigned_to_group",
      "cardinality": "N:1",
      "description": "The worker group a housekeeping task is assigned to, instead of one person: the group's pool. The member who starts it takes it (the edge re-points to `housekeeping_task_assigned_to`); a staff member outside the group may not start it. A task is assigned to a person or a group, never both (held at commit).",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.worker_group"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_entry_for_task",
      "cardinality": "N:1",
      "description": "The housekeeping task whose thread a line is on. Every entry has exactly one (validator-enforced); the thread is the task's incoming edges in `recorded_at` order.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task_entry",
      "to": "hospitality.housekeeping_task"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_entry_recorded_by",
      "cardinality": "N:1",
      "description": "The staff member who wrote a task's thread line — resolved from the principal (a handset session's principal is the staff member), as a work order's thread resolves its author. Absent when an agent, a service or a user with no staff record wrote it; the entry's `recorded_by` names the principal either way.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task_entry",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_for_area",
      "cardinality": "N:1",
      "description": "The public area a housekeeping task cleans or inspects — the lobby's daily clean, the pool's inspection. The alternative to `housekeeping_task_for_room`: a task is for a room or an area, never both, and only the public-area task types are an area's (held at commit). Written by `generate_housekeeping_tasks` and `create_public_area_task`.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.public_area"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_for_room",
      "cardinality": "N:1",
      "description": "The room the housekeeping task cleans or inspects; housekeeping work is dispatched and tracked per room.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_from_inspection",
      "cardinality": "N:1",
      "description": "A re-clean → the failed inspection that minted it. The re-clean is an ordinary manual task (no generation key) of the inspected task's type, at high priority, for the same room and business date.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.housekeeping_task"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_from_triage",
      "cardinality": "N:1",
      "description": "The triaged guest request this task was minted from; absent on the ordinary operational task, which nobody asked for by message. The edge the closing reply walks to find the conversation.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.message_triage"
    },
    {
      "api_name": "hospitality.link.housekeeping_task_requested_by_stay",
      "cardinality": "N:1",
      "description": "The guest stay that asked for a housekeeping task — the extra towels for room 214, hubOS's guest request. Written by `create_housekeeping_task` / `create_public_area_task` when a `stay_id` at the task's house is named, and only then; the task's `requested_by` is `guest`. The floor reads that a guest asked; the desk reads who.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.incidental_hold_policy_at_property",
      "cardinality": "N:1",
      "description": "Which property an incidental-hold rule applies at — the house whose check-ins `suggest_incidental_hold` derives the card hold for.",
      "domain": "finance",
      "from": "hospitality.incidental_hold_policy",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.ingredient_at_brand",
      "cardinality": "N:1",
      "description": "Which brand owns a brand-shared ingredient catalog row; property-specific ingredients attach at the property instead.",
      "domain": "operations",
      "from": "hospitality.ingredient",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.ingredient_at_property",
      "cardinality": "N:1",
      "description": "Which property owns a property-specific ingredient catalog row; brand-shared ingredients attach at brand instead.",
      "domain": "operations",
      "from": "hospitality.ingredient",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.ingredient_supplier_price_at_property",
      "cardinality": "N:1",
      "description": "Which property the supplier price applies at; a brand-shared ingredient stocked at two properties has two distinct price rows.",
      "domain": "operations",
      "from": "hospitality.ingredient_supplier_price",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.ingredient_supplier_price_for_ingredient",
      "cardinality": "N:1",
      "description": "Which ingredient a supplier-price row prices; invoices land as fresh append-only rows (the handler closes the prior open row) and the pour-cost rollup reads the active one.",
      "domain": "operations",
      "from": "hospitality.ingredient_supplier_price",
      "to": "hospitality.ingredient"
    },
    {
      "api_name": "hospitality.link.inspection_rates_task",
      "cardinality": "N:1",
      "description": "An inspection task → the cleaning it rated: the most recent completed non-inspection task for the same room and business date when the inspection was completed with a rating or an outcome. The rating and outcome are copied onto the cleaning task. A cleaning can be inspected more than once; it carries the latest verdict.",
      "domain": "operations",
      "from": "hospitality.housekeeping_task",
      "to": "hospitality.housekeeping_task"
    },
    {
      "api_name": "hospitality.link.inspection_round_completed_by",
      "cardinality": "N:1",
      "description": "The staff member who finished a round — hubOS's 'finalized by' — when one did; `completed_by` names the principal either way.",
      "domain": "operations",
      "from": "hospitality.inspection_round",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.inspection_round_from_template",
      "cardinality": "N:1",
      "description": "The template a round walks — the template's history of rounds is this edge read backwards. The round carries the template's checkpoints as they stood when it started.",
      "domain": "operations",
      "from": "hospitality.inspection_round",
      "to": "hospitality.round_template"
    },
    {
      "api_name": "hospitality.link.inspection_round_started_by",
      "cardinality": "N:1",
      "description": "The staff member who started a round, when one did (a handset session). A round the automation lane started when its template fell due has none; `started_by` names the principal either way.",
      "domain": "operations",
      "from": "hospitality.inspection_round",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.invoice_billed_to_company",
      "cardinality": "N:1",
      "description": "The company an invoice was addressed to — how their sheet finds their documents. Same rule as the person edge: the snapshot is the truth.",
      "domain": "finance",
      "from": "hospitality.invoice",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.invoice_billed_to_person",
      "cardinality": "N:1",
      "description": "The guest an invoice was addressed to. Navigation only: the legally operative details are the snapshot on the document, which this edge deliberately does not keep in step.",
      "domain": "finance",
      "from": "hospitality.invoice",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.invoice_credits_invoice",
      "cardinality": "N:1",
      "description": "A credit note to the invoice it reverses. The printed document names the original's *number*, taken from the snapshot rather than this edge — apaleo keeps `relatedInvoiceNumber` as a string for the same reason, and EU Art 226(16) makes the corrected number mandatory from 2030.",
      "domain": "finance",
      "from": "hospitality.invoice",
      "to": "hospitality.invoice"
    },
    {
      "api_name": "hospitality.link.invoice_for_folio",
      "cardinality": "N:1",
      "description": "The folio an invoice projects. ManyToOne rather than OneToOne because a folio credited and reissued accumulates documents over its life; the rule that actually binds — at most one *uncredited* invoice per folio — is a validator, not a cardinality.",
      "domain": "finance",
      "from": "hospitality.invoice",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.labour_entry_at_property",
      "cardinality": "N:1",
      "description": "Which property a labour cost posts against; the property accumulates the labour ledger that feeds its daily USALI labour rollup.",
      "domain": "labour",
      "from": "hospitality.labour_ledger_entry",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.labour_entry_for_shift",
      "cardinality": "N:1",
      "description": "The shift a timeclock-derived wage accrual came from; optional, present only when the entry was computed from a worked shift rather than imported.",
      "domain": "labour",
      "from": "hospitality.labour_ledger_entry",
      "to": "hospitality.staff_shift"
    },
    {
      "api_name": "hospitality.link.labour_entry_for_staff_member",
      "cardinality": "N:1",
      "description": "Which staff member a labour cost is attributed to; optional, since contracted labour and lump-sum payroll imports may name no individual staff row.",
      "domain": "labour",
      "from": "hospitality.labour_ledger_entry",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.lineup_at_venue",
      "cardinality": "N:1",
      "description": "The venue whose pre-shift lineup cadence this event belongs to; lets the pattern-review query walk venue → recent lineups directly.",
      "domain": "service_philosophy",
      "from": "hospitality.pre_shift_lineup_event",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.lost_found_item_claimed_by",
      "cardinality": "N:1",
      "description": "The guest who has asked for a lost item. Written by `claim_lost_found_item`; re-pointed if a later claim names someone else.",
      "domain": "operations",
      "from": "hospitality.lost_found_item",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.lost_found_item_found_by",
      "cardinality": "N:1",
      "description": "The staff member who found the item — the principal who recorded it, when that principal is a staff member (the handset's attendant or runner).",
      "domain": "operations",
      "from": "hospitality.lost_found_item",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.lost_found_item_in_room",
      "cardinality": "N:1",
      "description": "The room a lost item was found in. Absent for a find elsewhere in the house, which names its place in `found_location`.",
      "domain": "operations",
      "from": "hospitality.lost_found_item",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.lost_found_item_returned_to",
      "cardinality": "N:1",
      "description": "The person a lost item was handed back or sent to — evidence a `returned` item must carry.",
      "domain": "operations",
      "from": "hospitality.lost_found_item",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.loyalty_program_at_brand",
      "cardinality": "N:1",
      "description": "Which brand owns the loyalty program; chain programs live at brand level even though recognition is delivered property-by-property.",
      "domain": "loyalty",
      "from": "hospitality.loyalty_program",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.loyalty_program_at_property",
      "cardinality": "N:M",
      "description": "Which properties participate in a program; a property can run several programs simultaneously (Bonvoy plus a soft-brand recognition program, SLH INVITED plus Hilton Honors), and the temporal bounds track a property leaving one consortium and joining another.",
      "domain": "loyalty",
      "from": "hospitality.loyalty_program",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.loyalty_program_for_consortium",
      "cardinality": "N:1",
      "description": "Which external consortium a consortium-run program belongs to (LHW Leaders Club references LHW); the validator permits this link only when program_type = consortium_soft.",
      "domain": "loyalty",
      "from": "hospitality.loyalty_program",
      "to": "hospitality.external_consortium"
    },
    {
      "api_name": "hospitality.link.maintenance_plan_for_asset",
      "cardinality": "N:1",
      "description": "The equipment a preventive plan looks after. Every order the plan raises is raised for the same asset; retiring the asset retires the plan.",
      "domain": "operations",
      "from": "hospitality.maintenance_plan",
      "to": "hospitality.asset"
    },
    {
      "api_name": "hospitality.link.maintenance_plan_for_room",
      "cardinality": "N:1",
      "description": "The room a preventive-maintenance plan looks after. Written once at definition; every work order the plan raises is raised against this room.",
      "domain": "operations",
      "from": "hospitality.maintenance_plan",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.management_agreement_covers_property",
      "cardinality": "1:N",
      "description": "The hotels an HMA covers — one for the Condor/PHR-pattern single-asset agreement, several for a Strand-pattern portfolio HMA. OneToMany: a property sits under at most one agreement (operator churn is deferred; see archetype4-design.md §10).",
      "domain": "management",
      "from": "hospitality.management_agreement",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.management_agreement_with_company",
      "cardinality": "N:1",
      "description": "The owner party to an HMA. One ownership group may hold several agreements over time; each agreement names exactly one owner.",
      "domain": "management",
      "from": "hospitality.management_agreement",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.material_at_brand",
      "cardinality": "N:1",
      "description": "The brand whose catalogue a material is in — shared by every house of the brand. A material is in exactly one catalogue: this or `material_at_property` (validator-enforced).",
      "domain": "operations",
      "from": "hospitality.material",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.material_at_property",
      "cardinality": "N:1",
      "description": "The house whose own catalogue a material is in. A material is in exactly one catalogue: this or `material_at_brand` (validator-enforced).",
      "domain": "operations",
      "from": "hospitality.material",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.membership_account_has_membership",
      "cardinality": "1:N",
      "description": "The account spine: one membership account holds many memberships, so a household or corporate account (the Aman Founders shape — one account, five memberships) carries the primary and dependents as separate access records.",
      "domain": "membership",
      "from": "hospitality.membership_account",
      "to": "hospitality.membership"
    },
    {
      "api_name": "hospitality.link.membership_at_property",
      "cardinality": "N:M",
      "description": "The declared or home-property attachment for a membership (Soho House Every House grants several concurrent properties on one membership); resolved access rights live on membership_grants_access_at_property, not here.",
      "domain": "membership",
      "from": "hospitality.membership",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.membership_at_tier",
      "cardinality": "N:1",
      "description": "The membership's current tier; tier transitions supersede rather than mutate — the old link is closed with effective_to and a new link opens with effective_from, preserving the tier history.",
      "domain": "membership",
      "from": "hospitality.membership",
      "to": "hospitality.membership_tier"
    },
    {
      "api_name": "hospitality.link.membership_for_person",
      "cardinality": "N:1",
      "description": "Which person holds this membership — the identity binding that attaches recognition, preferences, and consent to the access record; optional while a corporate seat floats unassigned.",
      "domain": "membership",
      "from": "hospitality.membership",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.membership_grants_access_at_property",
      "cardinality": "N:M",
      "description": "The materialised access-rights surface that booking and access control read: operator policy and reciprocity-derived access resolve into these links, distinct from the declared home attachment on membership_at_property.",
      "domain": "membership",
      "from": "hospitality.membership",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.membership_initiation_for_account",
      "cardinality": "N:1",
      "description": "Which account a one-time initiation fee is charged to; like subscriptions, the initiation attaches to the billing root, not an individual membership.",
      "domain": "membership",
      "from": "hospitality.membership_initiation",
      "to": "hospitality.membership_account"
    },
    {
      "api_name": "hospitality.link.membership_subscription_for_account",
      "cardinality": "N:1",
      "description": "Which account a recurring subscription bills; the subscription attaches to the account (the billing root) rather than an individual membership so one family rate spans the primary and dependents.",
      "domain": "membership",
      "from": "hospitality.membership_subscription",
      "to": "hospitality.membership_account"
    },
    {
      "api_name": "hospitality.link.menu_for_outlet",
      "cardinality": "N:1",
      "description": "Which outlet a menu is served at; an outlet carries many menus across its dayparts.",
      "domain": "fnb",
      "from": "hospitality.menu",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.menu_item_in_class",
      "cardinality": "N:1",
      "description": "The item class a menu item inherits VAT class, revenue category and course from; an item with no class refuses to ring.",
      "domain": "fnb",
      "from": "hospitality.menu_item",
      "to": "hospitality.item_class"
    },
    {
      "api_name": "hospitality.link.menu_item_in_section",
      "cardinality": "N:M",
      "description": "Where an item appears; one item can sit in several sections across menus (the breakfast and room-service menus share their eggs).",
      "domain": "fnb",
      "from": "hospitality.menu_item",
      "to": "hospitality.menu_section"
    },
    {
      "api_name": "hospitality.link.menu_item_prepared_at_station",
      "cardinality": "N:1",
      "description": "The default prep station a fired line of this item routes to; optional — an item with no station lands on the course's unrouted (expo) ticket.",
      "domain": "fnb",
      "from": "hospitality.menu_item",
      "to": "hospitality.prep_station"
    },
    {
      "api_name": "hospitality.link.menu_item_uses_modifier_group",
      "cardinality": "N:M",
      "description": "A choice an item asks for when rung; the link may tighten or loosen the group's bounds for this item (Square's per-item modifier list overrides).",
      "domain": "fnb",
      "from": "hospitality.menu_item",
      "to": "hospitality.modifier_group"
    },
    {
      "api_name": "hospitality.link.menu_item_uses_recipe",
      "cardinality": "N:1",
      "description": "The recipe a menu item binds to so COGS and allergens ride the operations recipe surface; optional — a bought-in bottled beer has no recipe.",
      "domain": "fnb",
      "from": "hospitality.menu_item",
      "to": "hospitality.recipe"
    },
    {
      "api_name": "hospitality.link.menu_section_of_menu",
      "cardinality": "N:1",
      "description": "Which menu a section heads; a menu is a list of sections.",
      "domain": "fnb",
      "from": "hospitality.menu_section",
      "to": "hospitality.menu"
    },
    {
      "api_name": "hospitality.link.merchant_account_for_property",
      "cardinality": "N:1",
      "description": "The property whose card payments settle to this processor account. A property keeps its closed accounts as history and has at most one that is not closed.",
      "domain": "finance",
      "from": "hospitality.merchant_account",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.message_triage_at_property",
      "cardinality": "N:1",
      "description": "The property a triage is scoped to; a denormalised anchor for tenant-scoped triage queues (\"suggested triages at this property\").",
      "domain": "messaging",
      "from": "hospitality.message_triage",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.message_triage_chained_action_target",
      "cardinality": "1:1",
      "description": "The object a triage's chained action wrote; typed to guest_conversation as a kernel placeholder, the real target is set at apply time pending the Phase 3 polymorphic primitive.",
      "domain": "messaging",
      "from": "hospitality.message_triage",
      "to": "hospitality.guest_conversation"
    },
    {
      "api_name": "hospitality.link.message_triage_escalated_to_staff_member",
      "cardinality": "N:1",
      "description": "The staff member a triage was escalated to; optional, absent when escalation routes to a queue or role rather than a named individual.",
      "domain": "messaging",
      "from": "hospitality.message_triage",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.message_triage_for_message",
      "cardinality": "N:1",
      "description": "The message a triage classified; N:1 so re-triage on the same message adds a new generation rather than overwriting.",
      "domain": "messaging",
      "from": "hospitality.message_triage",
      "to": "hospitality.guest_message"
    },
    {
      "api_name": "hospitality.link.message_triage_superseded_by",
      "cardinality": "N:1",
      "description": "The fresher triage that superseded this one on the same message; the prior moves to superseded and points at the new one.",
      "domain": "messaging",
      "from": "hospitality.message_triage",
      "to": "hospitality.message_triage"
    },
    {
      "api_name": "hospitality.link.modifier_option_adds_variant",
      "cardinality": "N:1",
      "description": "The sellable an option adds, so it carries that item's own VAT class and revenue category; optional — an option with no variant is a kitchen instruction (medium rare).",
      "domain": "fnb",
      "from": "hospitality.modifier_option",
      "to": "hospitality.menu_item_variant"
    },
    {
      "api_name": "hospitality.link.modifier_option_in_group",
      "cardinality": "N:1",
      "description": "The group an option is a choice in; a group lists many options.",
      "domain": "fnb",
      "from": "hospitality.modifier_option",
      "to": "hospitality.modifier_group"
    },
    {
      "api_name": "hospitality.link.night_audit_run_for_business_date",
      "cardinality": "N:1",
      "description": "Which accounting day a night-audit run closed; many-to-one because re-runs create fresh runs against the same business date.",
      "domain": "audit",
      "from": "hospitality.night_audit_run",
      "to": "hospitality.business_date"
    },
    {
      "api_name": "hospitality.link.no_show_posting_rule_at_property",
      "cardinality": "N:1",
      "description": "Which property a no-show posting rule applies at — the house whose night audit consults it when it marks a booking a no-show.",
      "domain": "finance",
      "from": "hospitality.no_show_posting_rule",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.outlet_at_property",
      "cardinality": "N:1",
      "description": "Which property this revenue center belongs to; every outlet reports under exactly one property's USALI statement.",
      "domain": "revenue",
      "from": "hospitality.outlet",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.outlet_at_venue",
      "cardinality": "N:M",
      "description": "The physical venues hosting this revenue center; many-to-many because one room can host two revenue centers (banquets and a restaurant) and the rooms outlet has no venue at all.",
      "domain": "revenue",
      "from": "hospitality.outlet",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.outlet_has_parent",
      "cardinality": "N:1",
      "description": "Rollup hierarchy: the parent outlet aggregates this outlet's revenue, e.g. an F&B aggregate outlet over Main Restaurant, Pool Bar, and Lobby Café.",
      "domain": "revenue",
      "from": "hospitality.outlet",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.owner_statement_for_property",
      "cardinality": "N:1",
      "description": "Which hotel a monthly owner statement reports. The statement is property-scoped; this is the navigation edge the owner package folds along.",
      "domain": "management",
      "from": "hospitality.owner_statement",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.owner_statement_under_agreement",
      "cardinality": "N:1",
      "description": "The HMA whose reporting covenant a statement discharges — derived at submit time from the active agreement covering the property, so per-owner reads fan out from the agreement without re-deriving coverage.",
      "domain": "management",
      "from": "hospitality.owner_statement",
      "to": "hospitality.management_agreement"
    },
    {
      "api_name": "hospitality.link.package_at_brand",
      "cardinality": "1:N",
      "description": "Brand-scoped catalog ownership: a template package defined at the brand and inherited downward by its properties, the same inheritance shape as rate plans.",
      "domain": "organization",
      "from": "hospitality.brand",
      "to": "hospitality.package"
    },
    {
      "api_name": "hospitality.link.package_at_property",
      "cardinality": "1:N",
      "description": "Property-scoped catalog ownership: a bespoke package defined at and sold by a single property; brand templates inherit downward via the brand link instead.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.package"
    },
    {
      "api_name": "hospitality.link.package_component_alternate_outlet",
      "cardinality": "N:M",
      "description": "Qualified alternate outlets where this component's allowance may be consumed (Opera's alternate-outlet pattern: a breakfast credit redeemable at any of several restaurants).",
      "domain": "revenue",
      "from": "hospitality.package_component",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.package_component_targets_service_item",
      "cardinality": "N:1",
      "description": "The catalog service_item this component represents: the component carries the allowance that consumption debits; the service_item carries the actual fulfilment (menu item, treatment, retail SKU).",
      "domain": "revenue",
      "from": "hospitality.package_component",
      "to": "hospitality.service_item"
    },
    {
      "api_name": "hospitality.link.package_has_active_version",
      "cardinality": "1:1",
      "description": "The operational handle: at most one active version per package; supersede_package moves this edge to the new version atomically so new sales always pin the current one.",
      "domain": "revenue",
      "from": "hospitality.package",
      "to": "hospitality.package_version"
    },
    {
      "api_name": "hospitality.link.package_has_component",
      "cardinality": "1:N",
      "description": "The components making up this package — each an allowance (breakfast credit, spa treatment) that consumption events debit.",
      "domain": "revenue",
      "from": "hospitality.package",
      "to": "hospitality.package_component"
    },
    {
      "api_name": "hospitality.link.package_has_version",
      "cardinality": "1:N",
      "description": "Temporal version history: every supersede_package mints a new package_version and adds an edge here, so the package's full pricing/composition history stays walkable (proposal 0024).",
      "domain": "revenue",
      "from": "hospitality.package",
      "to": "hospitality.package_version"
    },
    {
      "api_name": "hospitality.link.package_sale_has_consumption",
      "cardinality": "1:N",
      "description": "The consumption events recorded against this sale; each one debits a component allowance as the guest redeems it.",
      "domain": "revenue",
      "from": "hospitality.package_sale",
      "to": "hospitality.package_component_consumption"
    },
    {
      "api_name": "hospitality.link.package_sale_uses_package_version",
      "cardinality": "N:1",
      "description": "The frozen pin: which package_version this sale was assembled against. Written by assemble_package_sale and never mutated, so a later supersede cannot rewrite a sold guest's terms (proposal 0024).",
      "domain": "revenue",
      "from": "hospitality.package_sale",
      "to": "hospitality.package_version"
    },
    {
      "api_name": "hospitality.link.package_version_supersedes",
      "cardinality": "N:1",
      "description": "Supersedence chain: each supersede_package writes one edge from the new version to the version it replaces, giving the full audit trail without scanning effective_to ranges.",
      "domain": "revenue",
      "from": "hospitality.package_version",
      "to": "hospitality.package_version"
    },
    {
      "api_name": "hospitality.link.participant_dispatch_for_run",
      "cardinality": "N:1",
      "description": "The federated run a dispatch attempt belongs to; populated for federated-run dispatches (exactly one of the two dispatch anchors is set per row).",
      "domain": "federated_intelligence",
      "from": "hospitality.participant_dispatch_attempt",
      "to": "hospitality.federated_run"
    },
    {
      "api_name": "hospitality.link.participant_dispatch_for_workflow_run",
      "cardinality": "N:1",
      "description": "The workflow run a dispatch attempt belongs to; populated for workflow agent-installation dispatches (exactly one of the two dispatch anchors is set per row).",
      "domain": "federated_intelligence",
      "from": "hospitality.participant_dispatch_attempt",
      "to": "hospitality.workflow_run"
    },
    {
      "api_name": "hospitality.link.pattern_at_property",
      "cardinality": "N:1",
      "description": "The property a recurring-failure pattern was detected at; the anchor for tenant-scoped pattern review.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.pattern_at_room",
      "cardinality": "N:1",
      "description": "The room a pattern localises to, when the recurring failure traces to one physical room (a recurring maintenance fault); optional.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.pattern_at_venue",
      "cardinality": "N:1",
      "description": "The venue a pattern localises to, when the recurring failure is venue-specific rather than property-wide; optional.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.pattern_clusters_housekeeping_task",
      "cardinality": "N:M",
      "description": "The housekeeping tasks this pattern clusters as supporting evidence; M:N, tying a recurring failure to the housekeeping record it recurs through.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.housekeeping_task"
    },
    {
      "api_name": "hospitality.link.pattern_clusters_message_triage",
      "cardinality": "N:M",
      "description": "The message triages this pattern clusters as supporting evidence; M:N, the messaging-surface half of the pattern's cross-surface evidence set.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.message_triage"
    },
    {
      "api_name": "hospitality.link.pattern_clusters_pre_shift_lineup_event",
      "cardinality": "N:M",
      "description": "The pre-shift lineup events this pattern clusters as supporting evidence; M:N, linking a recurring failure to the briefings that surfaced it.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.pre_shift_lineup_event"
    },
    {
      "api_name": "hospitality.link.pattern_clusters_recovery_event",
      "cardinality": "N:M",
      "description": "The service-recovery events this pattern generalises over; M:N because one event can support several patterns and a pattern clusters many events as its evidence.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.service_recovery_event"
    },
    {
      "api_name": "hospitality.link.pattern_clusters_work_order",
      "cardinality": "N:M",
      "description": "The work orders this pattern clusters as supporting evidence; M:N, tying a recurring failure to the maintenance record it recurs through.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.work_order"
    },
    {
      "api_name": "hospitality.link.pattern_escalated_to_staff_member",
      "cardinality": "N:1",
      "description": "The staff member a pattern was escalated to; set only on escalate_via_pattern_assessment, absent on other terminal states.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.pattern_part_of_run",
      "cardinality": "N:1",
      "description": "The detection run that surfaced this pattern; a run emits many candidate patterns, each belonging to exactly one run.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.recovery_pattern_detection_run"
    },
    {
      "api_name": "hospitality.link.pattern_references_external_record",
      "cardinality": "N:1",
      "description": "An external record referenced as evidence or remediation context for the pattern; optional.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.pattern_references_service_philosophy",
      "cardinality": "N:1",
      "description": "The service philosophy frame a pattern was read against, giving its recommended remediation a top-cover/authority context; optional.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.service_philosophy"
    },
    {
      "api_name": "hospitality.link.pattern_remediation_chained_action_target",
      "cardinality": "1:1",
      "description": "The object a pattern's applied remediation wrote; typed to work_order as a kernel placeholder, the concrete host-orchestrated result rides applied_action_correlation_id.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.work_order"
    },
    {
      "api_name": "hospitality.link.pattern_superseded_by",
      "cardinality": "N:1",
      "description": "The later pattern that superseded this one when a fresh run re-detected the same failure; the prior points at the new generation.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.service_recovery_pattern",
      "to": "hospitality.service_recovery_pattern"
    },
    {
      "api_name": "hospitality.link.payment_card_for_person",
      "cardinality": "N:1",
      "description": "Whose card this is. A person may hold several — the card they booked with, the card they present at the desk — so the person is the one side; the reverse role is what a desk reads to offer a picker instead of asking for the card again.",
      "domain": "finance",
      "from": "hospitality.payment_card",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.payment_card_for_reservation",
      "cardinality": "N:1",
      "description": "The booking a card guarantees. Optional for a guest's own card (which belongs to them, not to one stay) and the only binding an OTA virtual card has, since a card funded for a single reservation has no cardholder to attach to.",
      "domain": "finance",
      "from": "hospitality.payment_card",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.payment_drawn_on_card",
      "cardinality": "N:1",
      "description": "The stored card a payment was drawn on. Survives the card's removal, because a chargeback defence needs to name the instrument that funded a settled payment long after the guest asked for it to be forgotten.",
      "domain": "finance",
      "from": "hospitality.payment",
      "to": "hospitality.payment_card"
    },
    {
      "api_name": "hospitality.link.payment_for_fraud_assessment",
      "cardinality": "1:1",
      "description": "The assessment that gated this payment's authorise-with-review (proposal 0017); 1:1 because an assessment is single-use per authorisation, keeping the chargeback chain-of-evidence intact.",
      "domain": "finance",
      "from": "hospitality.payment",
      "to": "hospitality.fraud_risk_assessment"
    },
    {
      "api_name": "hospitality.link.payment_for_reservation",
      "cardinality": "N:1",
      "description": "The booking money was taken for when no folio exists to record it against. A reservation may accumulate several — a deposit, then a no-show fee — so the reservation is the one side.",
      "domain": "finance",
      "from": "hospitality.payment",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.payment_refunds_payment",
      "cardinality": "N:1",
      "description": "Ties a refund-kind payment leg back to the original settled payment it reverses; several partial-refund legs may target one payment, refunds-of-refunds are rejected, and a failed leg retries as a fresh leg rather than chaining.",
      "domain": "finance",
      "from": "hospitality.payment",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.payment_request_for_reservation",
      "cardinality": "N:1",
      "description": "The booking a payment request asks money for — the reservation whose deposit, prepayment or fee the guest is asked to pay, and whose account the payment lands on.",
      "domain": "finance",
      "from": "hospitality.payment_request",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.payout_for_merchant_account",
      "cardinality": "N:1",
      "description": "The hotel's processor account whose balance a payout paid out. Written when the payout is first recorded; a closed account keeps its payouts, since its last ones land after it closes.",
      "domain": "finance",
      "from": "hospitality.payout",
      "to": "hospitality.merchant_account"
    },
    {
      "api_name": "hospitality.link.payout_line_for_payment",
      "cardinality": "N:1",
      "description": "The ledger payment a payout line's money came from — the capture, the refund leg (or the payment it refunds), the payment a dispute contests. Absent on a line that matched nothing, which counts into its payout's unmatched amount; a payment can sit on several lines across payouts (its charge in one, its refund in a later one).",
      "domain": "finance",
      "from": "hospitality.payout_line",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.payout_line_for_payout",
      "cardinality": "N:1",
      "description": "The payout a balance transaction was paid out in. One line, one payout: the processor pays each balance transaction out once.",
      "domain": "finance",
      "from": "hospitality.payout_line",
      "to": "hospitality.payout"
    },
    {
      "api_name": "hospitality.link.person_companion_of",
      "cardinality": "N:M",
      "description": "A typed, symmetric companion relationship between two persons; temporal, and carrying its own consent flag because the companion is a separate GDPR data subject from the recognised person.",
      "domain": "people",
      "from": "hospitality.person",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.person_frequents_venue",
      "cardinality": "N:M",
      "description": "A projection-level \"is a regular\" relationship, distinct from the per-visit rows; temporal and updated by recognise_person to drive cross-property recognition handoff.",
      "domain": "people",
      "from": "hospitality.person",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.person_merged_into",
      "cardinality": "N:1",
      "description": "Non-destructive golden-record merge: the from-side (subordinate) person has been resolved into the to-side (survivor) person. Both rows live on; the subordinate keeps every inbound link and is excluded from default listings via resolution_status. Closing the link (split_person_merge) is the unmerge.",
      "domain": "people",
      "from": "hospitality.person",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.philosophy_has_recovery_toolkit",
      "cardinality": "1:1",
      "description": "The recovery toolkit a philosophy unlocks; opting into service philosophy is what carries the toolkit, so a property without a philosophy can't hold one.",
      "domain": "service_philosophy",
      "from": "hospitality.service_philosophy",
      "to": "hospitality.recovery_toolkit"
    },
    {
      "api_name": "hospitality.link.platform_ledger_line_for_dispute",
      "cardinality": "N:1",
      "description": "The chargeback a platform line moved money for: the network's withdrawal and its reinstatement, what the card rail recovered from the hotel, the fee passed through, money returned to the hotel after a late win.",
      "domain": "finance",
      "from": "hospitality.platform_ledger_line",
      "to": "hospitality.payment_dispute"
    },
    {
      "api_name": "hospitality.link.platform_ledger_line_for_merchant_account",
      "cardinality": "N:1",
      "description": "The hotel's processor account a line on the platform's own balance was for — the account the charge was made on behalf of, the transfer's destination, the account the application fee was taken from.",
      "domain": "finance",
      "from": "hospitality.platform_ledger_line",
      "to": "hospitality.merchant_account"
    },
    {
      "api_name": "hospitality.link.platform_ledger_line_for_payment",
      "cardinality": "N:1",
      "description": "The ledger payment a platform line's money came from: the capture (its charge, its transfer, its application fee, a transfer reversal or fee refund that undid some of it), the refund leg a refund line paid out, the payment a chargeback contests. Absent on a line that matched nothing.",
      "domain": "finance",
      "from": "hospitality.platform_ledger_line",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.portfolio_has_property",
      "cardinality": "1:N",
      "description": "Which portfolio groups this property; the rollup edge chains use for regional, operating-company, or franchisee reporting and permissions.",
      "domain": "organization",
      "from": "hospitality.portfolio",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.prep_station_at_outlet",
      "cardinality": "N:1",
      "description": "Which outlet a prep station belongs to; an outlet's kitchen has many fulfilment stations (grill, saute, bar, expo).",
      "domain": "fnb",
      "from": "hospitality.prep_station",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.price_at_outlet",
      "cardinality": "N:1",
      "description": "The outlet a price row is narrowed to; optional — a row with no outlet applies wherever the variant is sold.",
      "domain": "fnb",
      "from": "hospitality.item_price",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.price_for_variant",
      "cardinality": "N:1",
      "description": "The variant a price row prices; a variant has many rows.",
      "domain": "fnb",
      "from": "hospitality.item_price",
      "to": "hospitality.menu_item_variant"
    },
    {
      "api_name": "hospitality.link.privacy_budget_for_participation",
      "cardinality": "N:1",
      "description": "A privacy-budget account dedicated to one participation; optional, since most budgets anchor at program / definition / composition-group scope instead.",
      "domain": "federated_intelligence",
      "from": "hospitality.privacy_budget_account",
      "to": "hospitality.federated_participation"
    },
    {
      "api_name": "hospitality.link.property_budget_for_property",
      "cardinality": "N:1",
      "description": "Which hotel an annual operating budget plans. The statement's variance columns and the performance test's NOI prong read back through this edge.",
      "domain": "management",
      "from": "hospitality.property_budget",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.property_has_availability_day",
      "cardinality": "1:N",
      "description": "The property's per-date inventory ledger; together with the room-type edge each row pins the (property, room_type, business_date) cell that sellable-count truth lives in.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.availability_day"
    },
    {
      "api_name": "hospitality.link.property_has_business_date",
      "cardinality": "1:N",
      "description": "The property's accounting-day ledger; operational objects attach to a business date rather than wall-clock UTC, and each day is opened and closed per property.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.business_date"
    },
    {
      "api_name": "hospitality.link.property_has_channel_mix_snapshot",
      "cardinality": "1:N",
      "description": "The channel-mix snapshots materialised for this property; one row per (property, business_date, cadence), so the property accumulates a snapshot history.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.channel_mix_snapshot"
    },
    {
      "api_name": "hospitality.link.property_has_design_dna",
      "cardinality": "N:M",
      "description": "A design language the property expresses; many-to-many because a property can layer a core brand DNA with per-venue DNAs.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.design_dna"
    },
    {
      "api_name": "hospitality.link.property_has_night_audit_run",
      "cardinality": "1:N",
      "description": "The property's night-audit executions; a run's results are immutable on completion, so re-runs append new rows rather than mutating a finished one.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.night_audit_run"
    },
    {
      "api_name": "hospitality.link.property_has_philosophy",
      "cardinality": "1:1",
      "description": "The one service philosophy an opted-in property runs; properties without this link simply haven't opted into the Tier 1.2 framework.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.service_philosophy"
    },
    {
      "api_name": "hospitality.link.property_has_rate_plan",
      "cardinality": "1:N",
      "description": "Which property owns a bespoke rate plan; brand-standard plans attach via brand_has_rate_plan instead.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.property_has_room",
      "cardinality": "1:N",
      "description": "Which property a physical room belongs to; a room never moves between properties.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.property_has_room_type",
      "cardinality": "1:N",
      "description": "Which property offers this room type; guests book against the type and are only assigned a physical room at check-in.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.property_has_venue",
      "cardinality": "1:N",
      "description": "Which property contains this venue; venues are the sub-areas (restaurant, bar, spa, rooms block) where service items are sold and operational work happens.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.property_owned_by_company",
      "cardinality": "N:1",
      "description": "The owning entity of a property — the company (kind = hotel_owner) that holds the asset and, where branded, the franchise agreement. Distinct from the brand the property flies and from the operator running it under a management_agreement.",
      "domain": "organization",
      "from": "hospitality.property",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.proposed_action_confirmed_via_approval",
      "cardinality": "N:1",
      "description": "The formal 0020 approval gate that confirmed this proposal, set when requires_confirmation_from = approval_gate rather than a direct human/guest tap; optional.",
      "domain": "proposed_action",
      "from": "hospitality.proposed_action",
      "to": "hospitality.approval_gate"
    },
    {
      "api_name": "hospitality.link.proposed_action_for_conversation",
      "cardinality": "N:1",
      "description": "The guest conversation this proposal arose from, set when an inbound message (proposal 0014) triggered the agent to propose an action; optional.",
      "domain": "proposed_action",
      "from": "hospitality.proposed_action",
      "to": "hospitality.guest_conversation"
    },
    {
      "api_name": "hospitality.link.proposed_action_superseded_by",
      "cardinality": "N:1",
      "description": "The later proposal that replaced this one for the same (target_action, subject_ref); set when a re-proposal supersedes a still-pending proposal.",
      "domain": "proposed_action",
      "from": "hospitality.proposed_action",
      "to": "hospitality.proposed_action"
    },
    {
      "api_name": "hospitality.link.protocol_references_modalities",
      "cardinality": "N:M",
      "description": "Which modalities the protocol prescribes — HBOT twice a week plus contrast bathing three times a week plus daily resistance training compose one protocol from several modalities.",
      "domain": "membership",
      "from": "hospitality.wellness_protocol",
      "to": "hospitality.wellness_modality"
    },
    {
      "api_name": "hospitality.link.publication_run_for_connection",
      "cardinality": "N:1",
      "description": "Which conduit an ARI push ran against; the conduit accumulates its publication history, which is the operator's answer to 'did last night's rate change reach the OTAs'.",
      "domain": "channel_manager",
      "from": "hospitality.channel_publication_run",
      "to": "hospitality.channel_connection"
    },
    {
      "api_name": "hospitality.link.purpose_grant_for_purpose",
      "cardinality": "N:1",
      "description": "Which `access_purpose` this grant confers. The grant also denormalises the purpose's business id (`purpose_ref`) so the enforcement join is one hop.",
      "domain": "pbac",
      "from": "hospitality.purpose_grant",
      "to": "hospitality.access_purpose"
    },
    {
      "api_name": "hospitality.link.rail_instruction_for_payment",
      "cardinality": "N:1",
      "description": "The hold or settled payment a queued capture, release or refund acts on — the payment the desk named when the card rail could not reach the processor.",
      "domain": "finance",
      "from": "hospitality.rail_instruction",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.rail_instruction_for_stay",
      "cardinality": "N:1",
      "description": "The departed stay whose late charges a queued collection takes (its kept hold, then its card on file), or the in-house stay whose top-up a queued Top up now carries out.",
      "domain": "finance",
      "from": "hospitality.rail_instruction",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.rate_plan_has_rate_per_night",
      "cardinality": "1:N",
      "description": "The explicit per-date price rows a rate plan publishes; each row prices one (rate_plan, room_type, occupancy, date) tuple with no derivation implied.",
      "domain": "booking",
      "from": "hospitality.rate_plan",
      "to": "hospitality.rate_per_night"
    },
    {
      "api_name": "hospitality.link.rate_plan_has_restriction",
      "cardinality": "1:N",
      "description": "Which rate plan a sell restriction (CTA / CTD / MinLOS / MaxLOS / stop-sell) constrains; multiple restrictions on the same dates compose as a conjunction.",
      "domain": "booking",
      "from": "hospitality.rate_plan",
      "to": "hospitality.rate_restriction"
    },
    {
      "api_name": "hospitality.link.rate_plan_requires_cancellation_policy",
      "cardinality": "N:1",
      "description": "The rate-plan cancellation override, ranked above the property default exactly as the deposit engine ranks `rate_plan_requires_deposit_policy`. Apaleo requires every rate plan to carry a cancellation policy for the same reason: advance-purchase and event-date rates are sold on their terms, so the terms belong with the rate.",
      "domain": "booking",
      "from": "hospitality.rate_plan",
      "to": "hospitality.cancellation_policy"
    },
    {
      "api_name": "hospitality.link.rate_plan_requires_deposit_policy",
      "cardinality": "N:1",
      "description": "The rate-plan deposit override (OPERA precedence: rate code beats reservation type beats manual): reservations booked on this rate plan derive their deposit from this policy instead of the property default. Advance-purchase and event-date rates are the classic carriers.",
      "domain": "booking",
      "from": "hospitality.rate_plan",
      "to": "hospitality.deposit_policy"
    },
    {
      "api_name": "hospitality.link.receipt_for_check",
      "cardinality": "N:1",
      "description": "The check a receipt was issued for. Many-to-one: a receipt is never edited, so a check reopened and settled again would be issued another.",
      "domain": "fnb",
      "from": "hospitality.check_receipt",
      "to": "hospitality.guest_check"
    },
    {
      "api_name": "hospitality.link.recipe_at_brand",
      "cardinality": "N:1",
      "description": "Which brand owns a brand-standard recipe shared across its properties; bespoke recipes attach at the property instead.",
      "domain": "operations",
      "from": "hospitality.recipe",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.recipe_at_property",
      "cardinality": "N:1",
      "description": "Which property owns a property-specific recipe; brand-standard recipes attach at brand instead.",
      "domain": "operations",
      "from": "hospitality.recipe",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.recipe_for_service_item",
      "cardinality": "1:1",
      "description": "The production-side spec behind a saleable item; strictly one recipe per service item so pour cost and allergen disclosure resolve unambiguously.",
      "domain": "operations",
      "from": "hospitality.recipe",
      "to": "hospitality.service_item"
    },
    {
      "api_name": "hospitality.link.recipe_uses_ingredient",
      "cardinality": "N:M",
      "description": "The typed ingredients a recipe consumes; the edges the pour-cost rollup and the allergen roll-up walk.",
      "domain": "operations",
      "from": "hospitality.recipe",
      "to": "hospitality.ingredient"
    },
    {
      "api_name": "hospitality.link.recipe_uses_sub_recipe",
      "cardinality": "N:M",
      "description": "Recipe composition: a component recipe (a clarified milk-wash, a base sauce) is defined once and referenced by every recipe built from it.",
      "domain": "operations",
      "from": "hospitality.recipe",
      "to": "hospitality.recipe"
    },
    {
      "api_name": "hospitality.link.reciprocity_agreement_between_orgs",
      "cardinality": "N:M",
      "description": "Which organisations participate in a reciprocity agreement — a bilateral deal creates two links, a multi-party network (Every House) creates N; this participation surface is what drives resolved-access materialisation into membership_grants_access_at_property.",
      "domain": "membership",
      "from": "hospitality.reciprocity_agreement",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.recognition_event_at_property",
      "cardinality": "N:1",
      "description": "Where the recognition happened; recognition occurs in physical operating space, so the event carries a property even when the program's scope is brand-level.",
      "domain": "loyalty",
      "from": "hospitality.recognition_event",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.recognition_event_for_person",
      "cardinality": "N:1",
      "description": "Who was recognised; exactly one person per event — a multi-person recognition creates N events sharing recognition_subject_text, mirroring the recovery-event discipline from proposal 0002.",
      "domain": "loyalty",
      "from": "hospitality.recognition_event",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.recognition_event_for_program",
      "cardinality": "N:1",
      "description": "Which loyalty program the recognition happened under; optional because Aman-style recognition-only cultures record events with no formal program to reference.",
      "domain": "loyalty",
      "from": "hospitality.recognition_event",
      "to": "hospitality.loyalty_program"
    },
    {
      "api_name": "hospitality.link.recommendation_applied_to_rate_per_night",
      "cardinality": "N:1",
      "description": "The rate_per_night row a recommendation's price was written back to; absent until the apply transition lands. N:1 since a row takes every move applied to it over its life.",
      "domain": "revenue_management",
      "from": "hospitality.rate_recommendation",
      "to": "hospitality.rate_per_night"
    },
    {
      "api_name": "hospitality.link.recommendation_at_property",
      "cardinality": "N:1",
      "description": "The property a recommendation was raised at; the denormalised anchor for tenant-scoped recommendation queries.",
      "domain": "revenue_management",
      "from": "hospitality.rate_recommendation",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.recommendation_for_rate_plan",
      "cardinality": "N:1",
      "description": "The rate plan a recommendation targets; a rate plan accumulates many recommendations over time, each belonging to exactly one.",
      "domain": "revenue_management",
      "from": "hospitality.rate_recommendation",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.recommendation_for_room_type",
      "cardinality": "N:1",
      "description": "The room type a recommendation targets; an anchor for indexed lookup, N:1 since a room type accumulates many recommendations.",
      "domain": "revenue_management",
      "from": "hospitality.rate_recommendation",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.recommendation_part_of_run",
      "cardinality": "N:1",
      "description": "The run that produced this recommendation; an RMS batch run emits many recommendations, each belonging to one run.",
      "domain": "revenue_management",
      "from": "hospitality.rate_recommendation",
      "to": "hospitality.rate_recommendation_run"
    },
    {
      "api_name": "hospitality.link.recommendation_superseded_by",
      "cardinality": "N:1",
      "description": "The newer recommendation that replaced this one for the same (rate_plan, room_type, business_date, occupancy) tuple; N:1 so a successor may supersede several skipped prior generations.",
      "domain": "revenue_management",
      "from": "hospitality.rate_recommendation",
      "to": "hospitality.rate_recommendation"
    },
    {
      "api_name": "hospitality.link.recovery_event_for_person",
      "cardinality": "N:1",
      "description": "The guest who experienced the service failure; one person per event, so multi-person incidents log as N events sharing a failure_subject_text for joining.",
      "domain": "service_philosophy",
      "from": "hospitality.service_recovery_event",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.recovery_event_in_venue",
      "cardinality": "N:1",
      "description": "Where the failure happened; every recovery event has a venue, defaulting to the property's front-desk/lobby venue when not otherwise located.",
      "domain": "service_philosophy",
      "from": "hospitality.service_recovery_event",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.recovery_pattern_run_for_property",
      "cardinality": "N:1",
      "description": "The property a detection run swept; the property accumulates its run history for cadence tracking.",
      "domain": "recovery_pattern_detection",
      "from": "hospitality.recovery_pattern_detection_run",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.registration_card_for_reservation",
      "cardinality": "N:1",
      "description": "The booking the signed stay executes — how a chargeback's evidence, which starts from the disputed payment's booking, finds the card.",
      "domain": "booking",
      "from": "hospitality.registration_card",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.registration_card_for_stay",
      "cardinality": "N:1",
      "description": "The stay a registration card was signed for at check-in. A stay has one current card; a second signing replaces it and the first is kept as superseded.",
      "domain": "booking",
      "from": "hospitality.registration_card",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.reservation_booked_in_session",
      "cardinality": "N:1",
      "description": "The shopping session a booking was made in, as its first offer — the offer whose id names the session. A session with a booking on this edge is no regret, whichever of its offers the guest took; one without is (`regret_record`).",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.stay_offer"
    },
    {
      "api_name": "hospitality.link.reservation_delivery_for_connection",
      "cardinality": "N:1",
      "description": "The conduit an inbound reservation message arrived on. Scopes the `external_message_id` de-duplication key: two partners may legitimately mint the same message id.",
      "domain": "channel_manager",
      "from": "hospitality.channel_reservation_delivery",
      "to": "hospitality.channel_connection"
    },
    {
      "api_name": "hospitality.link.reservation_delivery_from_channel",
      "cardinality": "N:1",
      "description": "The demand channel that actually sold the booking, read off the message rather than inferred from the conduit. Absent while the message names a channel code we have no `channel` row for — which is itself the signal to add one.",
      "domain": "channel_manager",
      "from": "hospitality.channel_reservation_delivery",
      "to": "hospitality.channel"
    },
    {
      "api_name": "hospitality.link.reservation_delivery_produced_reservation",
      "cardinality": "N:1",
      "description": "What the message became once applied. Absent while the delivery is received, rejected or failed. ManyToOne rather than OneToOne because a reservation accumulates deliveries: the original booking, then every modification and the cancellation, all naming the same `external_reservation_id`.",
      "domain": "channel_manager",
      "from": "hospitality.channel_reservation_delivery",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.reservation_for_company",
      "cardinality": "N:1",
      "description": "The corporate account behind the booking, when there is one; drives negotiated-rate eligibility and company A/R settlement.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.reservation_for_guest",
      "cardinality": "N:1",
      "description": "The booking's primary guest — the person confirmations, the folio default, and guest communications key on.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.reservation_for_property",
      "cardinality": "N:1",
      "description": "Which property the reservation is booked at; fixed at booking time and the anchor for the stay's business dates, currency, and inventory.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.reservation_for_room_type",
      "cardinality": "N:1",
      "description": "The room type booked at reservation level — the convenience handle for single-room bookings; per-room detail lives on reservation_room_for_room_type.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.reservation_has_folio",
      "cardinality": "1:N",
      "description": "The booking's own account, for money owed before — or instead of — a stay. A booking that never checks in has no `stay_has_folio` to reach: the night audit opens this account when it posts a no-show fee, and `forfeit_deposit` opens it for a cancellation's retained deposit, so either charge can be settled and invoiced like any other. OPERA's reservation account, which Billing opens on a no-show or cancelled reservation.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.reservation_has_room",
      "cardinality": "1:N",
      "description": "The per-physical-room slices of a reservation; a '3 rooms x 4 nights' booking has three reservation_room children, each owning its own type, assignment, and nights.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.reservation_room"
    },
    {
      "api_name": "hospitality.link.reservation_has_stay",
      "cardinality": "1:N",
      "description": "The executions of the reservation once guests check in — one stay per reservation_room (the multi-room stay model: the physical room is the execution unit, exactly as Mews books one reservation per space and OPERA splits a multi-room booking at check-in). A single-room reservation therefore has exactly one stay; the per-leg anchor is stay_for_reservation_room.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.reservation_has_tax_exemption",
      "cardinality": "1:N",
      "description": "The tax exemptions a booking has carried. At most one is in force (no `revoked_at`); revoked rows stay as the trail of what the booking was exempt from and when.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.tax_exemption"
    },
    {
      "api_name": "hospitality.link.reservation_in_share_group",
      "cardinality": "N:1",
      "description": "Active share membership. ManyToOne is the one-share-per-reservation invariant. The edge is end-dated — never deleted — when the member departs, cancels, no-shows, or the group separates, so the share stays auditable history while the cardinality slot frees for a later share. The availability release paths and the double-assignment guard both key on the live edge.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.share_group"
    },
    {
      "api_name": "hospitality.link.reservation_includes_package",
      "cardinality": "N:M",
      "description": "The package_sales attached to this reservation; many-to-many because one stay can carry several packages and a sale may span combined group bookings.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.package_sale"
    },
    {
      "api_name": "hospitality.link.reservation_night_uses_rate_plan",
      "cardinality": "N:1",
      "description": "The rate plan in effect for one specific night; overrides the reservation-level plan so mid-stay rate changes, comp nights, and plan switches are representable.",
      "domain": "booking",
      "from": "hospitality.reservation_night",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.reservation_picked_up_from_block",
      "cardinality": "N:1",
      "description": "The block a reservation was picked up from. Drives the blocked -> sold move on pickup and, on cancellation, whether the room returns to the block or to house availability.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.group_block"
    },
    {
      "api_name": "hospitality.link.reservation_room_assigned_to",
      "cardinality": "N:1",
      "description": "The physical room currently assigned to this slice; a room move replaces the edge atomically in the same transaction, and re-assigning the same room is an idempotent no-op.",
      "domain": "booking",
      "from": "hospitality.reservation_room",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.reservation_room_charged_as_room_type",
      "cardinality": "N:1",
      "description": "The room type this slice is *charged* as when it differs from the physical type — OPERA's roomTypeCharged as a data fact. Pinned by a keep_rate cross-type room move (the comp upgrade: the guest sleeps in the suite, pays the booked type), cleared when a requote / override re-bases pricing on the physical type. Absent means charged-as == physical.",
      "domain": "booking",
      "from": "hospitality.reservation_room",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.reservation_room_for_room_type",
      "cardinality": "N:1",
      "description": "The room type this slice was booked as; in a multi-room booking each room can be a different type, so assignment checks run against this edge, not the reservation-level one. After an in-house cross-type move this edge tracks the *physical* room's type (availability truth); the pricing basis is reservation_room_charged_as_room_type when a keep_rate move pinned one.",
      "domain": "booking",
      "from": "hospitality.reservation_room",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.reservation_room_has_night",
      "cardinality": "1:N",
      "description": "The per-night detail a reservation_room owns; one reservation_night per occupied date carries that night's rate amount, rate plan, and room type.",
      "domain": "booking",
      "from": "hospitality.reservation_room",
      "to": "hospitality.reservation_night"
    },
    {
      "api_name": "hospitality.link.reservation_uses_cancellation_policy",
      "cardinality": "N:1",
      "description": "The structured cancellation terms bound at booking time; temporal so an audit can reconstruct which policy version was in effect when, while the immutable wording snapshot lives on the reservation itself.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.cancellation_policy"
    },
    {
      "api_name": "hospitality.link.reservation_uses_rate_plan",
      "cardinality": "N:1",
      "description": "The rate plan the booking was sold under at reservation level; per-night overrides (mid-stay rate changes) live on reservation_night_uses_rate_plan.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.reservation_via_channel",
      "cardinality": "N:1",
      "description": "The distribution channel a reservation arrived through. Typed edge rather than the free-text channel_reference, so channel commission and channel mix can resolve a policy instead of guessing from a string.",
      "domain": "booking",
      "from": "hospitality.reservation",
      "to": "hospitality.channel"
    },
    {
      "api_name": "hospitality.link.restriction_recommendation_applied_to_rate_restriction",
      "cardinality": "1:1",
      "description": "The rate_restriction row an applied restriction recommendation authored; 1:1, absent until the apply transition lands.",
      "domain": "revenue_management",
      "from": "hospitality.restriction_recommendation",
      "to": "hospitality.rate_restriction"
    },
    {
      "api_name": "hospitality.link.restriction_recommendation_at_property",
      "cardinality": "N:1",
      "description": "The property a restriction recommendation was raised at; the anchor the expiry sweep and tenant-scoped queries walk.",
      "domain": "revenue_management",
      "from": "hospitality.restriction_recommendation",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.restriction_recommendation_for_rate_plan",
      "cardinality": "N:1",
      "description": "The rate plan a restriction recommendation is scoped to; absent when the scope is a room type alone.",
      "domain": "revenue_management",
      "from": "hospitality.restriction_recommendation",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.restriction_recommendation_for_room_type",
      "cardinality": "N:1",
      "description": "The room type a restriction recommendation is scoped to; absent when the scope is a rate plan alone.",
      "domain": "revenue_management",
      "from": "hospitality.restriction_recommendation",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.restriction_recommendation_lifted_rate_restriction",
      "cardinality": "1:N",
      "description": "The rate_restriction rows an applied lift recommendation took off the board — each stamped `lifted_at` and kept as history; absent until the apply transition lands.",
      "domain": "revenue_management",
      "from": "hospitality.restriction_recommendation",
      "to": "hospitality.rate_restriction"
    },
    {
      "api_name": "hospitality.link.restriction_recommendation_part_of_run",
      "cardinality": "N:1",
      "description": "The run that produced this restriction recommendation; a run carries rate and restriction recommendations side by side, and its counters count both.",
      "domain": "revenue_management",
      "from": "hospitality.restriction_recommendation",
      "to": "hospitality.rate_recommendation_run"
    },
    {
      "api_name": "hospitality.link.restriction_recommendation_superseded_by",
      "cardinality": "N:1",
      "description": "The newer restriction recommendation that replaced this one for the same scope and date range; N:1 so a successor may supersede several skipped prior generations.",
      "domain": "revenue_management",
      "from": "hospitality.restriction_recommendation",
      "to": "hospitality.restriction_recommendation"
    },
    {
      "api_name": "hospitality.link.retail_product_available_at_property",
      "cardinality": "N:M",
      "description": "A property a retail product is placed at (in-room amenity bottles not tied to one venue); many-to-many and temporal.",
      "domain": "brand_venue",
      "from": "hospitality.retail_product",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.retail_product_available_at_venue",
      "cardinality": "N:M",
      "description": "A venue a retail product is sold at (spa shelves, gift shops, back-bar offerings); many-to-many and temporal.",
      "domain": "brand_venue",
      "from": "hospitality.retail_product",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.retail_product_belongs_to_line",
      "cardinality": "N:1",
      "description": "The product line a retail product belongs to; every product rolls up to exactly one line.",
      "domain": "brand_venue",
      "from": "hospitality.retail_product",
      "to": "hospitality.retail_product_line"
    },
    {
      "api_name": "hospitality.link.retail_product_line_has_brand_parent",
      "cardinality": "N:1",
      "description": "The group/property brand a retail line rolls up to (Cowshed under Soho House Group); compatible with the venue-brand parent since a line can straddle both surfaces.",
      "domain": "brand_venue",
      "from": "hospitality.retail_product_line",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.retail_product_line_has_venue_brand_parent",
      "cardinality": "N:1",
      "description": "The venue brand a retail line rolls up to (Cowshed under the Cowshed venue brand); compatible with the group-brand parent.",
      "domain": "brand_venue",
      "from": "hospitality.retail_product_line",
      "to": "hospitality.venue_brand"
    },
    {
      "api_name": "hospitality.link.room_discrepancy_for_room",
      "cardinality": "N:1",
      "description": "The room a skip/sleep/person discrepancy was raised against; open discrepancies are the front desk's investigation queue and resolve with a typed outcome.",
      "domain": "inventory",
      "from": "hospitality.room_discrepancy",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.room_queue_entry_for_reservation",
      "cardinality": "N:1",
      "description": "The reservation waiting at the desk for its room to turn. At most one waiting entry per reservation (handler-enforced); the room being turned is derived through the reservation's assignment, never copied onto the entry.",
      "domain": "operations",
      "from": "hospitality.room_queue_entry",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.room_status_block_for_room",
      "cardinality": "N:1",
      "description": "The room an out-of-order / out-of-service block applies to; a room accumulates blocks over its life and at most one active block at a time (handler-enforced — overlapping active blocks refuse).",
      "domain": "inventory",
      "from": "hospitality.room_status_block",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.room_type_has_availability_day",
      "cardinality": "1:N",
      "description": "Which room type an inventory-ledger day counts; physical / sellable / sold are tracked per room type per business date.",
      "domain": "inventory",
      "from": "hospitality.room_type",
      "to": "hospitality.availability_day"
    },
    {
      "api_name": "hospitality.link.room_type_has_rate_per_night",
      "cardinality": "1:N",
      "description": "The room type a per-date price row prices; together with the rate-plan edge it pins the (rate_plan, room_type, occupancy, date) tuple.",
      "domain": "inventory",
      "from": "hospitality.room_type",
      "to": "hospitality.rate_per_night"
    },
    {
      "api_name": "hospitality.link.room_type_has_restriction",
      "cardinality": "1:N",
      "description": "Which room type a sell restriction constrains; paired with the rate-plan edge to scope the restriction to a rate plan / room type pair.",
      "domain": "inventory",
      "from": "hospitality.room_type",
      "to": "hospitality.rate_restriction"
    },
    {
      "api_name": "hospitality.link.room_type_has_room",
      "cardinality": "1:N",
      "description": "Which category a physical room sells as; room assignment walks this edge to keep the assigned room consistent with the type that was booked.",
      "domain": "inventory",
      "from": "hospitality.room_type",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.round_template_at_property",
      "cardinality": "N:1",
      "description": "The house whose walk a round template is. Written once at definition; a template's name is unique among the house's active templates, and its checkpoints name only this house's rooms and equipment.",
      "domain": "operations",
      "from": "hospitality.round_template",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.run_for_property",
      "cardinality": "N:1",
      "description": "The property a recommendation run swept; the property accumulates its RMS run history.",
      "domain": "revenue_management",
      "from": "hospitality.rate_recommendation_run",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.sell_limit_recommendation_applied_to_availability_day",
      "cardinality": "N:M",
      "description": "The availability_day rows an applied sell-limit recommendation moved, one per night of its range; absent until the apply transition lands. A row is moved by many recommendations over its life.",
      "domain": "revenue_management",
      "from": "hospitality.sell_limit_recommendation",
      "to": "hospitality.availability_day"
    },
    {
      "api_name": "hospitality.link.sell_limit_recommendation_at_property",
      "cardinality": "N:1",
      "description": "The property a sell-limit recommendation was raised at; the anchor the expiry sweep and tenant-scoped queries walk.",
      "domain": "revenue_management",
      "from": "hospitality.sell_limit_recommendation",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.sell_limit_recommendation_for_room_type",
      "cardinality": "N:1",
      "description": "The room type whose availability ledger a sell-limit recommendation would move; a room type accumulates many.",
      "domain": "revenue_management",
      "from": "hospitality.sell_limit_recommendation",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.sell_limit_recommendation_part_of_run",
      "cardinality": "N:1",
      "description": "The run that produced this sell-limit recommendation; a run carries rate, restriction and sell-limit recommendations side by side, and its counters count all three.",
      "domain": "revenue_management",
      "from": "hospitality.sell_limit_recommendation",
      "to": "hospitality.rate_recommendation_run"
    },
    {
      "api_name": "hospitality.link.sell_limit_recommendation_superseded_by",
      "cardinality": "N:1",
      "description": "The newer sell-limit recommendation that replaced this one for the same room type and date range; N:1 so a successor may supersede several skipped prior generations.",
      "domain": "revenue_management",
      "from": "hospitality.sell_limit_recommendation",
      "to": "hospitality.sell_limit_recommendation"
    },
    {
      "api_name": "hospitality.link.service_area_of_outlet",
      "cardinality": "N:1",
      "description": "Which outlet a floor area belongs to; an outlet's floor is divided into many service areas.",
      "domain": "fnb",
      "from": "hospitality.service_area",
      "to": "hospitality.outlet"
    },
    {
      "api_name": "hospitality.link.service_item_is_retail_product",
      "cardinality": "N:1",
      "description": "The retail product a POS service item sells, so a venue can sell it without duplicating identity; the validator forces service_item.category = retail when this link is present.",
      "domain": "operations",
      "from": "hospitality.service_item",
      "to": "hospitality.retail_product"
    },
    {
      "api_name": "hospitality.link.service_table_in_area",
      "cardinality": "N:1",
      "description": "Which floor area a table sits in; an area holds many tables.",
      "domain": "fnb",
      "from": "hospitality.service_table",
      "to": "hospitality.service_area"
    },
    {
      "api_name": "hospitality.link.share_group_at_property",
      "cardinality": "N:1",
      "description": "The house whose room the sharers occupy. Written once at the first combine; every member reservation must already stand at the same property.",
      "domain": "booking",
      "from": "hospitality.share_group",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.shift_at_property",
      "cardinality": "N:1",
      "description": "Where a shift was worked; the property accumulates the shifts that drive its daily labour rollup.",
      "domain": "labour",
      "from": "hospitality.staff_shift",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.shift_for_staff_member",
      "cardinality": "N:1",
      "description": "Who worked this shift; the staff member accumulates a shift history for scheduling and labour-cost attribution.",
      "domain": "labour",
      "from": "hospitality.staff_shift",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.spa_service_for_modality",
      "cardinality": "N:1",
      "description": "The wellness modality a service instantiates (proposal 0005 reconciliation): taxonomy, evidence level, and the requires_medical_licensing gate ride the modality; price, duration, and resourcing ride the service. Optional — a plain Swedish massage needs no modality binding.",
      "domain": "spa",
      "from": "hospitality.spa_service",
      "to": "hospitality.wellness_modality"
    },
    {
      "api_name": "hospitality.link.staff_certification_for_staff",
      "cardinality": "N:1",
      "description": "Which staff member holds a certification grant; renewals append rows, and the current credential of a kind is the latest non-revoked, non-expired row.",
      "domain": "people",
      "from": "hospitality.staff_certification",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.staff_employment_period_for_staff",
      "cardinality": "N:1",
      "description": "Whose employment history a period row records; rehire cycles append rows, and at most one row per staff member is open (null termination_date) at a time.",
      "domain": "people",
      "from": "hospitality.staff_employment_period",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.staff_performs_spa_service",
      "cardinality": "N:M",
      "description": "Which services a provider can perform (Book4Time's per-technician Performed By map). Declared-optional: book_appointment enforces the map only when the provider declares at least one performable service.",
      "domain": "people",
      "from": "hospitality.staff_member",
      "to": "hospitality.spa_service"
    },
    {
      "api_name": "hospitality.link.staff_works_at_property",
      "cardinality": "N:1",
      "description": "The staff member's home property; chain-spanning corporate roles are modelled at higher scope levels rather than by multiplying this edge.",
      "domain": "people",
      "from": "hospitality.staff_member",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.stay_for_reservation_room",
      "cardinality": "1:1",
      "description": "The reservation_room this stay executes — strictly one stay per leg; check_in refuses to mint a second rather than break the cardinality. Written atomically with reservation_has_stay at check-in; per-leg consumers (check_out, move_room, the audit's room posting) resolve the stay through this edge, never by picking the reservation's first stay.",
      "domain": "booking",
      "from": "hospitality.stay",
      "to": "hospitality.reservation_room"
    },
    {
      "api_name": "hospitality.link.stay_has_folio",
      "cardinality": "1:N",
      "description": "The stay's accounting ledgers. Check-in mints the primary folio (window 1); a stay carries up to eight folio windows (`folio.window_number` 1..=8, the split-billing surface — guest window vs company window), each belonging to exactly one stay. Posting flows that don't specify a window target the primary.",
      "domain": "booking",
      "from": "hospitality.stay",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.stay_in_room",
      "cardinality": "N:1",
      "description": "The physical room the stay occupies, wired at check-in from the reservation_room's assigned room.",
      "domain": "booking",
      "from": "hospitality.stay",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.stay_offer_for_property",
      "cardinality": "N:1",
      "description": "The house that quoted the offer. A session belongs to one house: its offers and the booking made in it are all this property's.",
      "domain": "booking",
      "from": "hospitality.stay_offer",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.stay_offer_for_rate_plan",
      "cardinality": "N:1",
      "description": "The plan the offer quoted: a regret is a price declined on a plan, and the plan says which rung it was.",
      "domain": "booking",
      "from": "hospitality.stay_offer",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.stay_offer_for_room_type",
      "cardinality": "N:1",
      "description": "The room type the offer quoted — OPERA's regret row keeps the room category beside the rate code.",
      "domain": "booking",
      "from": "hospitality.stay_offer",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.stock_level_for_material",
      "cardinality": "N:1",
      "description": "The material a stock level counts. Every level has exactly one (validator-enforced); the material's incoming edges are where it is kept.",
      "domain": "operations",
      "from": "hospitality.stock_level",
      "to": "hospitality.material"
    },
    {
      "api_name": "hospitality.link.stock_level_in_store",
      "cardinality": "N:1",
      "description": "The store a stock level's stock is kept in. Every level has exactly one (validator-enforced); the store's incoming edges are its stock list, one level per material.",
      "domain": "operations",
      "from": "hospitality.stock_level",
      "to": "hospitality.stock_store"
    },
    {
      "api_name": "hospitality.link.stock_movement_for_housekeeping_task",
      "cardinality": "N:1",
      "description": "The minibar or laundry task whose completion consumed or issued the stock (complete_housekeeping_task's `consumed`). A consumption or an issue may carry it (validator-enforced).",
      "domain": "operations",
      "from": "hospitality.stock_movement",
      "to": "hospitality.housekeeping_task"
    },
    {
      "api_name": "hospitality.link.stock_movement_for_level",
      "cardinality": "N:1",
      "description": "The level a movement moved. Every movement has exactly one (validator-enforced); the level's incoming edges in `recorded_at` order are the store's stock card for the material, and what recount_stock_level folds.",
      "domain": "operations",
      "from": "hospitality.stock_movement",
      "to": "hospitality.stock_level"
    },
    {
      "api_name": "hospitality.link.stock_movement_for_room",
      "cardinality": "N:1",
      "description": "The room the stock went to — the minibar that was drunk from, the room the laundry issued to. A consumption or an issue may carry it (validator-enforced).",
      "domain": "operations",
      "from": "hospitality.stock_movement",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.stock_movement_for_work_order",
      "cardinality": "N:1",
      "description": "The fault a consumption was used on — written when a work order's thread line names a catalogued material (record_work_order_entry). Only a consumption carries it (validator-enforced); an order's parts are its incoming edges.",
      "domain": "operations",
      "from": "hospitality.stock_movement",
      "to": "hospitality.work_order"
    },
    {
      "api_name": "hospitality.link.stock_movement_recorded_by",
      "cardinality": "N:1",
      "description": "The staff member who recorded a movement — resolved from the principal (a handset session's principal is the staff member), as a work-order thread line's author is. Absent when an agent, a service or a user with no staff record recorded it; the movement's `recorded_by` names the principal either way.",
      "domain": "operations",
      "from": "hospitality.stock_movement",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.stock_movement_transfer_from",
      "cardinality": "N:1",
      "description": "A transfer's receiving leg names its issuing leg — the two movements transfer_stock writes in one transaction. Present exactly on a receiving transfer leg, pointing at a transfer of the opposite quantity (validator-enforced).",
      "domain": "operations",
      "from": "hospitality.stock_movement",
      "to": "hospitality.stock_movement"
    },
    {
      "api_name": "hospitality.link.stock_store_at_property",
      "cardinality": "N:1",
      "description": "The house a stock store is at — the stores a house keeps are this edge's incoming side, read without a house-scoped search. Every store has exactly one (validator-enforced); define_stock_store writes it, and re-stating a store defined before the edge existed writes it then.",
      "domain": "operations",
      "from": "hospitality.stock_store",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.stock_store_in_room",
      "cardinality": "N:1",
      "description": "The room whose minibar a `room_minibar` store is — present exactly on that kind (validator-enforced), one per room (define_stock_store refuses a second). A minibar task in the room draws its consumption from it.",
      "domain": "operations",
      "from": "hospitality.stock_store",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.stored_object_for_asset",
      "cardinality": "N:1",
      "description": "The piece of equipment this stored object documents — its manual, its nameplate, a photo of where it stands. An asset accumulates many over its life.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.asset"
    },
    {
      "api_name": "hospitality.link.stored_object_for_checklist_result",
      "cardinality": "N:1",
      "description": "The checklist result this stored object is evidence for — the photo a checklist's `photo` item asks the attendant or the inspecting supervisor to take. A result accumulates many; recording the list again replaces the result in place, so its photos stay attached.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.checklist_result"
    },
    {
      "api_name": "hospitality.link.stored_object_for_checkpoint_result",
      "cardinality": "N:1",
      "description": "The checkpoint of an inspection round this stored object is evidence for — the corroded valve, the fire door's closer, the gauge read. A result accumulates many; recording the checkpoint again replaces the result in place, so its photos stay attached.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.checkpoint_result"
    },
    {
      "api_name": "hospitality.link.stored_object_for_guest_feedback",
      "cardinality": "N:1",
      "description": "The guest feedback this stored object is attached to — the photo of the stain, the scan of the comment card, hubOS's attached file on the feedback form. A feedback accumulates many.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.guest_feedback"
    },
    {
      "api_name": "hospitality.link.stored_object_for_housekeeping_task_entry",
      "cardinality": "N:1",
      "description": "The line on a housekeeping task's thread this stored object is a photo for — the stain the note is about, the room as the attendant left it. A line accumulates many.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.housekeeping_task_entry"
    },
    {
      "api_name": "hospitality.link.stored_object_for_invoice",
      "cardinality": "N:1",
      "description": "The invoice this stored object is the rendered PDF of. The one attachment host whose bytes the kernel writes rather than a person uploading: the document is sealed first, rendered from its own snapshot, then PUT through the same presigned path as every other attachment.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.invoice"
    },
    {
      "api_name": "hospitality.link.stored_object_for_lost_found_item",
      "cardinality": "N:1",
      "description": "The lost & found item this stored object is a photo of — what a claim is matched against. An item accumulates many.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.lost_found_item"
    },
    {
      "api_name": "hospitality.link.stored_object_for_person",
      "cardinality": "N:1",
      "description": "The person this stored object is an attachment of; a person accumulates many attachments (ID scans, correspondence) over its life.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.stored_object_for_property",
      "cardinality": "N:1",
      "description": "The property this stored object is an attachment of; a property accumulates many attachments (licences, floor plans) over its life.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.stored_object_for_reservation",
      "cardinality": "N:1",
      "description": "The reservation this stored object is an attachment of; a reservation accumulates many attachments (contracts, rooming lists) over its life.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.stored_object_for_room",
      "cardinality": "N:1",
      "description": "The room this stored object is an attachment of — a room-condition photo from the floor. A room accumulates many over its life.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.stored_object_for_work_order",
      "cardinality": "N:1",
      "description": "The work order this stored object is an attachment of — the photo-at-incident a fault report carries, the evidence a completion records. A work order accumulates many.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.work_order"
    },
    {
      "api_name": "hospitality.link.stored_object_for_work_order_entry",
      "cardinality": "N:1",
      "description": "The work-order thread line this stored object belongs to — the photo of the part replaced, the meter reading the engineer logged with the time. An entry accumulates many.",
      "domain": "stored_objects",
      "from": "hospitality.stored_object",
      "to": "hospitality.work_order_entry"
    },
    {
      "api_name": "hospitality.link.sync_run_for_connector",
      "cardinality": "N:1",
      "description": "Which connector produced this sync run; every run belongs to exactly one connector, and the connector accumulates its run history for the Explorer health view.",
      "domain": "connectors",
      "from": "hospitality.connector_sync_run",
      "to": "hospitality.connector"
    },
    {
      "api_name": "hospitality.link.tax_rule_at_property",
      "cardinality": "N:1",
      "description": "Which property a tax rule applies at. Property-level only — tax is jurisdictional, and a brand spans jurisdictions; a chain rolling out a common rule authors it per property.",
      "domain": "finance",
      "from": "hospitality.tax_rule",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.ticket_at_station",
      "cardinality": "N:1",
      "description": "The prep station a ticket fires at; optional — a ticket whose lines carry no station binding is the unrouted (expo) ticket.",
      "domain": "fnb",
      "from": "hospitality.kitchen_ticket",
      "to": "hospitality.prep_station"
    },
    {
      "api_name": "hospitality.link.ticket_for_check",
      "cardinality": "N:1",
      "description": "The check a kitchen ticket fulfils; firing a check's courses cuts many tickets, each belonging to one check.",
      "domain": "fnb",
      "from": "hospitality.kitchen_ticket",
      "to": "hospitality.guest_check"
    },
    {
      "api_name": "hospitality.link.treatment_room_at_venue",
      "cardinality": "N:1",
      "description": "Which venue (the spa) a treatment room belongs to; a spa venue holds many bookable rooms.",
      "domain": "spa",
      "from": "hospitality.treatment_room",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.treatment_room_supports_service",
      "cardinality": "N:M",
      "description": "Which services a room can host (Book4Time's facility-type requirement). Declared-optional: book_appointment enforces the map only when the room declares at least one supported service — a spa that never authors the map never hits the gate.",
      "domain": "spa",
      "from": "hospitality.treatment_room",
      "to": "hospitality.spa_service"
    },
    {
      "api_name": "hospitality.link.turnaway_for_guest",
      "cardinality": "N:1",
      "description": "Who asked, when the desk knew. A repeat guest turned away is a different loss from an anonymous enquiry, and the recognition surface reads this edge.",
      "domain": "booking",
      "from": "hospitality.turnaway",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.turnaway_for_property",
      "cardinality": "N:1",
      "description": "The house that refused the request. The one link a turnaway cannot be recorded without — lost demand belongs to a property's nights.",
      "domain": "booking",
      "from": "hospitality.turnaway",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.turnaway_for_rate_plan",
      "cardinality": "N:1",
      "description": "The plan the guest was quoted on, when a rate was in play — a regret is a price declined on a plan, and the plan says which rung it was. Absent for a request refused before a rate was offered.",
      "domain": "booking",
      "from": "hospitality.turnaway",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.turnaway_for_room_type",
      "cardinality": "N:1",
      "description": "What was asked for, when the guest asked for a type. Absent for a request the desk refused before a type was in play — a full house or a minimum stay refuses the night, not the room.",
      "domain": "booking",
      "from": "hospitality.turnaway",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.utility_entry_at_property",
      "cardinality": "N:1",
      "description": "Which property a Schedule 9 energy/water/waste entry posts against; the property accumulates the utility ledger that feeds its daily EWW rollup.",
      "domain": "eww",
      "from": "hospitality.utility_ledger_entry",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.variant_of_menu_item",
      "cardinality": "N:1",
      "description": "The item a variant is a size or measure of; every item has at least one variant.",
      "domain": "fnb",
      "from": "hospitality.menu_item_variant",
      "to": "hospitality.menu_item"
    },
    {
      "api_name": "hospitality.link.venue_brand_has_design_dna",
      "cardinality": "N:1",
      "description": "The design language the venue brand carries; at most one DNA per venue brand, while a property may layer several.",
      "domain": "brand_venue",
      "from": "hospitality.venue_brand",
      "to": "hospitality.design_dna"
    },
    {
      "api_name": "hospitality.link.venue_brand_has_parent_brand",
      "cardinality": "N:1",
      "description": "The group or property brand the venue brand rolls up to (Cecconi's under Soho House Group); optional because an independent licensed venue brand carries no parent, and temporal so acquisitions and divestments transition cleanly.",
      "domain": "brand_venue",
      "from": "hospitality.venue_brand",
      "to": "hospitality.brand"
    },
    {
      "api_name": "hospitality.link.venue_brand_originated_at_property",
      "cardinality": "N:1",
      "description": "The property where the venue brand was founded — a historical-attribution fact, distinct from the venues where the brand currently operates; optional.",
      "domain": "brand_venue",
      "from": "hospitality.venue_brand",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.venue_brand_owned_by_company",
      "cardinality": "N:1",
      "description": "The legal entity that owns the brand itself when it differs from the tenant (Hakkasan owned by Tao Group Hospitality); records brand ownership, not venue operation — that concern is venue_has_operator.",
      "domain": "brand_venue",
      "from": "hospitality.venue_brand",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.venue_has_brand",
      "cardinality": "N:M",
      "description": "A brand identity the venue trades under; many-to-many because one venue may carry concurrent concepts (day cafe by day, bar brand by night) and one venue brand operates at many venues.",
      "domain": "organization",
      "from": "hospitality.venue",
      "to": "hospitality.venue_brand"
    },
    {
      "api_name": "hospitality.link.venue_has_operator",
      "cardinality": "N:1",
      "description": "The company that operates a venue under a white-label F&B contract; at most one at a time, temporal for contract turnover — distinct from brand and from property ownership.",
      "domain": "organization",
      "from": "hospitality.venue",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.venue_has_register",
      "cardinality": "1:1",
      "description": "The one service register a venue overrides its axes with; venues without a register inherit the property philosophy's *_default axes implicitly.",
      "domain": "organization",
      "from": "hospitality.venue",
      "to": "hospitality.service_register"
    },
    {
      "api_name": "hospitality.link.venue_has_service_item",
      "cardinality": "1:N",
      "description": "Which venue sells this catalog item; the edge that lets revenue be analysed by venue and category.",
      "domain": "organization",
      "from": "hospitality.venue",
      "to": "hospitality.service_item"
    },
    {
      "api_name": "hospitality.link.venue_residency",
      "cardinality": "N:M",
      "description": "A company running a residency at a venue (a chef pop-up, a partner-brand activation); many-to-many and temporal, the bounds carrying the contract dates.",
      "domain": "organization",
      "from": "hospitality.venue",
      "to": "hospitality.company"
    },
    {
      "api_name": "hospitality.link.venue_visit_at_venue",
      "cardinality": "N:1",
      "description": "The venue a visit happened at; a venue accumulates its visit history.",
      "domain": "brand_venue",
      "from": "hospitality.venue_visit",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.venue_visit_for_modality",
      "cardinality": "N:1",
      "description": "Which modality a venue visit was a session of; book_modality_session establishes the link, closing the structural loop \"what was booked\" that the visit object alone doesn't carry.",
      "domain": "brand_venue",
      "from": "hospitality.venue_visit",
      "to": "hospitality.wellness_modality"
    },
    {
      "api_name": "hospitality.link.venue_visit_for_person",
      "cardinality": "N:1",
      "description": "The guest a venue visit records; one person per visit, so a multi-person visit creates N rows (companions held in observed_companions_text).",
      "domain": "brand_venue",
      "from": "hospitality.venue_visit",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.venue_visit_references_external_record",
      "cardinality": "N:1",
      "description": "The clinical seam for bookings (proposal 0005): mandatory whenever the visited modality has requires_medical_licensing = true — the booking handler refuses to persist the session without it, and the venue_visit cross-link validator catches bypass-path graph writes arriving without the reference.",
      "domain": "brand_venue",
      "from": "hospitality.venue_visit",
      "to": "hospitality.external_record_reference"
    },
    {
      "api_name": "hospitality.link.waitlist_entry_became_reservation",
      "cardinality": "1:1",
      "description": "The reservation an accepted entry turned into — the audit trail from turnaway to recovered booking, and what makes \"how much revenue did the waitlist save us\" answerable. Written once by accept_waitlist_entry and never re-pointed.",
      "domain": "booking",
      "from": "hospitality.waitlist_entry",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.waitlist_entry_for_guest",
      "cardinality": "N:1",
      "description": "Who is waiting. Many entries per person is deliberate: a guest chasing a sold-out weekend may wait on two date options at once, and collapsing those into one entry would lose which one they actually wanted.",
      "domain": "booking",
      "from": "hospitality.waitlist_entry",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.waitlist_entry_for_property",
      "cardinality": "N:1",
      "description": "The house the guest is waiting on. The night audit's expiry sweep walks this edge, so it is the one link an entry cannot be created without — an entry hanging off no property would never age out.",
      "domain": "booking",
      "from": "hospitality.waitlist_entry",
      "to": "hospitality.property"
    },
    {
      "api_name": "hospitality.link.waitlist_entry_for_rate_plan",
      "cardinality": "N:1",
      "description": "The plan the request was quoted against. Carried because the plan decides the restrictions accept re-checks; a waitlisted request whose plan has since closed out refuses at accept rather than booking on a plan nobody offered.",
      "domain": "booking",
      "from": "hospitality.waitlist_entry",
      "to": "hospitality.rate_plan"
    },
    {
      "api_name": "hospitality.link.waitlist_entry_for_room_type",
      "cardinality": "N:1",
      "description": "What was asked for. Read-only demand signal while the entry waits — it is never counted against the type's availability — and the type accept_waitlist_entry replays the create against.",
      "domain": "booking",
      "from": "hospitality.waitlist_entry",
      "to": "hospitality.room_type"
    },
    {
      "api_name": "hospitality.link.wellness_equipment_at_venue",
      "cardinality": "N:1",
      "description": "Which venue a piece of wellness equipment sits in; equipment is venue-scoped at any given time even when portable, with the temporal bounds carrying the venue-assignment lifetime.",
      "domain": "membership",
      "from": "hospitality.wellness_equipment",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.wellness_modality_at_venue",
      "cardinality": "N:M",
      "description": "Where a wellness modality is offered; one modality can run at several venues across a property's wellness floors, with the temporal bounds carrying seasonal offerings such as a summer cryotherapy pop-up.",
      "domain": "membership",
      "from": "hospitality.wellness_modality",
      "to": "hospitality.venue"
    },
    {
      "api_name": "hospitality.link.wellness_protocol_for_membership",
      "cardinality": "N:1",
      "description": "Whose protocol this is: a wellness protocol belongs to one individual membership, keeping a member's protocol distinct from their spouse's on the same account.",
      "domain": "membership",
      "from": "hospitality.wellness_protocol",
      "to": "hospitality.membership"
    },
    {
      "api_name": "hospitality.link.work_order_answers_feedback",
      "cardinality": "N:1",
      "description": "The complaint a work order answers — hubOS's `related_complaint` on the fault. Written by `raise_work_order_from_feedback`; a complaint may need more than one order.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.guest_feedback"
    },
    {
      "api_name": "hospitality.link.work_order_assigned_to",
      "cardinality": "N:1",
      "description": "The engineer the work order is currently assigned to; assign_work_order re-points the edge on reassignment and an unassigned order simply lacks it.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.work_order_assigned_to_group",
      "cardinality": "N:1",
      "description": "The worker group a work order is assigned to, instead of one engineer — the night crew's queue. The member who starts it takes it (the edge re-points to `work_order_assigned_to`); a staff member outside the group may not start it. An order is assigned to a person or a group, never both (held at commit).",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.worker_group"
    },
    {
      "api_name": "hospitality.link.work_order_entry_for_work_order",
      "cardinality": "N:1",
      "description": "The work order whose thread a line is on. Every entry has exactly one (validator-enforced); the thread is the order's incoming edges in `recorded_at` order.",
      "domain": "operations",
      "from": "hospitality.work_order_entry",
      "to": "hospitality.work_order"
    },
    {
      "api_name": "hospitality.link.work_order_entry_recorded_by",
      "cardinality": "N:1",
      "description": "The staff member who wrote a thread line — resolved from the principal (a handset session's principal is the staff member), exactly as starting a task self-assigns. Absent when an agent, a service or a user with no staff record wrote it; the entry's `recorded_by` names the principal either way.",
      "domain": "operations",
      "from": "hospitality.work_order_entry",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.work_order_for_area",
      "cardinality": "N:1",
      "description": "The public area a fault is in — the lobby's blown light, the lift that sticks. The alternative to `work_order_for_room`: an order is for a room or an area, never both (held at commit), and carries the area's id as `area_id`. Written by `create_public_area_work_order` and by a round checkpoint's `raise_fault.area_id`.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.public_area"
    },
    {
      "api_name": "hospitality.link.work_order_for_asset",
      "cardinality": "N:1",
      "description": "The equipment a work order is for, beside the room it was raised against — the room is where the fault shows, the asset is what failed, and they need not be the same place (no hot water in 101, the boiler in the plant room). The asset's incoming edges are its service history.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.asset"
    },
    {
      "api_name": "hospitality.link.work_order_for_room",
      "cardinality": "N:1",
      "description": "The room a maintenance work order is raised against; whether the room is out of order while the order stays open is the work_order_holds_block edge, never a flag.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.work_order_from_checkpoint",
      "cardinality": "N:1",
      "description": "The checkpoint a work order was raised from (hubOS's 'checkpoint report fault'); the result's `faults` counts these. The round is one edge further, and on the order's own `work_order_from_round`.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.checkpoint_result"
    },
    {
      "api_name": "hospitality.link.work_order_from_maintenance_plan",
      "cardinality": "N:1",
      "description": "The plan that raised a work order. The order's own `maintenance_plan_id` carries the same id on its image, so the automation that moves the plan on when the order closes can select a plan's orders without walking this edge; the edge is what the plan's history reads.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.maintenance_plan"
    },
    {
      "api_name": "hospitality.link.work_order_from_round",
      "cardinality": "N:1",
      "description": "The round that raised a work order — hubOS's `revision` on a fault raised from a checkpoint. The round's `total_faults` counts these.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.inspection_round"
    },
    {
      "api_name": "hospitality.link.work_order_from_triage",
      "cardinality": "N:1",
      "description": "The triaged guest request this order was raised from — the reported fault, as the guest phrased it. Absent on an order raised by an engineer's own inspection.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.message_triage"
    },
    {
      "api_name": "hospitality.link.work_order_holds_block",
      "cardinality": "1:1",
      "description": "The out-of-order block a work order holds while the room is physically unusable — a real room_status_block minted through the same code path as take_room_out_of_order, so ledger weight, assignment guards, and the night audit's through-date sweep all see maintenance downtime identically. complete_work_order releases it through the block's own release path.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.room_status_block"
    },
    {
      "api_name": "hospitality.link.work_order_of_fault_type",
      "cardinality": "N:1",
      "description": "The catalogue entry a work order was raised from (`create_work_order.fault_type_id`). The order carries the same id as `fault_type_id` (validator-held); the fault type's incoming edges are every time it happened.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.fault_type"
    },
    {
      "api_name": "hospitality.link.work_order_reported_by_stay",
      "cardinality": "N:1",
      "description": "The guest stay that reported the fault. The edge carries no name: the floor reads `reported_by_guest` on the order, and the guest behind the stay stays behind the desk's clearance.",
      "domain": "operations",
      "from": "hospitality.work_order",
      "to": "hospitality.stay"
    },
    {
      "api_name": "hospitality.link.worker_group_has_member",
      "cardinality": "N:M",
      "description": "A member of a worker group — hubOS's workers group is a named list of user ids. A staff member may sit in several groups (the third-floor runners and the late shift). Set as a whole by `set_worker_group_members`.",
      "domain": "operations",
      "from": "hospitality.worker_group",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.workflow_run_for_definition",
      "cardinality": "N:1",
      "description": "Pins the run to the exact definition version it executes (frozen at start via definition_hash); a definition spawns many runs and each run executes exactly one definition.",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.workflow_definition"
    },
    {
      "api_name": "hospitality.link.workflow_run_started_by_agent_installation",
      "cardinality": "N:1",
      "description": "The installed agent that initiated this run; optional, set when an installation rather than a human or schedule started it.",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.agent_installation"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_charge",
      "cardinality": "N:M",
      "description": "The charges this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.charge"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_folio",
      "cardinality": "N:M",
      "description": "The folios this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.folio"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_fraud_risk_assessment",
      "cardinality": "N:M",
      "description": "The fraud risk assessments this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.fraud_risk_assessment"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_membership",
      "cardinality": "N:M",
      "description": "The memberships this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.membership"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_message_triage",
      "cardinality": "N:M",
      "description": "The message triages this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.message_triage"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_payment",
      "cardinality": "N:M",
      "description": "The payments this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.payment"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_person",
      "cardinality": "N:M",
      "description": "The persons this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.person"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_rate_recommendation",
      "cardinality": "N:M",
      "description": "The rate recommendations this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.rate_recommendation"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_reservation",
      "cardinality": "N:M",
      "description": "The reservations this workflow run acts on — one of the twelve concrete v1 target links standing in for a polymorphic target edge (link endpoints must be concrete types, proposal 0020); graph traversal goes through this link, not the run's redacted target_refs_snapshot.",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.reservation"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_restriction_recommendation",
      "cardinality": "N:M",
      "description": "The restriction recommendations this workflow run acts on; the thirteenth concrete target link, added with the restriction recommendation (2026-09-22).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.restriction_recommendation"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_room",
      "cardinality": "N:M",
      "description": "The rooms this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.room"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_service_recovery_event",
      "cardinality": "N:M",
      "description": "The service recovery events this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.service_recovery_event"
    },
    {
      "api_name": "hospitality.link.workflow_run_targets_staff_member",
      "cardinality": "N:M",
      "description": "The staff members this workflow run acts on; one of the twelve concrete target links that stand in for the run's polymorphic target set (proposal 0020).",
      "domain": "workflows",
      "from": "hospitality.workflow_run",
      "to": "hospitality.staff_member"
    },
    {
      "api_name": "hospitality.link.workflow_step_run_compensates_step_run",
      "cardinality": "N:1",
      "description": "Points a compensating step at the original step it undoes; compensation attempts stack on the original over time (a failed compensation can spawn another) without rewriting it.",
      "domain": "workflows",
      "from": "hospitality.workflow_step_run",
      "to": "hospitality.workflow_step_run"
    },
    {
      "api_name": "hospitality.link.workflow_step_run_for_workflow_run",
      "cardinality": "N:1",
      "description": "Which run owns this step run; a run decomposes into many step runs and each step run belongs to exactly one run.",
      "domain": "workflows",
      "from": "hospitality.workflow_step_run",
      "to": "hospitality.workflow_run"
    },
    {
      "api_name": "hospitality.link.workflow_step_run_used_agent_installation",
      "cardinality": "N:1",
      "description": "The installed agent that executed, proposed, or resolved this step; optional, and with Principal::Agent on the event answers \"was this agent enabled and allowed to take this step then?\".",
      "domain": "workflows",
      "from": "hospitality.workflow_step_run",
      "to": "hospitality.agent_installation"
    }
  ],
  "object_types": [
    {
      "actions": [
        {
          "api_name": "hospitality.action.revoke_access_credential",
          "description": "Kill a credential: status → `revoked`, the token is REMOVED (the tap lookup only sees token-bearing rows, so a revoked card fails closed at the reader and its UID becomes free for re-issue), `token_hint` stays behind for audit. Idempotent on an already-revoked credential. Check-out does this automatically for every credential on the stay; this action is the manual path — lost card, guest request, fraud stop. Emits `access.credential_revoked.recorded`."
        }
      ],
      "api_name": "hospitality.access_credential",
      "description": "A per-stay physical or virtual credential — the room key card, wristband, or mobile key — bound to a stay so the opaque vendor token a reader picks up at tap time resolves to the stay's open folio. The token is a locator, never an authenticator (card UIDs are clonable): resolution requires `status = active`, an unexpired `expires_at`, and an in-house stay, and every posting is bounded by the credential's `per_charge_cap` / `daily_cap`. Check-out revokes every credential on the stay — a dropped card in the express-checkout box must fail closed.",
      "display_name": "Access credential",
      "domain": "access",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Credential id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Credential form",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "token",
          "description": "",
          "display_name": "Opaque vendor token (card UID — present only while active)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "token_hint",
          "description": "",
          "display_name": "Display fragment of the token (last four) for staff UIs and audit",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the credential was issued",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "Hard expiry — resolution fails closed past this instant",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "revoked_at",
          "description": "",
          "display_name": "When the credential was revoked",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "per_charge_cap",
          "description": "",
          "display_name": "Maximum amount a single tap may post",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "daily_cap",
          "description": "",
          "display_name": "Maximum total the credential may post per property-local business day",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_access_purpose",
          "description": "Retire an `access_purpose`: no new grants, no enforcement gating from it, audit trail kept. Existing active grants keep running until they expire or are revoked — retirement changes what can be granted, not what was. Idempotent on a retired purpose. Emits `access_purpose.retired`."
        }
      ],
      "api_name": "hospitality.access_purpose",
      "description": "An operator-defined purpose for accessing marked data — the PBAC vocabulary entry (e.g. `chargeback investigation`). Names the marking compartments it can unlock (`covered_markings`) and bounds how long a grant of it may live. A purpose is a definition, not a permission: nothing is conferred until a `purpose_grant` is approved. While a tenant has an `active` purpose covering a marking and the deployment enforces PBAC, bearer clearances for that marking require an active grant.",
      "display_name": "Access purpose",
      "domain": "pbac",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Purpose id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "display_name",
          "description": "",
          "display_name": "Operator-facing name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "justification",
          "description": "",
          "display_name": "Why this purpose exists — the standing business/legal ground grants under it inherit (distinct from each grant's own rationale)",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "covered_markings",
          "description": "",
          "display_name": "Marking api-names this purpose can unlock (JSON array, e.g. [\"pii.guest\"]); validated against the registered marking catalogue",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "max_grant_hours",
          "description": "",
          "display_name": "Ceiling on a grant's lifetime under this purpose; a `request_purpose_grant` asking for longer is rejected",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_agent_eval_case",
          "description": "Take a promoted eval case out of the agent's effective suite (`status = retired`) — the off-ramp for a mistaken promotion or a case whose subject went stale. Retired, not deleted: the provenance trail back to the source trace survives, and retiring an already-retired case is an idempotent no-op. Emits `agent_eval_case.retired`."
        }
      ],
      "api_name": "hospitality.agent_eval_case",
      "description": "One agent-run eval case, promoted from a production session trace (`promote_agent_trace_to_eval_case`). Carries the task and a graders array in the runtime's agent-run suite grammar (`run.completes` / `run.proposes` / `run.never_calls` / `final.mentions`); the runtime merges `active` registry cases with file-based suites at eval time (`retire_agent_eval_case` is the off-ramp). `source_session_id` is the provenance pointer back to the trace the case was minted from.",
      "display_name": "Agent eval case",
      "domain": "agents",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Eval case id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agent_key",
          "description": "",
          "display_name": "The agent definition this case evaluates",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "case_name",
          "description": "",
          "display_name": "Suite-unique case name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "task",
          "description": "",
          "display_name": "The task handed to the agent",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "graders",
          "description": "",
          "display_name": "Graders array in the agent-run suite grammar (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "source_session_id",
          "description": "",
          "display_name": "The session trace this case was promoted from",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "promoted_by",
          "description": "",
          "display_name": "`principal.stable_id()` of the promoter",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "promoted_at",
          "description": "",
          "display_name": "When the case was promoted",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle: `active` joins the effective suite; `retired` is the off-ramp for a mistaken promotion or a stale subject (retire_agent_eval_case)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the case was retired",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "retired_by",
          "description": "",
          "display_name": "`principal.stable_id()` of the retirer",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "retired_reason",
          "description": "",
          "display_name": "Why the case was retired",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.approve_agent_installation",
          "description": "Activate a previously-requested installation (`requested → enabled`) — the approval half of the marketplace flow opened by `request_agent_installation`. Re-checks what can have moved since the request: the registry entry's installability and the requested scopes against the entry's current declarations. Stamps `enabled_by` / `enabled_at` and emits `agent_installation.enabled`. Denial goes through `suspend_agent_installation(new_status = revoked)`."
        },
        {
          "api_name": "hospitality.action.configure_agent_installation_dispatch",
          "description": "Publish (or rotate) an installation's proposal 0022 dispatch surface: the receiver URL the dispatcher POSTs signed task envelopes to, the runtime's §8.3 identity URI, and its kid → base64 Ed25519 verification keys. Key rotation is additive — send old + new kid during the window. Endpoint URL follows the §6.1 rule (https, or http for loopback receivers only); key material must be public verification keys (RFC 8032 raw 32-byte, base64). Rejected on terminal (revoked/expired) installations. Emits `agent_installation.dispatch_configured`."
        },
        {
          "api_name": "hospitality.action.record_agent_installation_use",
          "description": "Stamp `last_used_at` on an installation when its bound OAuth client mints a token. Emitted at most once per staleness window (the token endpoint throttles), so the property answers \"is this installation still alive\" for governance review, not \"how often does it run\"."
        },
        {
          "api_name": "hospitality.action.suspend_agent_installation",
          "description": "Suspend, revoke, disable, or expire an installation. Does not mutate historical events or completed step runs; future workflow steps that reference the installation are blocked by `agent_installation_enabled_before_use`."
        }
      ],
      "api_name": "hospitality.agent_installation",
      "description": "Tenant / property enablement of a registry entry. Permission-bearing — `enabled` installations are what `enable_agent_installation` issues per-tenant. `enabled_action_scopes` and `enabled_function_scopes` are subsets of the registry entry's declared maxima. `installed_scope_ref` is required and type-checked for non-tenant scopes; the broad catch-all scopes (`actions:apply`, `functions:invoke`) and any `internal:workflow:*` scope are rejected at install time.",
      "display_name": "Agent installation",
      "domain": "agents",
      "interfaces": [
        "hospitality.interface.expirable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Installation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "installation_status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "installed_scope",
          "description": "",
          "display_name": "Tenancy scope this installation grants access at",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "installed_scope_ref",
          "description": "",
          "display_name": "Typed reference to the scoping object. Required for non-tenant scopes; forbidden for `installed_scope = tenant`. Type-checked by `agent_installation_scope_ref_coherent`.",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "enabled_action_scopes",
          "description": "",
          "display_name": "Subset of the registry entry's declared action scopes",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "enabled_function_scopes",
          "description": "",
          "display_name": "Subset of the registry entry's declared function scopes",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "enabled_read_scopes",
          "description": "",
          "display_name": "Subset of the registry entry's declared read scopes. When present it is the installation's whole object-read grant: a token minted for it carries `objects:<type>:read` for exactly these types and never the broad `objects:read`. Absent, reads are not narrowed by the installation.",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "enabled_data_classes",
          "description": "",
          "display_name": "Subset of the registry entry's declared data classes",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "credential_reference",
          "description": "",
          "display_name": "Opaque pointer into an external secret manager. `credential_reference_no_secret_material` rejects obvious secret bodies (bearer tokens, API keys, PEM blocks).",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "auth_mode",
          "description": "",
          "display_name": "How the installation authenticates the agent at call time",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "installed_by",
          "description": "",
          "display_name": "Principal stable id of the installer",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "enabled_by",
          "description": "",
          "display_name": "Principal stable id that approved enablement",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "enabled_at",
          "description": "",
          "display_name": "Enablement time",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "suspended_at",
          "description": "",
          "display_name": "Suspension time",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "suspension_reason",
          "description": "",
          "display_name": "Required when suspended / revoked",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "Optional time-bound install",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "last_used_at",
          "description": "",
          "display_name": "Updated by agent-run attribution",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "approval_policy",
          "description": "",
          "display_name": "Gates required before this agent can execute high-risk steps. Required when any enabled action / function / allowed workflow has a risk_tier in the high-risk set (per `agent_installation_high_risk_requires_policy`).",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "allowed_workflow_keys",
          "description": "",
          "display_name": "Optional workflow-key allowlist; empty/null means action/function scopes decide",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "dispatch_endpoint_url",
          "description": "",
          "display_name": "Runtime-published URL the proposal 0022 dispatcher POSTs signed task envelopes to (§6.1 rules: https required, http admissible only for loopback receivers). Unset = this installation is not dispatch-taskable.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "runner_identity_ref",
          "description": "",
          "display_name": "Stable §8.3 identity URI the runtime presents in its signed acks/results (SPIFFE URI, JWK URL, or urn:aerandil form).",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "runner_public_keys",
          "description": "",
          "display_name": "kid → base64 Ed25519 verification keys (RFC 8032 raw 32-byte) the receiver routes verify this installation's ack / result_submit envelope signatures against. Public key material only — rotation is additive (keep old + new kid during the window).",
          "kind": "json",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.block_agent_registry_entry",
          "description": "Security kill switch: mark a registry entry `blocked` AND disable every live installation of it in the same transaction (status `disabled`, the system-driven not-running state) — the VS Code Marketplace block precedent, where a malicious extension is force-disabled on existing installs, not just delisted. Emits `agent_registry_entry.blocked` plus one `agent_installation.disabled` per affected installation."
        },
        {
          "api_name": "hospitality.action.deprecate_agent_registry_entry",
          "description": "Mark a registry entry `deprecated` — new installations are refused (`agent_registry_approved_before_install`), but existing installations keep running so operators migrate on their own schedule (npm-deprecate semantics: a warning, not a kill switch — that is `block_agent_registry_entry`). Emits `agent_registry_entry.deprecated`."
        },
        {
          "api_name": "hospitality.action.enable_agent_installation",
          "description": "Enable a reviewed registry entry for a tenant / property scope. Validators (commit 11): `agent_registry_approved_before_install`, `agent_installation_scopes_subset_declared`, `agent_installation_no_broad_catch_all_scope`, `agent_installation_no_internal_workflow_scope`, `agent_installation_scope_ref_coherent`, `agent_installation_unique_per_scope_entry`, `agent_installation_scope_within_tenancy`, `agent_installation_high_risk_requires_policy`, `credential_reference_no_secret_material`."
        },
        {
          "api_name": "hospitality.action.request_agent_installation",
          "description": "Propose an installation of a reviewed registry entry (marketplace-style request-then-approve, proposal 0020 §9.1). Runs the full enable-time validation pipeline but lands the installation at `installation_status = requested` with no activation stamps; an operator then activates it via `enable_agent_installation` with `installation_id`, or denies it via `suspend_agent_installation(new_status = revoked)`. Emits `agent_installation.requested`."
        },
        {
          "api_name": "hospitality.action.review_agent_registry_entry",
          "description": "Record the curator's review verdict on a `submitted` registry entry: `approved` makes it installable (`agent_registry_approved_before_install`); `rejected` refuses it. Stamps `reviewed_by` / `reviewed_at` and emits `agent_registry_entry.reviewed`. Re-reviewing an entry already in the same verdict is a no-op; deprecation and blocking are their own actions with different installation semantics."
        }
      ],
      "api_name": "hospitality.agent_registry_entry",
      "description": "Catalog metadata for a first-party or third-party agent, MCP server, or agent skill. Declares maximum action / function / data-class scopes the agent claims to need plus vendor / model / safety metadata. **A registry entry is not permission to run** — installation enables the agent for a specific tenant / property scope (see `agent_installation`). The MCP Registry's `server.json` is the precedent for storing metadata rather than artifacts; AWS Agent Registry's curation + approval workflow is the precedent for the `review_status` gate.",
      "display_name": "Agent registry entry",
      "domain": "agents",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Registry entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agent_key",
          "description": "",
          "display_name": "Stable registry key (e.g. `io.github.vendor/recovery-agent`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "publisher",
          "description": "",
          "display_name": "Vendor / publisher namespace",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "version",
          "description": "",
          "display_name": "Agent package / server / skill version",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "title",
          "description": "",
          "display_name": "Display name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Bounded to 2048 characters",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "registry_source",
          "description": "",
          "display_name": "Origin of this registry entry",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "registry_record_uri",
          "description": "",
          "display_name": "Source registry URL / ARN / id",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "protocols",
          "description": "",
          "display_name": "Protocols the agent supports",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "mcp_server_uri",
          "description": "",
          "display_name": "Canonical remote MCP endpoint when applicable; HTTPS-only enforced by `mcp_remote_requires_https`",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "package_references",
          "description": "",
          "display_name": "Package or artifact references using the source registry's shape",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "declared_action_scopes",
          "description": "",
          "display_name": "Maximum action scopes the agent claims it may need",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "declared_function_scopes",
          "description": "",
          "display_name": "Maximum function scopes the agent claims it may need",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "declared_read_scopes",
          "description": "",
          "display_name": "Object types the agent reads, each `objects:<object type>:read` naming a registered type. The ceiling an installation's `enabled_read_scopes` is held to; empty or absent declares no narrowed read set.",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "declared_data_classes",
          "description": "",
          "display_name": "Data classes the agent processes",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "declared_risk_tiers",
          "description": "",
          "display_name": "Risk tiers the agent claims it may take steps under",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "model_provider",
          "description": "",
          "display_name": "Model provider when known (e.g. `anthropic`, `openai`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "model_family",
          "description": "",
          "display_name": "Model family when known (e.g. `claude-opus`, `gpt-5`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "safety_profile",
          "description": "",
          "display_name": "Prompt-injection, data handling, human-review, evaluation metadata",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "privacy_url",
          "description": "",
          "display_name": "Publisher privacy / DPA documentation",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "security_url",
          "description": "",
          "display_name": "Security review / SOC2 / vuln disclosure docs",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "review_status",
          "description": "",
          "display_name": "Curator review status; only `approved` is installable",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reviewed_by",
          "description": "",
          "display_name": "Principal stable id of the reviewer",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "reviewed_at",
          "description": "",
          "display_name": "Review timestamp",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "published_at",
          "description": "",
          "display_name": "Source publish time",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "definition_document",
          "description": "",
          "display_name": "The runtime-interpreted agent definition (design D3; runtime owns the grammar)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "obo_scopes",
          "description": "",
          "display_name": "Execute scopes exercisable on behalf of an invoking user (K5 ceiling)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "grounding_scope_ref",
          "description": "",
          "display_name": "scope_key of the `grounding_scope` injected into the retrieval lane (D11)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.append_agent_thread_message",
          "description": "Append one turn to an open `agent_thread` — the operator's task (`role = user`) or the agent's final reply (`role = agent`, correlated to its session trace via `session_id`). Appends into a closed thread are rejected; `parent_message_id` makes the thread branchable (the Snowflake parent-pointer pattern) and must name a message in the same thread. Refreshes the thread's `last_message_at` / `message_count` projections."
        },
        {
          "api_name": "hospitality.action.close_agent_thread",
          "description": "Close an open `agent_thread`. Terminal for appends — reopening is a deliberate future action, not a status flip. Closing an already-closed thread is rejected. Emits `agent_thread.closed`."
        }
      ],
      "api_name": "hospitality.agent_thread",
      "description": "A persistent operator↔agent conversation. Threads are server-side ontology objects (agent-stack design D9): the Explorer agent panel and the agent runtime both read and append through actions, so no client holds private history. `agent_key` names the agent definition the thread converses with; message turns are `agent_thread_message` rows reached via `agent_thread_message_in_thread`. Appends are rejected once `thread_status` is `closed`.",
      "display_name": "Agent thread",
      "domain": "agents",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Thread id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agent_key",
          "description": "",
          "display_name": "The agent definition this thread converses with",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "subject",
          "description": "",
          "display_name": "Display subject (defaults to the opening task's first line)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "thread_status",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "opened_by",
          "description": "",
          "display_name": "`principal.stable_id()` of the opener",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "When the thread opened",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_message_at",
          "description": "",
          "display_name": "Most recent turn timestamp (denormalised; refreshed by append)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "message_count",
          "description": "",
          "display_name": "Count of turns in this thread (denormalised; refreshed by append)",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.agent_thread_message",
      "description": "One turn in an `agent_thread` — a `user` task or the agent's final reply. The full step sequence behind an `agent` turn (tool calls, usage, deltas) is the trace store's business, reached via `session_id`; the thread keeps only what a conversation replay needs. `parent_message_ref` makes threads branchable (the Snowflake parent-pointer pattern). Immutable once appended.",
      "display_name": "Agent thread message",
      "domain": "agents",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Message id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "thread_ref",
          "description": "",
          "display_name": "The containing thread",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "role",
          "description": "",
          "display_name": "Who authored this turn",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "content",
          "description": "",
          "display_name": "The turn's text",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "parent_message_ref",
          "description": "",
          "display_name": "Branch parent (omitted = continues the latest turn)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "session_id",
          "description": "",
          "display_name": "The runtime session (trace) that produced an `agent` turn",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "authored_by",
          "description": "",
          "display_name": "`principal.stable_id()` of the author",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "authored_at",
          "description": "",
          "display_name": "When the turn was appended",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.allergy_alert",
      "description": "Person-attached structured record of a single food allergy: which EU 1169/2011-listed allergen (matching `recipe.allergens` so menu↔guest joins are trivial), how severe the documented reaction, how confident the diagnosis (self-reported → oral food challenge), and what the kitchen / front-of-house protocol should be. One alert per (person, allergen); persons with multiple allergies carry multiple alert rows. Pairs with `recipe.allergens` / `recipe_ingredient.allergen_flags` on the producer side and with `external_record_reference` on the clinical-record seam (Aerandil never carries the underlying diagnostic payload — see `external_records.rs`). The free-text `dietary` field on `person.preferences` is consumer-side lifestyle (vegan, kosher); allergen alerts live here so the medical surface stays structured.",
      "display_name": "Allergy alert",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Alert id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "allergen",
          "description": "",
          "display_name": "Allergen (EU 1169/2011 Annex II vocabulary)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "severity",
          "description": "",
          "display_name": "Severity (operator-facing five-rung ladder)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "clinical_status",
          "description": "",
          "display_name": "Diagnosis provenance",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cross_contact_sensitive",
          "description": "",
          "display_name": "Kitchen must use separate equipment / surfaces",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "airborne_sensitive",
          "description": "",
          "display_name": "Reaction possible from cooking vapour / particulate",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "epi_pen_required",
          "description": "",
          "display_name": "Epinephrine on-person required (paired with severity ∈ {severe, life_threatening})",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "emergency_action_plan_text",
          "description": "",
          "display_name": "Operator-readable action plan (free text — clinical plan stays in `clinical_record_ref`)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "diagnosed_on",
          "description": "",
          "display_name": "Diagnosis date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "last_verified_on",
          "description": "",
          "display_name": "When the operator last re-confirmed the alert with the person",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "expires_on",
          "description": "",
          "display_name": "When the alert becomes inactive (childhood allergies can be outgrown; null = no expiry)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Operator notes (front-of-house phrasing, prior-incident references)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.begin_appointment_service",
          "description": "The treatment began: checked_in → in_service (Book4Time's In-Service block; Zenoti's progress = Start). The actual start instant feeds treatment-room utilisation reads against the booked window. Business time: `service_started_at` defaults to now. Emits `spa.appointment_in_service`."
        },
        {
          "api_name": "hospitality.action.cancel_appointment",
          "description": "Cancel a not-yet-started appointment, typed at cancel time (Mangomint's normal / late split plus operator cancellations): `guest` is an in-policy guest cancellation, `late` is inside the property's late-cancel window, `operator` is a property-side cancellation (therapist sick, room out of service). Fee posting is deliberately not automated (Book4Time posture — deposits and discretionary charging); a late-cancel fee is an operator post_charge. Business time: `cancelled_at` defaults to now. Cancelling a cancelled appointment is a no-op. Emits `spa.appointment_cancelled`."
        },
        {
          "api_name": "hospitality.action.check_in_appointment",
          "description": "The guest arrived at the spa desk: booked / confirmed → checked_in (Book4Time front-desk check-in; Mangomint's waiting room). Business time: `checked_in_at` defaults to now. Re-checking-in is a no-op. Emits `spa.appointment_checked_in`."
        },
        {
          "api_name": "hospitality.action.complete_appointment",
          "description": "The treatment ended: checked_in / in_service → completed. Money is not resolved here — the desk (or the folio, for in-house guests) settles via settle_appointment; the completed/settled split mirrors the spa-floor reality that the treatment ends in the room and payment happens at the desk. Business time: `completed_at` defaults to now. Emits `spa.appointment_completed`."
        },
        {
          "api_name": "hospitality.action.confirm_appointment",
          "description": "Mark the guest's reconfirmation on a booked appointment (Book4Time's Reminder Confirmed; Mindbody's Confirmed). Business time: `confirmed_at` defaults to now. Re-confirming a confirmed appointment is a no-op. Emits `spa.appointment_confirmed`."
        },
        {
          "api_name": "hospitality.action.mark_appointment_no_show",
          "description": "Mark a booked / confirmed appointment as a no-show — the guest never arrived. Distinct from cancel_appointment because no-show is its own operating KPI in every major spa system (Mindbody, Square, Zenoti). The instant lands in `cancelled_at` (the shared did-not-happen stamp); `cancellation_kind` stays absent — no_show is a status, not a cancellation flavour. Fee posting is not automated (same posture as cancel). Emits `spa.appointment_no_show`."
        },
        {
          "api_name": "hospitality.action.reschedule_appointment",
          "description": "Move an appointment to a new start (duration preserved) and/or re-assign its room / provider. Re-runs the same conflict and capability checks as book_appointment against the new window and resources. Refused once the treatment has begun (in_service or later) — post-service corrections are an operator-surface repair, not a reschedule. Emits `spa.appointment_rescheduled`."
        },
        {
          "api_name": "hospitality.action.settle_appointment",
          "description": "Resolve the money on a completed appointment — settle_check's twin. The amount due is the snapshotted price plus gratuity. `tender = room_charge` requires `folio_id` and atomically posts one aggregated spa charge to the folio (bucket = the outlet's usali_department, health_club_spa fallback; charge_at_outlet linked) plus the appointment_settled_to_folio link, emitting `charge.posted.recorded` — the §9.1 fold input. Other tenders stamp `tender` + `settlement_reference`; their revenue reaches the signals tier through the enriched `spa.appointment_settled` event (outlet, bucket, amount, business day) rather than a folio charge — a walk-in's card payment clears through the POS/PSP, not a guest ledger, so no folio is minted for day-spa trade. The charge decomposes like post_charge's: `amount` is the treatment revenue, `gratuity` its own figure (a voluntary tip is a pass-through, not department revenue under USALI), and the folio debit is their sum. Business time: `settled_at` defaults to now; the charge's posted_at and the business day derive from it. A zero-due appointment (fully comped) settles with tender = comp. Re-settling a settled appointment is a no-op."
        }
      ],
      "api_name": "hospitality.appointment",
      "description": "A timed-service booking: one guest × one service in a time window, with the price and buffer pair snapshotted from the spa_service at booking. Lifecycle: booked → confirmed → checked_in → in_service → completed → settled, with cancelled / no_show off-ramps — mutates only through the nine spa actions. `booked_at` is the business instant the booking was taken (pace and lead-time reads derive from it, the reservation discipline); `start_at`/`end_at` bound the treatment itself, and room occupancy extends to [start_at - pre_time, end_at + cleanup_time]. settle_appointment is settle_check's twin: a room_charge tender posts one aggregated folio charge; every tender emits the enriched spa.appointment_settled event, which is the revenue row for card/cash walk-ins (no folio is minted for a POS sale).",
      "display_name": "Appointment",
      "domain": "spa",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Appointment id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Appointment lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "start_at",
          "description": "",
          "display_name": "Treatment start",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "end_at",
          "description": "",
          "display_name": "Treatment end (start + service duration at booking)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "pre_time_minutes",
          "description": "",
          "display_name": "Setup buffer snapshotted from the service at booking",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "cleanup_time_minutes",
          "description": "",
          "display_name": "Turnover buffer snapshotted from the service at booking",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "price",
          "description": "",
          "display_name": "Price snapshotted from the service at booking",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "booked_at",
          "description": "",
          "display_name": "When the booking was taken (business time — pace/lead-time reads)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "booked_by",
          "description": "",
          "display_name": "Principal::stable_id() of the booking principal",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "guest_kind",
          "description": "",
          "display_name": "Who the client is to the property (in-house capture dimension)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "confirmed_at",
          "description": "",
          "display_name": "When the guest reconfirmed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "checked_in_at",
          "description": "",
          "display_name": "When the guest arrived at the spa desk",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "service_started_at",
          "description": "",
          "display_name": "When the treatment actually began",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "When the treatment ended",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "settled_at",
          "description": "",
          "display_name": "When the appointment settled",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancelled_at",
          "description": "",
          "display_name": "When the appointment was cancelled or marked no-show",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancellation_kind",
          "description": "",
          "display_name": "Cancellation kind (required when cancelled; absent on no_show)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "tender",
          "description": "",
          "display_name": "Settlement tender (absent until settle; shares the check vocabulary)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "settlement_reference",
          "description": "",
          "display_name": "PSP / drawer / platform reference for non-room tenders",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "gratuity",
          "description": "",
          "display_name": "Voluntary gratuity added at settle (pass-through, not department revenue)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Front-desk / therapist notes (may carry intake detail)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for replayed / connector-landed bookings",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.approval_decision",
      "description": "One approver's decision on an approval gate. `decided_by` uses `Principal::stable_id()` for humans, services, and agents alike. Agent decisions are recorded for audit but cannot satisfy human-required gates. Supersession is on the `approval_decision_supersedes_decision` self-link, not an inline UUID — the same discipline the rest of the schema uses for supersedence (proposal §4.5 / §6).",
      "display_name": "Approval decision",
      "domain": "approvals",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Decision id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "decision",
          "description": "",
          "display_name": "Decision value recorded by the approver",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_by",
          "description": "",
          "display_name": "Principal::stable_id() of the decider (human / service / agent)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "decider_principal_kind",
          "description": "",
          "display_name": "Principal kind snapshot at decision time (system / user / service / agent)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "counts_toward_approval",
          "description": "",
          "display_name": "False for agent approvals and other audit-only decisions",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "Decision timestamp",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decision_note",
          "description": "",
          "display_name": "Bounded to 1024 characters",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "context_hash",
          "description": "",
          "display_name": "Must match the gate context hash unless recording stale evidence",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "external_reference",
          "description": "",
          "display_name": "Slack interaction id, Linear comment id, etc.",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.assign_approval_gate",
          "description": "Narrow a live gate to specific assignees — the approver-side assignment-tracking half of the §4.4.1 lifecycle. Replaces `assigned_to` with the supplied principal set (which must not intersect `excluded_principal_ids`), advances a `requested` gate to `assigned` (later statuses never regress), and emits `approval_gate.assigned` whenever the assignee set changes. Idempotent when the set is unchanged."
        },
        {
          "api_name": "hospitality.action.decide_approval_gate",
          "description": "Record a human approval decision. Inserts an `approval_decision`, validates `approval_gate_eligible_decider` and `approval_decision_context_hash_matches`, recomputes gate status per the strategy, and on `approved` transitions the guarded step to `ready`. Agent decisions can be recorded for audit but cannot satisfy human-required gates (`approval_gate_agent_cannot_satisfy_human_gate`). Supersession uses the `approval_decision_supersedes_decision` self-link, constrained to same-approver / non-terminal decisions (`approval_decision_supersedes_same_approver_only`)."
        },
        {
          "api_name": "hospitality.action.expire_approval_gate",
          "description": "Expire a live (non-terminal) approval gate whose `expires_at` has elapsed: transitions the gate to `expired` and emits `approval_gate.expired`. Rejected while `expires_at` is absent or still in the future. The parked workflow step is untouched — an expired gate blocks execution; re-request or cancel to move on."
        },
        {
          "api_name": "hospitality.action.open_approval_gate",
          "description": "Record the first in-product open/view of a live gate — the approver-side view-tracking half of the §4.4.1 lifecycle. Stamps `opened_at` once, advances a requested / assigned / notified gate to `opened` (a partially-approved gate keeps its status), and emits `approval_gate.opened` on that first open only. Re-opens are no-ops — the spec tracks the first view, not every view."
        }
      ],
      "api_name": "hospitality.approval_gate",
      "description": "Durable approval gate. Carries an `approval_strategy`, eligible principals / role refs, a `context_snapshot` of the facts shown to approvers, and a `context_hash` over the §4.4.2 input set. Material drift on the underlying parameters, prior outputs, or policy invalidates the gate via `workflow_step_context_hash_stable`. Gates can be used standalone for single-action approvals or attached to a workflow step via `approval_gate_for_workflow_step_run`.",
      "display_name": "Approval gate",
      "domain": "approvals",
      "interfaces": [
        "hospitality.interface.expirable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Gate id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "gate_key",
          "description": "",
          "display_name": "Stable within the workflow run or action correlation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "approval_strategy",
          "description": "",
          "display_name": "How the gate combines decisions to reach `approved`",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "required_approval_count",
          "description": "",
          "display_name": "Required for `approval_strategy = quorum`",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "eligible_principal_ids",
          "description": "",
          "display_name": "Exact eligible principals when known (Principal::stable_id() shape)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "eligible_role_refs",
          "description": "",
          "display_name": "Staff roles / groups that may approve",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "excluded_principal_ids",
          "description": "",
          "display_name": "Requesting agent / author / conflicted principals",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "requested_by",
          "description": "",
          "display_name": "Principal stable id of the gate requester",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "assigned_to",
          "description": "",
          "display_name": "Current assignees, if narrowed",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "context_snapshot",
          "description": "",
          "display_name": "Frozen, redacted facts shown to approvers",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "context_hash",
          "description": "",
          "display_name": "Canonical JCS+SHA-256 over the §4.4.2 input set",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "decision_summary",
          "description": "",
          "display_name": "Final bounded summary",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "requested_at",
          "description": "",
          "display_name": "Gate creation",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "notified_at",
          "description": "",
          "display_name": "First notification sent",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "First in-product open/view event when known",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "Final decision time",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "Gate timeout",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "notification_target",
          "description": "",
          "display_name": "Where the gate notification surfaced",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "external_reference",
          "description": "",
          "display_name": "Slack ts, Linear issue id, PMS task id, email thread id, etc.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "stale_on_context_change",
          "description": "",
          "display_name": "Whether material drift invalidates the gate (default true, GitHub-style)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "risk_tier",
          "description": "",
          "display_name": "Copied from guarded step / action; drives high-risk policy validators",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_asset",
          "description": "The equipment is gone — removed, replaced, scrapped. Terminal: stamps retired_at and the reason, and retires every preventive plan that looks after it in the same transaction (a plan for a boiler that is no longer there raises nothing more). Work orders already open for it stay open — the order that replaces it may be one of them. Its code stays taken. Retiring a retired asset is a no-op."
        },
        {
          "api_name": "hospitality.action.set_asset_status",
          "description": "Put a piece of equipment out of service (with the reason) or back in service. Refuses a retired asset — retirement is retire_asset's, and final. Setting the status it already has, with the same reason, is a no-op."
        }
      ],
      "api_name": "hospitality.asset",
      "description": "A piece of equipment a house maintains — the boiler, a room's air conditioner, the kitchen's walk-in (hubOS's hardware). Keyed by `code`, the house's tag for it, unique within the house: `register_asset` with a code the house already holds re-states that asset in place. Stands in at most one place — a room (`asset_in_room`) or a venue (`asset_at_venue`) — or nowhere in particular. Work orders (`work_order_for_asset`) and preventive plans (`maintenance_plan_for_asset`) attach to it as well as to a room, so its service history is a read. `active`, `out_of_service` (with the reason) or `retired` (terminal; its plans retire with it). Manuals and photos hang off it as attachments.",
      "display_name": "Asset",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "The house's tag for the asset — unique within the house",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "What the asset is, in words (\"Boiler 2\", \"AC unit, room 101\")",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "The trade that maintains it — the work-order categories",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "make",
          "description": "",
          "display_name": "Manufacturer",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "model",
          "description": "",
          "display_name": "Model",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "serial",
          "description": "",
          "display_name": "Manufacturer's serial number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "installed_year",
          "description": "",
          "display_name": "Year of installation",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Notes on the asset",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "active, out_of_service (with the reason), or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "registered_at",
          "description": "",
          "display_name": "When the asset was first registered",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "out_of_service_reason",
          "description": "",
          "display_name": "Why the asset is out of service (present exactly while it is)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "status_changed_at",
          "description": "",
          "display_name": "When the status last changed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the asset was retired",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "retire_reason",
          "description": "",
          "display_name": "Why the asset was retired — removed, replaced, scrapped",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.activate_automation",
          "description": "Enable a `draft` or `paused` automation — the evaluator starts matching its condition and firing its effect. Idempotent on an already-active automation; refused on an `archived` one. Emits `automation.activated`."
        },
        {
          "api_name": "hospitality.action.archive_automation",
          "description": "Archive an automation — terminal; the rule is retired and never fires again. The row and its run history stay (soft-delete is the ontology-wide contract). Idempotent on an already-archived automation. Emits `automation.archived`."
        },
        {
          "api_name": "hospitality.action.pause_automation",
          "description": "Pause an `active` automation — the evaluator stops firing it, reversibly. `reason` lands on the audit event. Idempotent on an already-paused automation; refused on an `archived` one. Emits `automation.paused`."
        },
        {
          "api_name": "hospitality.action.run_automation_now",
          "description": "Fire an `active` scheduled automation — a `time_cron` rule or an `object_set_all` sweep — now, off its clock: the operator's \"fire the effect by hand\", and how a gate proves a sweep without waiting for its quarter hour. The verb records the request (`automation.run_requested`); the automation lane's event tail fires the rule once for it, exactly as its schedule would — a sweep over the objects matching its filter at the instant of the request, a cron rule's effect once — through the same effectively-once claim (keyed on the request), the same approval staging and the same run artefact, so the run is read where every run is (`automation_run`). Refused on an automation that is not active and on an event-driven one (it fires on its event, and has no firing without one), and to an automation's own principal (rules chain by events, not by running each other). Writes nothing else; with the automation lane off, the request is recorded and nothing fires."
        },
        {
          "api_name": "hospitality.action.update_automation",
          "description": "Tune a `draft` or `paused` automation's parameters — display name, description, the object-set filter, the modified-property watch, the cron expression, the effect templates, execution mode, approval routing, and the cascade-depth guard. Structural identity (condition kind, effect kind, watched object type, effect action/function) is immutable — to change it, archive and redefine. Refused on an `active` or `archived` automation (pause first). Only provided parameters are patched. Emits `automation.updated`."
        }
      ],
      "api_name": "hospitality.automation",
      "description": "A declarative monitor→effect rule: when a condition is met — an object entering/leaving/changing within a filtered object set, a periodic sweep, or a cron schedule — the automation fires an effect (apply a registered action, call a read-only function, or send a notification). The effect never mutates state directly; `apply_action` fires the target action through the normal scope-checked action surface, and the `requires_approval` variant routes it through a 0020 approval_gate instead of applying. Only `active` automations fire. Mirrors Palantir AIP Automate; see `products/labs/docs/palantir-aip-deep-scan.md` §4.1.",
      "display_name": "Automation",
      "domain": "automation",
      "interfaces": [
        "hospitality.interface.approvable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Automation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "display_name",
          "description": "",
          "display_name": "Human-readable name, surfaced in the operator console",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description_text",
          "description": "",
          "display_name": "What this automation is for — the operator's own note",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "created_by",
          "description": "",
          "display_name": "Principal::stable_id() of the operator/agent that defined it — the audit anchor",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "condition_kind",
          "description": "",
          "display_name": "The trigger-predicate class",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "object_type_ref",
          "description": "",
          "display_name": "The object type the object-set condition watches (ApiName form); required for `object_set_*` kinds, absent for `time_cron` (`automation_condition_coherent`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "object_set_filter",
          "description": "",
          "display_name": "The search-filter predicate that defines the watched object set over `object_type_ref` (same filter grammar as object search); absent = every object of the type",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "modified_property_watch",
          "description": "",
          "display_name": "For `object_set_modified`: the property api-names whose change trips the condition (JSON array); empty/absent = any property change. An entry `name:decreased` / `name:increased` trips only when that number moved that way (an absent side reads as zero)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "cron_expression",
          "description": "",
          "display_name": "The schedule (5-field cron, evaluated in UTC by the host-side schedule worker): required for `time_cron` and for the `object_set_all` sweep (`automation_condition_coherent`); meaningless for the tail-driven kinds",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "event_type_ref",
          "description": "",
          "display_name": "For `event_named`: the named domain event the condition watches (ApiName form, e.g. `room_queue.room_ready`). The event's payload is the image `object_set_filter` matches and `${event.<field>}` reads; required for `event_named` and forbidden for every other kind (`automation_condition_coherent`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "effect_kind",
          "description": "",
          "display_name": "The effect fired when the condition is met",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "effect_action",
          "description": "",
          "display_name": "For `apply_action`: the registered action to fire (ApiName form); validated against the registry and forbidden from targeting an internal-only action (`automation_effect_coherent`, `automation_no_internal_effect_action`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "effect_function",
          "description": "",
          "display_name": "For `call_function`: the read-only registry function to invoke (ApiName form)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "effect_params_template",
          "description": "",
          "display_name": "Template mapping triggering-object fields → the effect's input params (JSON; `${object.<prop>}` / `${previous.<prop>}` placeholders resolved per-firing by the evaluator)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "effect_notification_template",
          "description": "",
          "display_name": "For `send_notification`: the templated notification body/target (JSON)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "execution_mode",
          "description": "",
          "display_name": "Per-object vs batch effect firing for object-set conditions",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "requires_approval",
          "description": "",
          "display_name": "When true, the effect opens a 0020 approval_gate instead of applying (the human-approval-staging variant); requires `approval_policy_ref` (`automation_approval_requires_policy`)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "approval_policy_ref",
          "description": "",
          "display_name": "The approval policy the staged gate is opened under (proposal 0020)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "max_cascade_depth",
          "description": "",
          "display_name": "Loop guard: the maximum trigger-chain depth an effect firing may reach before the evaluator suppresses it (default 3). An automation whose effect trips another automation increments the depth along the `correlation_id` lineage",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Enablement lifecycle; only `active` automations fire",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.automation_run",
      "description": "A frozen, auditable record of one automation firing: which objects tripped the condition (with their current + previous image), which effect fired, and the terminal outcome (`effect_applied`, `staged_for_approval`, `effect_failed`, or `suppressed`). Written by the host-side evaluator via the internal `record_automation_run` action; immutable post-insert like every other Aerandil audit artefact. The `effect_correlation_id` joins to the chained action's event stream; `approval_gate_ref` joins to the staged gate.",
      "display_name": "Automation run",
      "domain": "automation",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Automation run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "automation_ref",
          "description": "",
          "display_name": "The automation that fired (id of the `automation` object)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "fired_at",
          "description": "",
          "display_name": "When the evaluator dispatched the effect",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "condition_kind",
          "description": "",
          "display_name": "Snapshot of the automation's condition kind at firing time",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "triggering_object_type",
          "description": "",
          "display_name": "The watched object type (absent for `time_cron`); for `event_named`, the named event type that tripped it",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "triggering_object_refs",
          "description": "",
          "display_name": "Ids of the objects that tripped the condition (JSON array; bounded); for `event_named`, the triggering event's sequence number",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "triggering_delta_snapshot",
          "description": "",
          "display_name": "For `object_set_modified`: the before/after delta the evaluator matched; bounded excerpts only",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "effect_kind",
          "description": "",
          "display_name": "Snapshot of the effect kind fired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "effect_target",
          "description": "",
          "display_name": "The action/function fired (ApiName form; absent for `send_notification`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "effect_input_snapshot",
          "description": "",
          "display_name": "The resolved effect input the evaluator dispatched — the audit-grade record of what actually fired",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "outcome",
          "description": "",
          "display_name": "Terminal outcome of the firing",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "effect_correlation_id",
          "description": "",
          "display_name": "For `effect_applied` on `apply_action`: the chained action's correlation id — the audit-trail join (`automation_run_outcome_coherent`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "recorded_correlation_id",
          "description": "",
          "display_name": "The correlation id this run was itself recorded under — stamped by `record_automation_run` from its own invocation, so every event the recording emits joins back to the run and the cascade guard sees the lane's own bookkeeping as an echo, never a fresh depth-0 trigger",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "approval_gate_ref",
          "description": "",
          "display_name": "For `staged_for_approval`: the opened 0020 approval_gate id (`automation_run_outcome_coherent`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "error_text",
          "description": "",
          "display_name": "For `effect_failed`: the error the effect returned (`automation_run_outcome_coherent`)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cascade_depth",
          "description": "",
          "display_name": "This firing's depth in the trigger chain (0 = directly operator/data-driven)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "correlation_id",
          "description": "",
          "display_name": "Trigger-lineage anchor — shared across a cascade so depth and cycles are traceable",
          "kind": "string",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.match_waitlist_entries",
          "description": "Offer a freed room-night to the waitlist. Takes the ledger row that moved; reads its room type's waiting entries in working order (priority, then requested_at, then id) and offers every one whose whole stay the house can now take — the selling ceiling on every ledger night, the rate plan's stay window and the restriction wall, exactly what accept will check — each offer holding its nights until the property's offer deadline, so the next entry is judged against what the last one left. Passes over entries already offered, entries whose offer once lapsed (the desk can still offer them), `guest_request` entries (a need the ledger cannot see) and arrivals already past. The automation lane fires it when a night's headroom rises: a cancellation, a no-show, a block or offer released, a room back in order, more rooms on sale. A no-op when nothing clears."
        }
      ],
      "api_name": "hospitality.availability_day",
      "description": "Inventory ledger entry per `(property, room_type, business_date)`. The single source of truth for whether a room is sellable. `physical` is the underlying room count; `sellable` is the count exposed to channels after holds/walls; `sold` is occupancy as of the snapshot; `oversell_limit` is the operator's declared headroom above `sellable`, which offsets cancellations and no-shows — declared by `set_availability`, moved by an applied sell-limit recommendation; `declared_oversell_limit` keeps the operator's declaration (what a recommendation's reset restores) and `oversell_ceiling` the most headroom a recommendation may set; `out_of_order` is the count of this type's rooms blocked out of order this date (owned by the room_status_block verbs, never by set_availability); `blocked` is the count held by group blocks and not yet picked up (owned by the group-block verbs, likewise never by set_availability). The booking flow refuses at `sellable - out_of_order - blocked + oversell_limit`, and the validator on this type enforces `0 <= sold <= sellable + oversell_limit`, `sellable <= physical`, `0 <= out_of_order <= physical`, and `0 <= blocked <= physical` on every write.",
      "display_name": "Availability day",
      "domain": "distribution",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Availability id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "physical",
          "description": "",
          "display_name": "Physical room count",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "sellable",
          "description": "",
          "display_name": "Sellable room count",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "sold",
          "description": "",
          "display_name": "Sold count",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "blocked",
          "description": "",
          "display_name": "Rooms held by group blocks and not yet picked up",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "oversell_limit",
          "description": "",
          "display_name": "Oversell limit",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "declared_oversell_limit",
          "description": "The oversell headroom the operator declared — what a sell-limit recommendation's reset restores",
          "display_name": "Declared oversell limit",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "oversell_ceiling",
          "description": "The most oversell headroom a sell-limit recommendation may set (the operator's guardrail; absent, the declared limit)",
          "display_name": "Oversell ceiling",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "out_of_order",
          "description": "",
          "display_name": "Rooms of this type blocked out of order this date",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "committed_oversell",
          "description": "",
          "display_name": "Room-nights accepted past the ceiling because a channel had already sold them",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.add_banquet_order_line",
          "description": "Add a resource to the order — a menu per person, a screen at a fixed quantity — in the order's currency. Adding to an issued order marks it `revised`, so the departments get it again; a posted or cancelled order refuses."
        },
        {
          "api_name": "hospitality.action.cancel_banquet_event_order",
          "description": "Withdraw the order: `draft`, `issued` or `revised` becomes `cancelled`. A posted order cannot be cancelled — its charges are on a folio and are reversed there."
        },
        {
          "api_name": "hospitality.action.issue_banquet_event_order",
          "description": "Send the order to the departments: `draft` or `revised` becomes `issued`, the revision number steps, `issued_at` is stamped. Needs at least one line and a `definite` or `actual` event — a tentative function is not cooked for."
        },
        {
          "api_name": "hospitality.action.post_banquet_event_charges",
          "description": "Post every line of an issued order as a charge: per-person lines at the billable count (the higher of the guarantee and the actual) times their quantity, fixed lines at their quantity, the service charge on food and beverage lines carried as the charge's gratuity leg. Needs the event `actual` and the order `issued` or `revised`. The charges go to the order's folio, else the block's master folio, and a routing rule bound there is obeyed; the folio must be open and in the order's currency. The order becomes `posted` and its lines freeze."
        }
      ],
      "api_name": "hospitality.banquet_event_order",
      "description": "The banquet event order (function sheet) for one function event: the resources the departments set, cook and serve — its lines — with the setup, service and billing notes, issued to the house under a revision number and re-issued when a line changes after distribution. Once the event is actual, `post_banquet_event_charges` posts every line at the billable count to the order's folio or the block's master folio and freezes the order as `posted`.",
      "display_name": "Banquet event order",
      "domain": "catering",
      "interfaces": [],
      "properties": [
        {
          "api_name": "number",
          "description": "",
          "display_name": "BEO number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Order status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "revision",
          "description": "",
          "display_name": "How many times the order has been issued",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency every line is priced in",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the current revision went to the departments",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "posted_at",
          "description": "",
          "display_name": "When the charges were posted",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "service_charge_rate",
          "description": "",
          "display_name": "Service charge applied to food and beverage lines at posting (0.125 = 12.5%)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "billing_instructions",
          "description": "",
          "display_name": "How the client pays — master account, deposits, what the guests settle",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "setup_notes",
          "description": "",
          "display_name": "Setup: tables, linen, staging, signage",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "service_notes",
          "description": "",
          "display_name": "Service: the run of show, course timings, bar hours",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "av_notes",
          "description": "",
          "display_name": "Audiovisual: equipment, technicians, rehearsal",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.remove_banquet_order_line",
          "description": "Take a line off the order. Removing from an issued order marks it `revised`; a posted or cancelled order's lines are frozen."
        },
        {
          "api_name": "hospitality.action.update_banquet_order_line",
          "description": "Change a line (any subset of its fields). Changing a line on an issued order marks the order `revised`; a posted or cancelled order's lines are frozen."
        }
      ],
      "api_name": "hospitality.banquet_order_line",
      "description": "One resource on a banquet event order — a menu, a coffee break, the room, a screen. `quantity_basis` says whether it bills per person (times the event's billable count) or as a fixed quantity; `unit_price` is in the order's currency. Posting links the line to the charge it became.",
      "display_name": "Banquet order line",
      "domain": "catering",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Line id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "sequence",
          "description": "",
          "display_name": "Order of the line on the sheet",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "Section of the sheet the line belongs to",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "What the line is",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "quantity_basis",
          "description": "",
          "display_name": "Per person, or a fixed quantity",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "quantity",
          "description": "",
          "display_name": "Fixed quantity, or units per person",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "unit_price",
          "description": "",
          "display_name": "Price per unit",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "serve_at",
          "description": "",
          "display_name": "When the line is served or set",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Dietary, presentation, placement",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.block_allocation",
      "description": "One cell of a group block's room-type x date grid. `blocked`          is the UNPICKED hold — the number this block still keeps out          of house availability — and `picked` counts rooms already          taken up by reservations. Rooms available to the group are          `blocked - picked`, derived rather than stored.          `blocked_original` is the allotment as first committed and          does not move, so a wash or an attrition claim always has a          baseline.",
      "display_name": "Block allocation",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Allocation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Stay night",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "blocked",
          "description": "",
          "display_name": "Rooms still held and unpicked",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "blocked_original",
          "description": "",
          "display_name": "Allotment as first committed",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "picked",
          "description": "",
          "display_name": "Rooms picked up by reservations",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "rate_amount",
          "description": "",
          "display_name": "Per-cell rate override",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_board_sort_pattern",
          "description": "Give a board back to the kernel's default order: the pattern stops ordering it (`board_sort` reads the default, or a staff member's own preference). Retiring a retired pattern is a no-op; set_board_sort_pattern for the same board revives it. Emits `board_sort_pattern.retired`."
        }
      ],
      "api_name": "hospitality.board_sort_pattern",
      "description": "The order a property sets for one of its boards (hubOS's server-defined sort pattern): the attendant's housekeeping sheet, the supervisor's rack, the engineer's work-order board, or the pacing report. `keys` is an ordered list — the first key decides, the next breaks its ties — each with a direction and, for a ranked key (priority, task type, status), the house's order of its values (departures before stayovers, say). One per board per house, authored by `set_board_sort_pattern` (a repeated board replaces it in place) and retired by `retire_board_sort_pattern`, which gives the board back to the kernel's default. A staff member's own preference (`staff_member.sort_preferences`) wins over it; `hospitality.function.board_sort` reads the order a caller sees.",
      "display_name": "Board sort pattern",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Board sort pattern id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "board",
          "description": "",
          "display_name": "The board this pattern orders",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "keys",
          "description": "",
          "display_name": "The keys the board is ordered by, first key first — each a key valid for the board, a direction, and for a ranked key optionally the order of its values",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Active (orders the board) or retired (the kernel's default does)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the pattern was retired",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.booking_origin",
      "description": "The IP address and device a guest booked from, captured only for a booking the guest made online themselves (`source = direct_web`). Card-network evidence for a fraud chargeback (Visa Compelling Evidence 3.0); kept apart from the reservation so its own retention clock applies and its values never travel in an event image.",
      "display_name": "Booking origin",
      "domain": "booking",
      "interfaces": [
        "hospitality.interface.retention_bound"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Origin record id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "ip_address",
          "description": "",
          "display_name": "The IP address the booking was made from, as the booking engine saw it (IPv4 or IPv6, canonical form)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "device_id",
          "description": "",
          "display_name": "A stable identifier for the device the booking was made on, as the booking engine assigns it. Visa counts device id or device fingerprint as one element, never both.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "captured_at",
          "description": "",
          "display_name": "When the booking engine saw the guest book",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "retain_until",
          "description": "",
          "display_name": "Storage-limitation clock: captured_at plus 485 days, the 364-day CE 3.0 lookback and the 120-day window for a dispute to arrive. The same discipline `payment_card` and `guest_id_document` carry.",
          "kind": "date",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.brand",
      "description": "A hospitality brand. Used by enterprise tenants that operate multiple properties under a shared name and standards. Boutique tenants typically have zero or one Brand.",
      "display_name": "Brand",
      "domain": "organization",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Brand code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Brand name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "website",
          "description": "",
          "display_name": "Website",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "founded",
          "description": "",
          "display_name": "Founded",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "segment",
          "description": "",
          "display_name": "Segment",
          "kind": "enum",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.void_brand_operator_cost",
          "description": "Void the target entry — terminal, the amount stays immutable as the audit record. Emits `brand_operator.cost_voided.decided` so the `daily_brand_operator` projection folds the amount back out."
        }
      ],
      "api_name": "hospitality.brand_operator_cost_entry",
      "description": "One posted mandatory brand/operator charge (USALI 12 Schedule 16) — royalty, marketing assessment, loyalty, reservations, technology, training, purchasing, QA, or management fee. Only mandatory charges; optional services and pass-throughs are excluded by Schedule 16's definition. Immutable except the terminal void.",
      "display_name": "Brand & operator cost entry",
      "domain": "brand_operator",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "USALI Schedule 16 line",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "counterparty",
          "description": "",
          "display_name": "Counterparty (brand | operator)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "period_start",
          "description": "",
          "display_name": "Invoice-period start",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "period_end",
          "description": "",
          "display_name": "Invoice-period end",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Source",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "External ref",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "posted_at",
          "description": "",
          "display_name": "Posted at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "voided",
          "description": "",
          "display_name": "Voided",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.business_date",
      "description": "The property's accounting day. Created when the property opens the day, closed by the night-audit run. Most operational objects (charges, room postings, restrictions) attach to a business date rather than to wall-clock UTC.",
      "display_name": "Business date",
      "domain": "audit",
      "interfaces": [],
      "properties": [
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "Opened at (UTC)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "closed_at",
          "description": "",
          "display_name": "Closed at (UTC)",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.cancellation_policy",
      "description": "Structured cancellation terms — an ordered penalty ladder keyed on hours-before-arrival, plus the no-show carve-out and the notice-window clock basis. Reservations bind both a live `cancellation_policy_ref` and a `cancellation_policy_text` snapshot at booking time per the structured-policy + immutable-snapshot pattern (design-spec §B.2). Brand-standard policies live at brand; bespoke / property-specific at property. Non-refundable advance-purchase rates ship with empty `tiers`, `non_refundable = true`, and `applies_to_no_show = always_100_percent`.",
      "display_name": "Cancellation policy",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Policy code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Display name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "non_refundable",
          "description": "",
          "display_name": "Non-refundable (advance-purchase shape)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency (ISO 4217)",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "tiers",
          "description": "",
          "display_name": "Penalty tier ladder (sorted descending by trigger offset)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "applies_to_no_show",
          "description": "",
          "display_name": "No-show handling",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "notice_window_basis",
          "description": "",
          "display_name": "Notice-window clock basis",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.cashier",
      "description": "A cash drawer's identity at one property — the till that shifts open against and cash movements reconcile to. Mews's cashier without the user-assignment mirror; OPERA's cashier ID without the PIN ceremony. Retire a cashier rather than deleting it: closed sessions keep pointing at the till they reconciled.",
      "display_name": "Cashier",
      "domain": "cashiering",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Cashier code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Cashier name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.close_cashier_session",
          "description": "Close a shift against the operator's count. Expected = opening float + cash in - cash out over the session's movements; variance = counted - expected, recorded as declared data (the over/short) — never a GL posting, because no benchmark vendor verifiably posts one. Closed is immutable: re-closing with the same count is a no-op, a different count refuses, and corrections are compensating entries in the next session. The close never gates the business date — the night audit only counts open sessions as an exception signal."
        },
        {
          "api_name": "hospitality.action.record_cashier_transaction",
          "description": "Record one typed cash movement in an open session — cash_in, cash_out, tip_out, expense (the paid-out), or float_adjustment. Amounts are positive; the kind carries the direction. Append-only: a mistake is cancelled by a compensating entry, never an edit or a void. Pass payment_id when the movement mirrors a folio cash payment — the payment stays the ledger truth, the session entry reconciles the physical drawer."
        }
      ],
      "api_name": "hospitality.cashier_session",
      "description": "One cash shift against a cashier: opens with a float, accumulates typed cash movements, and closes against an operator count. Expected balance = opening float + cash in - cash out; variance = counted - expected, recorded as declared data (the over/short), never a GL posting. Single-currency by design — a multi-currency desk opens one session per currency. Closed sessions are immutable; corrections are compensating transactions in the next session.",
      "display_name": "Cashier session",
      "domain": "cashiering",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Cashier session id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Session currency (ISO 4217)",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "opening_float",
          "description": "",
          "display_name": "Opening float",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "Opened at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "closed_at",
          "description": "",
          "display_name": "Closed at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "counted_amount",
          "description": "",
          "display_name": "Counted at close",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "expected_amount",
          "description": "",
          "display_name": "Expected at close",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "variance",
          "description": "",
          "display_name": "Variance at close",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.cashier_transaction",
      "description": "One typed cash movement in a cashier session — cash_in, cash_out, tip_out, expense (OPERA's paid-out), or float_adjustment. Amounts are non-negative; the kind carries the direction. Append-only: corrections are compensating entries, never edits. Optionally mirrors a folio payment through cashier_transaction_mirrors_payment — the drawer entry is the mirror, the payment stays the ledger truth.",
      "display_name": "Cashier transaction",
      "domain": "cashiering",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Cashier transaction id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Movement kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "occurred_at",
          "description": "",
          "display_name": "Occurred at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes (the reason for the movement)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.channel",
      "description": "A distribution channel: direct booking, an OTA, a GDS, a tour operator, etc. Channels carry their own credentials/state and are connected to rate plans they distribute via the `channel_distributes_rate_plan` link, replacing the prior single-channel enum on `RatePlan`.",
      "display_name": "Channel",
      "domain": "distribution",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Channel code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Channel name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Channel kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "external_code",
          "description": "",
          "display_name": "External / partner code",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "commission_rate",
          "description": "",
          "display_name": "Current commission rate",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "commission_kind",
          "description": "",
          "display_name": "Commission kind",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "commission_flat_amount",
          "description": "",
          "display_name": "Per-booking flat commission",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "target_share",
          "description": "",
          "display_name": "Operational target share",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "target_share_basis",
          "description": "",
          "display_name": "Target share basis",
          "kind": "enum",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.channel_commission_period",
      "description": "Time-varying commission contract for a (channel, property) pair. Carries the rate, kind, and flat-amount in effect for the period. Authoritative ledger over the `current` surface on `channel`; the channel scalar is the convenient operator- facing handle for the present row.",
      "display_name": "Channel commission period",
      "domain": "distribution",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Period id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "Effective from",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Effective to (null = currently in effect)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "commission_rate",
          "description": "",
          "display_name": "Commission rate (percent kinds)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "commission_kind",
          "description": "",
          "display_name": "Commission kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "commission_flat_amount",
          "description": "",
          "display_name": "Flat per-booking commission (flat kinds)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "commission_kind_notes",
          "description": "",
          "display_name": "Operator-facing qualifier",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.close_channel_connection",
          "description": "Terminal end of a conduit — the property stopped distributing through this partner. Publication stops and inbound reservations are rejected as `connection_inactive` rather than silently dropped, because a partner still holding the connection open needs a definite answer. Soft-delete stays the ontology-wide contract; `closed` is the domain-level end state. Emits `channel_connection.closed`."
        },
        {
          "api_name": "hospitality.action.configure_channel_connection",
          "description": "Re-point a conduit: endpoint, publication horizon, price basis, credential reference, partner codes, or the set of demand channels it carries. Every parameter is optional and only the ones given move — this is a patch, not a restatement. `rate_model` is deliberately absent: switching between per-day and occupancy-based pricing means re-certifying with the partner and re-publishing the whole horizon, so it is a new conduit, not an edit. Emits `channel_connection.configured`."
        },
        {
          "api_name": "hospitality.action.map_channel_inventory",
          "description": "Bind one of our room types or rate plans to the code the partner knows it by. Upsert on (conduit, mapping kind, our room type / rate plan): re-mapping closes the prior binding with `effective_to` and mints a fresh row, so the history of what was published under which code survives. `included_occupancy` is load-bearing under occupancy-based pricing — a rate push that omits the occupancy level matching the partner's configured included occupancy is silently not applied, with no error and no price. The first mapping on a `pending_mapping` conduit promotes it to `active`. Emits `channel_mapping.mapped`."
        },
        {
          "api_name": "hospitality.action.pause_channel_connection",
          "description": "Stop publishing on a conduit from our side — a credential rotation, a maintenance window, a mapping rebuild. Reversible with `resume_channel_connection`. Distinct from `suspend_channel_connection`, which records that the *partner* stopped us; collapsing the two would hide which side owns the remediation. Inbound reservations keep arriving while paused and are still recorded — a guest who already booked does not stop existing because we stopped publishing. Emits `channel_connection.paused`."
        },
        {
          "api_name": "hospitality.action.receive_channel_reservation",
          "description": "Record an inbound reservation message from a partner, keyed on their message id. A replay returns the existing delivery untouched rather than minting a second one: every channel wire is at-least-once, so duplicate delivery is the norm and de-duplication here is what stops one OTA booking becoming three. Recording is deliberately separate from applying — a message we cannot yet act on is still a message we received, and losing it because the booking failed is how an OTA reservation goes missing. `origin_channel_code` is kept verbatim even when it maps to no `channel` row, because an unrecognised code is exactly the signal to add one. Emits `channel_reservation.received`."
        },
        {
          "api_name": "hospitality.action.resume_channel_connection",
          "description": "Return a paused or suspended conduit to `active`. Resuming does not itself publish anything: the dispatcher's next pass picks up whatever accumulated in the outbox while the conduit was stopped, which is why a long pause is followed by a large delta rather than a full flush. Emits `channel_connection.resumed`."
        },
        {
          "api_name": "hospitality.action.start_channel_publication",
          "description": "Open one audited ARI push against an `active` conduit — creates a `channel_publication_run` in `running` and links it. `delta` is the steady state and the only trigger permitted in normal operation: SiteMinder enforces delta-only publication and names repeated full flushes inside 24 hours as grounds for immediate interface suspension. So `full`, `recovery` and `manual` are refused when `channel_connection.last_full_publication_at` is inside the last 24 hours, and a successful one stamps it. Emits `channel_publication.started`."
        },
        {
          "api_name": "hospitality.action.suspend_channel_connection",
          "description": "Record that the *partner* stopped the conduit, and why. SiteMinder gives a non-compliant partner 90 days to remediate and reserves immediate suspension for production-affecting issues, so this is a state an operator must be able to see and distinguish from our own pause: the clock and the remediation belong to the other side. Emits `channel_connection.suspended`."
        }
      ],
      "api_name": "hospitality.channel_connection",
      "description": "One conduit between a property and a distribution partner — a channel manager, an OTA switch, or a partner that subscribed to our ARI. Identity, wire dialect and lifecycle only: credentials live in the secret store (`secret_ref` is a name, never a value), the per-date publication outbox lives in Postgres, and the demand channels the conduit carries are linked, not inlined. A conduit is not a channel — one connection fronts Booking.com, Expedia and dozens more, each of which stays its own `channel` row with its own commission period.",
      "display_name": "Channel connection",
      "domain": "channel_manager",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Connection id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "display_name",
          "description": "",
          "display_name": "Human-readable name shown in the Explorer",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "provider",
          "description": "",
          "display_name": "Partner identifier (siteminder / d_edge / derbysoft / …)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "protocol",
          "description": "",
          "display_name": "Wire dialect this conduit speaks",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "provisioning",
          "description": "",
          "display_name": "Who stood the conduit up (we call them, or they subscribed to us)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Connection lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "ari_endpoint_url",
          "description": "",
          "display_name": "HTTPS endpoint we publish ARI into",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "secret_ref",
          "description": "",
          "display_name": "Name of the credential in the external secret store (never the value)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "partner_code",
          "description": "",
          "display_name": "The partner's identifier for our system (SiteMinder RequestorID / pmsCode)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "property_code",
          "description": "",
          "display_name": "The partner's identifier for this property (SiteMinder HotelCode)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "owner_client_ref",
          "description": "",
          "display_name": "Client that registered this subscription (subscription provisioning only)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "rate_model",
          "description": "",
          "display_name": "Published rate model — fixed per conduit, never per property",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "price_basis",
          "description": "",
          "display_name": "Whether published amounts are gross, net, or both",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reservation_delivery",
          "description": "",
          "display_name": "How reservations reach us from the partner",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "horizon_days",
          "description": "",
          "display_name": "Days forward from today that ARI is published for",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "last_full_publication_at",
          "description": "",
          "display_name": "When a full (non-delta) publication last ran — gates the next one",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "paused_reason",
          "description": "",
          "display_name": "Why publication is stopped (required while status = paused or suspended)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "registered_by",
          "description": "",
          "display_name": "Principal::stable_id() of the registering principal",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "registered_at",
          "description": "",
          "display_name": "When the connection was opened",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Free-form operator notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.unmap_channel_inventory",
          "description": "Retire a code binding by closing it with `effective_to`. The conduit stops publishing that room type or rate plan and starts rejecting inbound reservations naming its code. The row is closed rather than deleted because reservations already taken under that code still have to resolve, and an operator asked 'what did this OTA code mean in March' needs an answer. Emits `channel_mapping.unmapped`."
        }
      ],
      "api_name": "hospitality.channel_mapping",
      "description": "Binds one of our room types or rate plans to the code a partner knows it by. Codes are per-connection — the same room type is `DBL` to one partner and `RT-004` to the next — and nothing can be published or accepted under a code with no mapping row. That refusal is not defensive programming: 'unmapped space code' and 'unmapped rate code' are named, required tests in the Mews certification suite, and an unmapped code is the single most common cause of a booking that never reaches the PMS.",
      "display_name": "Channel mapping",
      "domain": "channel_manager",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Mapping id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "mapping_kind",
          "description": "",
          "display_name": "What this row binds",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "external_code",
          "description": "",
          "display_name": "The partner's code for this room type / rate plan",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "external_name",
          "description": "",
          "display_name": "The partner's display name for it (drift check against their config)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "included_occupancy",
          "description": "",
          "display_name": "Adult occupancy the partner's price is quoted for (occupancy-based pricing)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "publish_rates",
          "description": "",
          "display_name": "Publish rate amounts for this binding",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "publish_restrictions",
          "description": "",
          "display_name": "Publish restrictions for this binding",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "First date this binding applies",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Last date this binding applies (null = currently in effect)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Free-form operator notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.channel_mix_snapshot",
      "description": "Daily-materialised projection of bookings, room-nights, and revenue per channel for a property. Mirrors `night_audit_run` semantics: immutable post-write, one row per (property, business_date, cadence). Re-materialisation creates a new row, never a mutation. Operators that need to correct a snapshot delete the prior row first; the delete is itself an event so the correction is auditable.",
      "display_name": "Channel mix snapshot",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Snapshot id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "cadence",
          "description": "",
          "display_name": "Cadence",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "per_channel",
          "description": "",
          "display_name": "Per-channel contributions",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "direct_share_observed",
          "description": "",
          "display_name": "Direct share (decimal)",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "ota_share_observed",
          "description": "",
          "display_name": "OTA share (decimal)",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "gds_share_observed",
          "description": "",
          "display_name": "GDS share (decimal)",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "materialised_at",
          "description": "",
          "display_name": "When the snapshot was materialised (UTC)",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.complete_channel_publication",
          "description": "Close a `running` publication run with its outcome and counts. `partial` is a real outcome, not a rounding of success or failure: a run that published four hundred date-cells and had eleven refused for an unmapped rate plan is neither, and flattening it loses the only signal that mapping is broken. `error_summary` is required for `failed` and `partial`. Cells and messages are counted separately because bundling makes them diverge — the same changed cells are one message or forty depending on how well consecutive dates collapsed, and the partner's load is measured in messages while ours is measured in cells. Emits `channel_publication.completed`."
        }
      ],
      "api_name": "hospitality.channel_publication_run",
      "description": "One audited ARI push at a conduit: what triggered it, when it ran, how many date-cells it published across availability, rates and restrictions, and how many the partner refused. Run-grain only — per-cell state stays in the publication outbox in Postgres. `partial` is a first-class outcome: a run that published four hundred cells and had eleven refused for an unmapped rate plan neither succeeded nor failed, and flattening it to either loses the only signal that mapping is broken.",
      "display_name": "Channel publication run",
      "domain": "channel_manager",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Publication run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Publication-run lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "trigger",
          "description": "",
          "display_name": "What initiated this run",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the run started",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "finished_at",
          "description": "",
          "display_name": "When the run reached a terminal status",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "window_start",
          "description": "",
          "display_name": "First business date covered by this run",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "window_end",
          "description": "",
          "display_name": "Last business date covered by this run",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "cells_published",
          "description": "",
          "display_name": "Date-cells successfully published across all facets",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "cells_rejected",
          "description": "",
          "display_name": "Date-cells the partner refused",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "messages_sent",
          "description": "",
          "display_name": "Wire messages sent (bundling collapses many cells into one)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "error_summary",
          "description": "",
          "display_name": "Why the run failed or was partial (required for those outcomes)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.acknowledge_channel_reservation",
          "description": "Record that our acknowledgement reached the partner, closing the at-least-once loop. Acknowledgement is a timestamp rather than a state because it composes with both outcomes: the partner is told the message was handled whether we applied it or refused it, and only silence triggers redelivery. A delivery still `received` or `failed` cannot be acknowledged — that is precisely the case where we want the partner to send it again. Emits `channel_reservation.acknowledged`."
        },
        {
          "api_name": "hospitality.action.apply_channel_reservation",
          "description": "Land a received message in the ontology. This is the committed-upstream path, and it is the reason the seam exists: the sale completed on the partner's side before we heard about it, so our selling ceiling and our restriction wall stop being gates. Oracle states the rule for a push channel plainly — inventory and restriction checks are bypassed because the booking is already committed upstream. Refusing here would not un-sell the room; it would only leave the house ignorant of a guest who is turning up. Where the booking goes past the ceiling the breach is counted on `availability_day.committed_oversell` and reported on the delivery as `oversold_by`, so the operator sees the exposure instead of a refusal. Booking parameters are the same set `create_reservation` takes, shared in code so the two cannot drift — a divergence would be a booking an OTA can make and the desk cannot represent. Serves the `book` intent only: a cancellation goes to `cancel_channel_reservation`, which needs a reservation and nothing else, and a restatement goes to `restate_channel_reservation`, which names both the reservation and the stay. Emits `channel_reservation.applied`."
        },
        {
          "api_name": "hospitality.action.cancel_channel_reservation",
          "description": "Release a booking the partner cancelled on their side, through the same in-place cancel the desk uses — so the folio, the share group and the inventory all unwind exactly as they would at the front desk. Separate from `apply_channel_reservation` because the required parameters differ: a cancellation names a reservation and nothing else, while a booking needs the whole stay, and one verb whose required set depends on a field it does not carry is a verb no generated SDK or agent tool can call correctly. Releasing recomputes any committed oversell from the new sold count, so the breach unwinds with the booking that caused it. Emits `channel_reservation.applied`."
        },
        {
          "api_name": "hospitality.action.reject_channel_reservation",
          "description": "Permanently refuse a message — an unmapped room type or rate plan, an unknown property, a malformed payload. Permanence is the point and is dictated by the wire: SiteMinder reads an error response as a final decision, stops retrying, and emails the property. So this verb is only for faults that will still be faults on the tenth attempt. A transient problem must leave the delivery `failed` and unacknowledged instead, so redelivery can fix it. Emits `channel_reservation.rejected`."
        },
        {
          "api_name": "hospitality.action.restate_channel_reservation",
          "description": "Apply a partner's modification to a booking we already hold. A modification is a full restatement on every wire — SiteMinder re-send the whole reservation, Mews re-send the whole group and treat omission as cancellation — so this takes the stay as the partner now states it and moves ours to match, through the same date-change the front desk uses. Committed upstream, like `apply_channel_reservation`: the guest has already been told they can stay the extra night, so the selling ceiling and the restriction wall stop being gates and become facts to record. That is the whole reason this is a verb rather than a call to `modify_reservation_dates` — routing a channel restatement through the gated path would silently refuse exactly the extensions that matter, and leave the house expecting a guest to leave on a day the OTA has already sold them. Any breach lands on `availability_day.committed_oversell` and is reported on the delivery as `oversold_by`. Separate from `apply_channel_reservation` because the required sets differ: a restatement names the reservation it restates *and* the stay, where a booking names only the stay. A restatement that matches what we already hold is a no-op — which is the commonest case on a wire that re-sends everything. Restating the room type is not served here: a mid-stay room swap is a different unwind (see `docs/deferred.md`). Emits `channel_reservation.applied`."
        }
      ],
      "api_name": "hospitality.channel_reservation_delivery",
      "description": "One inbound reservation message from a partner, with its de-duplication and acknowledgement state. Every channel wire is at-least-once — SiteMinder re-pushes every minute for ten attempts then every five, Booking.com re-serves anything unacknowledged inside its pickup window — so duplicate delivery is the norm and `external_message_id` is the key that makes a replay a no-op. The message is a report of a sale that already completed on the partner's side, not a request: where inventory is short, this row records the oversell rather than refusing the booking.",
      "display_name": "Channel reservation delivery",
      "domain": "channel_manager",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Delivery id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "connection_ref",
          "description": "",
          "display_name": "Business id of the conduit this arrived on (scopes the de-duplication key)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "external_message_id",
          "description": "",
          "display_name": "The partner's identifier for this message (de-duplication key)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "external_reservation_id",
          "description": "",
          "display_name": "The partner's identifier for the reservation (stable across book/modify/cancel)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "intent",
          "description": "",
          "display_name": "What the message asks of us",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Delivery lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "received_at",
          "description": "",
          "display_name": "When the message reached us",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "applied_at",
          "description": "",
          "display_name": "When the message landed in the ontology",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "acknowledged_at",
          "description": "",
          "display_name": "When our acknowledgement was sent to the partner",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "origin_channel_code",
          "description": "",
          "display_name": "The partner's code for the demand channel that sold the booking",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payload_digest",
          "description": "",
          "display_name": "Digest of the verbatim partner payload held in raw staging",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "rejection_kind",
          "description": "",
          "display_name": "Why the message was permanently refused (state = rejected)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "rejection_detail",
          "description": "",
          "display_name": "Operator-facing detail behind the rejection",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "oversold_by",
          "description": "",
          "display_name": "Rooms the property went over its selling ceiling to accept this booking",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Free-form operator notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.reverse_charge",
          "description": "Take back a posted charge by minting an equal-and-opposite reversal charge, linked to the original via `charge_reverses_charge`. Full-amount only: amount, tax lines, and gratuity are mirrored negated, so the pair nets to zero in the folio balance and in every per-bucket / per-kind rollup. The original stands (`voided` stays false) — both lines show, which is how a corrected bill reads. The reversal lands on the charge's own folio while it is open; once sealed, it lands on the open rebate folio linked via `folio_rebates_folio` — a sealed bill within the reopen window goes through `reopen_folio` first, beyond it through `open_rebate_folio`. Refuses a voided charge, a reversal charge (never chained — reverse the original), and a package-attributed charge (night-audit reconciliation owns those). Re-applying to an already-reversed charge is a no-op."
        },
        {
          "api_name": "hospitality.action.transfer_charge",
          "description": "Transfer a charge from its current folio to a target folio. Emits a single event with both folio refs for atomic audit. Idempotent on (charge, target_folio, business_date)."
        }
      ],
      "api_name": "hospitality.charge",
      "description": "A line item posted to a folio. Carries a USALI 12 `bucket` (the operated department revenue rolls up to) plus either a `charge_for_service_item` link (when posted against the service catalog) or a `system_charge_kind` enum (when the charge is system-generated — a room night, tax line, service charge, gratuity, adjustment, package discount).",
      "display_name": "Charge",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Charge id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tax",
          "description": "",
          "display_name": "Tax",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "gratuity",
          "description": "",
          "display_name": "Gratuity",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "posted_at",
          "description": "",
          "display_name": "Posted at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "bucket",
          "description": "",
          "display_name": "USALI 12 department",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "system_charge_kind",
          "description": "",
          "display_name": "System-generated charge kind",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "outlet_id",
          "description": "",
          "display_name": "USALI 12 operated department (Schedule 3 or 4) the charge rolls up to. A synthetic `miscellaneous_income`-bucket outlet per property covers genuinely non-department revenue (interest, cancellation fees, lost-key charges).",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "transferred_from_folio_id",
          "description": "",
          "display_name": "Folio this charge was transferred from, when it did not originate on the folio it currently sits on.",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "package_attribution_kind",
          "description": "",
          "display_name": "Package-attribution kind",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "voided",
          "description": "",
          "display_name": "Voided",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for connector-landed charges",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "revenue_category",
          "description": "What an F&B posting sold (food / soft drinks / alcohol / other), for a charge a room-charged check posted — one per category on the check. Absent on every other charge.",
          "display_name": "F&B revenue category",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "signed_by",
          "description": "The guest who signed for a posting made at an outlet — the guest the server confirmed on the booking's bill — so the bill says who signed. Set by a room-charged F&B payment that named its guest; absent otherwise.",
          "display_name": "The guest who signed for it",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "device_ref",
          "description": "",
          "display_name": "The device a key-card tap was made at (set by `post_charge_by_credential`)",
          "kind": "reference",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.comp_check_line",
          "description": "Write off a FIRED or FULFILLED line — product was made and delivered, so this hits food cost and is excluded from the settled total. Carries a reason and the authorising staff member — whoever is signed in (the session's staff member, recorded as `comp_authorised_by`), never one named in the body: a body `authorised_by` must be that same staff member or the comp is refused (`comp_not_authenticated: …`); a caller that is no member of staff records none. When a `comp_authority_policy` row exists for (role, food_beverage) and the line value exceeds its per-incident limit, the line posts with `comp_escalation_required = true` rather than blocking service — the soft-escalation posture the policy object documents. The line's modifier lines are comped with it (their value counts toward the limit); a modifier line is never comped alone. The check's totals drop by what was comped. Emits `fnb.line_comped`."
        },
        {
          "api_name": "hospitality.action.void_check_line",
          "description": "Remove an ENTERED line — one that has not fired to the kitchen. No product was made: no COGS, no sale, no authority gate; the reason code is the control (Restaurant365 / Square discipline). A fired or fulfilled line cannot be voided — that is a comp (`comp_check_line`), which is a P&L event. The line's modifier lines are voided with it; a modifier line is never voided alone. The check's totals drop by what was voided. Emits `fnb.line_voided`."
        }
      ],
      "api_name": "hospitality.check_line",
      "description": "One ordered item on a guest check: menu item and variant, quantity, course, seat, unit price, revenue category and VAT snapshotted at order time, the kitchen's instructions, and the entered → fired → fulfilled lifecycle with the void/comp split: voided = removed BEFORE preparation (no COGS, no sale); comped = delivered but written off (product consumed — carries authority metadata). A modifier option that adds something sellable (add bacon) is a line of its own, linked to the line it modifies by check_line_modifies_line and priced and taxed as its own item; it follows its parent through fire, bump, void and comp and is never voided or comped alone.",
      "display_name": "Check line",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Line id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Line lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "quantity",
          "description": "",
          "display_name": "Quantity",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "course_number",
          "description": "",
          "display_name": "Course",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "seat_number",
          "description": "The seat the item is for, so the runner knows who gets which plate and a split by seat has its key. Absent = the table's (shared, or not taken).",
          "display_name": "Seat",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "unit_price",
          "description": "",
          "display_name": "Unit price snapshotted from the variant's price in force at order time",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "revenue_category",
          "description": "",
          "display_name": "Revenue category, from the item class at order time",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "tax_class",
          "description": "",
          "display_name": "Tax class the line's VAT resolved through (eat-in or takeaway)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "tax_amount",
          "description": "Total tax on quantity × unit price, each rule rounded to the penny half away from zero (never down — VAT Notice 700 §17.6). Inside the price when the rules are inclusive, on top when they add on.",
          "display_name": "Tax on the line",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tax_lines",
          "description": "One entry per tax rule applied — code, jurisdiction, rate, amount, inclusive — the per-rate figures a VAT receipt totals. Empty only when no rule applied, which a sale never does.",
          "display_name": "Tax breakdown",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "modifier_prefix",
          "description": "On a modifier line, the prefix its option was chosen with — `no` rings it free, `extra` another portion, `light` / `side` change how it is served. Absent on an item rung on its own, and on a modifier chosen plain.",
          "display_name": "Prefix the modifier was chosen with (modifier lines only)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "instructions",
          "description": "What the kitchen must do differently, in the order the modifiers were chosen: an option that adds nothing sellable (Medium rare), and a no / light / side prefix on any option (NO Onion, SIDE Dressing). Options that add something sellable are lines of their own.",
          "display_name": "Kitchen instructions",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Kitchen-facing note",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "void_reason",
          "description": "",
          "display_name": "Why the line was voided (required when voided)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "comp_reason",
          "description": "",
          "display_name": "Why the line was comped (required when comped)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "comp_authorised_by",
          "description": "",
          "display_name": "Staff member who authorised the comp — the one signed in when it was comped",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "comp_escalation_required",
          "description": "",
          "display_name": "Comp exceeded the authoriser's per-incident limit",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "fired_at",
          "description": "",
          "display_name": "When the line fired to the kitchen",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "fulfilled_at",
          "description": "",
          "display_name": "When the line's ticket bumped (or, for a connector-landed line, when service completed)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for replayed / connector-landed lines",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.check_payment",
      "description": "One tender taken against a guest check (check_payment_on_check) — the kernel is the system of record for a check's payments. `amount` is what it applied to the check, `tip` rides beside it and stays out of what was paid; cash may be `tendered` above the balance and gives `change_given`. A card payment carries the terminal's references (`psp_reference`, `terminal_transaction_id`, `poiid`), never card data. A room charge or charge-to-key posts to its folio (check_payment_to_folio) one charge per revenue category and the tip as a gratuity charge (check_payment_posted_charge). `allocation` records the payment's share of each revenue category and its VAT, so a check paid in parts sums exactly. Written only by add_check_payment / settle_check.",
      "display_name": "Check payment",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Payment id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "tender",
          "description": "",
          "display_name": "Tender",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Applied to the check (tip excluded)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tip",
          "description": "The tip taken with the payment — a pass-through to the staff under the Employment (Allocation of Tips) Act 2023, never revenue and never in `paid_total`. Zero when none.",
          "display_name": "Tip",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tendered",
          "description": "",
          "display_name": "Cash handed over (cash only)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "change_given",
          "description": "",
          "display_name": "Change handed back (cash only)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "psp_reference",
          "description": "",
          "display_name": "The processor's reference for the card payment (Adyen pspReference) — an identifier, not a credential",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "terminal_transaction_id",
          "description": "",
          "display_name": "The terminal's own transaction id for the card payment",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "poiid",
          "description": "",
          "display_name": "The terminal (point of interaction) the card was taken on",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "paid_at",
          "description": "",
          "display_name": "When the payment was taken — business time",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "recorded_by",
          "description": "",
          "display_name": "Principal::stable_id() of the principal that took the payment",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "staff_member_id",
          "description": "",
          "display_name": "The staff member who took the payment, when the session is one",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "reservation_id",
          "description": "The booking whose bill a room charge or charge-to-key posted to — the folio belongs to it (a stay's window or the booking's own account). OHIP posts to a reservation, never to a room.",
          "display_name": "The reservation a room charge posted to",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "guest_id",
          "description": "The guest the server confirmed for a room charge — the reservation's guest (a sharer signs on their own booking). Carried onto the folio charges as `signed_by`.",
          "display_name": "The guest who signed",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "allocation",
          "description": "Per revenue category, what of `amount` paid it and the tax inside that share, per rule — apportioned by largest remainder over what the earlier payments left, so a check's payments sum exactly to its categories and its VAT. A room charge posts these shares to the folio.",
          "display_name": "The payment's share of each revenue category",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "allowance_applied",
          "description": "What of the payment the stay's package allowance covered — the food and soft-drink share, capped at what the day's allowance had left, never the tip. The folio carries that part as the package's balance-neutral consumption line and only the rest as charges the guest owes; the consumption row names this payment (`consumption_for_check_payment`). Absent when nothing was covered.",
          "display_name": "Package allowance this room charge consumed",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for replayed / connector-landed payments",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "override_kind",
          "description": "",
          "display_name": "What a manager's override let through: the stay's no-post flag (no_post) or its credit (no_credit)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "authorised_by",
          "description": "",
          "display_name": "The manager whose say-so overrode the stay's posting rules — the staff member signed in when the payment was taken",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "override_reason",
          "description": "",
          "display_name": "Why the manager overrode the stay's posting rules",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.check_receipt",
      "description": "The guest's receipt for a settled check — issued when the check's balance reaches zero, numbered from the outlet's receipt series (`R-{outlet}-{000001}`) and chained by hash onto the property's issued documents, invoices and credit notes included. A snapshot of what a UK simplified invoice needs (VAT Notice 700 §16.6.1: supplier name, address and VAT number, time of supply, what was supplied, per-rate VAT-inclusive totals and rates) plus the check number, outlet, server, business date, lines, tenders, tips and change. Not a VAT invoice: no recipient. A room charge prints as charged to the room, with the guest's name and no card detail. Never edited after issue.",
      "display_name": "Check receipt",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "number",
          "description": "`R-{outlet code}-{000001}` — drawn from the outlet's dense receipt series inside the settling transaction and never reissued.",
          "display_name": "Receipt number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "series",
          "description": "The sequence the number came from — `{property_id}:receipt:{outlet_id}`: one run per outlet (a till prints its own), dense, and kept apart from the property's invoice and credit-note runs.",
          "display_name": "Number series",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "sequence_value",
          "description": "The bare counter value `number` was rendered from, so a gap is detectable without parsing the number.",
          "display_name": "Sequence value",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "check_number",
          "description": "The check's own number, the one the server says aloud.",
          "display_name": "Check number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "outlet_name",
          "description": "",
          "display_name": "Outlet",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "order_kind",
          "description": "",
          "display_name": "How the order reached the guest",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "table_label",
          "description": "",
          "display_name": "Table",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "covers",
          "description": "",
          "display_name": "Covers",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "server_name",
          "description": "The owning server's given name, as it printed.",
          "display_name": "Server",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "business_date",
          "description": "The check's business date — the hotel date it opened on.",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "settled_at",
          "description": "When the check settled — the time of supply the receipt prints (Notice 700 §16.6.1).",
          "display_name": "Time of supply",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "time_of_supply",
          "description": "`settled_at` in the house's own time zone, as the slip prints it (`2026-11-02 19:42`; ` UTC` appended when the property names no zone) — snapshotted so a render needs no clock and no zone database.",
          "display_name": "Time of supply, as printed",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "The instant the receipt was issued.",
          "display_name": "Issued at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "subtotal",
          "description": "The live lines' gross — comps and voids out, inclusive VAT in.",
          "display_name": "Subtotal",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tax_total",
          "description": "",
          "display_name": "VAT",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "total_due",
          "description": "What the guest owed: subtotal plus any add-on tax.",
          "display_name": "Total",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "paid_total",
          "description": "What the payments applied to the check, tips out.",
          "display_name": "Paid",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tip_total",
          "description": "Tips taken with the payments — outside the scope of VAT.",
          "display_name": "Tips",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "change_total",
          "description": "Cash handed back across the payments.",
          "display_name": "Change",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "vat",
          "description": "Per tax rule and rate over the live lines: the code, the rate, and the net, tax and VAT-inclusive gross — the per-rate figures Notice 700 §16.6.1 asks a simplified invoice to show.",
          "display_name": "VAT per rate",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "lines",
          "description": "What was supplied, in the order it rang: each fulfilled or comped line with its modifier lines after it (`modifier: true`). Voided lines were never made and are not printed; a comped line prints at its price, marked.",
          "display_name": "Lines",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "payments",
          "description": "Each tender in the order it was taken: amount, tip, cash tendered and change; a room charge with its room and the name of the guest who signed — never card data.",
          "display_name": "Payments",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "supplier_name",
          "description": "The house's name as the slip heads it.",
          "display_name": "Supplier",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "issuer",
          "description": "The supplier's legal identity as it printed — the property's legal name, address and VAT number from its billing identity; its own name and address under scheme `none` when it has no billing identity; absent when it has neither (a till never refuses a guest their slip over back-office configuration).",
          "display_name": "Issuer",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "simplified",
          "description": "Always true: a till receipt is the retailer's simplified invoice, issued without the customer's details.",
          "display_name": "Simplified",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "content_hash",
          "description": "SHA-256 over the receipt's canonical fields (the invoice's field list: issuer tax reference, series, number, business date, kind `receipt`, tax, total) and `previous_hash`.",
          "display_name": "Content hash",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "previous_hash",
          "description": "The property's previous issued document's `content_hash` — invoice, credit note or receipt: one chain per property. Absent only on the property's first document.",
          "display_name": "Previous content hash",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.checklist_result",
      "description": "One recording of a checklist template against one housekeeping task (`checklist_result_for_task`, `checklist_result_for_template`): the answered items, the recorder's remarks, who ticked it (`checklist_result_recorded_by`) and when. `score` is derived, never typed: the template's `check` items ticked ok ÷ all its `check` items, × 100, rounded half up — `checks_passed` and `checks_total` are its evidence; a template with no `check` items scores nothing. Photo items' pictures hang off the result as attachments (`stored_object_for_checklist_result`). One result per task × template: recording again replaces it in place while the task is open. A supervisor's inspection list is the same object recorded against the inspection task.",
      "display_name": "Checklist result",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Checklist result id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "items",
          "description": "",
          "display_name": "The answered items, by template item code",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "remarks",
          "description": "",
          "display_name": "The recorder's remarks on the whole list",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "checks_total",
          "description": "",
          "display_name": "How many of the template's items are `check` items",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "checks_passed",
          "description": "",
          "display_name": "How many of those were ticked ok",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "score",
          "description": "",
          "display_name": "checks_passed ÷ checks_total × 100, rounded half up; absent when the template has no check items",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "recorded_at",
          "description": "",
          "display_name": "When the list was ticked (the handset's tap, when it says)",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_checklist_template",
          "description": "Stop a checklist template resolving onto tasks — and so stop it gating their completion. Results already recorded against it stay. Retiring a retired template is a no-op. Emits `checklist_template.retired`."
        }
      ],
      "api_name": "hospitality.checklist_template",
      "description": "The list an attendant ticks — or a supervisor inspects against — for one housekeeping task type, optionally one room type (`checklist_template_for_room_type`). A brand standard (`checklist_template_at_brand`) or the house's own (`checklist_template_at_property`); a task resolves the most specific active template: the house's for its room type, the house's for every room, the brand's for its room type, the brand's for every room. `required_for_completion` makes `complete_housekeeping_task` refuse until a `checklist_result` for this template is recorded against the task — hubOS's 'checklist for inspection' building toggle, per list instead of per building. One active template per (scope, task type, room type): re-defining one revises its items in place. A public-area task type's list may name a kind of area instead (`applies_to_area_kind`, 2026-10-14): an area's task resolves the list for its kind before the list for every area, at the house and then the brand.",
      "display_name": "Checklist template",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Checklist template id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "What the list is called on the handset",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "applies_to_task_type",
          "description": "",
          "display_name": "The housekeeping task type the list is for",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "applies_to_area_kind",
          "description": "",
          "display_name": "The kind of public area the list is for (on a public-area task type only); absent: every area, or a room's list",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "items",
          "description": "",
          "display_name": "The items, in the order the attendant meets them",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "required_for_completion",
          "description": "",
          "display_name": "Whether the task cannot complete without a recorded result",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Active (resolves onto tasks) or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the template was retired",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.checkpoint_result",
      "description": "What one checkpoint of an inspection round recorded (`checkpoint_result_in_round`) — hubOS's revision chapter. The checkpoint's `checkpoint_position` and `kind` in the round, the room (`checkpoint_result_for_room`) or equipment (`checkpoint_result_for_asset`) it is at — neither for a general place — the answered `items`, the walker's `observations`, when it was started and finished (its duration is the difference), and who recorded it (`recorded_by`; `checkpoint_result_recorded_by` when a staff member did). `anomalies` is derived, never typed: the items answered not ok. `faults` counts the work orders raised from it (`work_order_from_checkpoint`). One result per round × checkpoint: recording the checkpoint again replaces it in place while the round is open. Its id may be the walker's own (a v4 or v7 UUID minted on the handset), so a photo taken with no signal waits for it. Photos hang off it as attachments; a checkpoint's `photo` items are answered by them, and the round does not complete without them.",
      "display_name": "Checkpoint result",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Checkpoint result id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "checkpoint_position",
          "description": "",
          "display_name": "The checkpoint's position in the round",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "room, asset or general — the checkpoint's kind in the round",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "items",
          "description": "",
          "display_name": "The answered items, by the checkpoint's item codes",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "observations",
          "description": "",
          "display_name": "What the walker noticed at the checkpoint",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "anomalies",
          "description": "",
          "display_name": "Items answered not ok — derived from the items",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "faults",
          "description": "",
          "display_name": "Work orders raised from this checkpoint",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the walker began the checkpoint",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "When the checkpoint was finished (the handset's own time when it says)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "recorded_by",
          "description": "",
          "display_name": "The principal that recorded it (`user:…`, `agent:…`, `service:…`, `system`)",
          "kind": "string",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.update_coaching_plan",
          "description": "Update the target coaching plan. Operators update coaching plans in coherent passes (a check-in cycle reviewing current focus, next review date, and protocol narrative together) rather than per-field; the action mirrors that operational shape."
        }
      ],
      "api_name": "hospitality.coaching_plan",
      "description": "The non-clinical operational counterpart to `wellness_protocol`. A narrative trajectory across multiple protocols, lifestyle work, and check-in cadence. Distinct from the protocol because the protocol is a typed modality-pattern prescription and the coaching plan is the broader member trajectory across them.",
      "display_name": "Coaching plan",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Coaching plan id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When coaching began",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "coach_text",
          "description": "",
          "display_name": "Coach identifier (free text — practitioner may not be staff)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "coach_staff_ref",
          "description": "",
          "display_name": "Coach (typed reference when staff)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "current_focus_text",
          "description": "",
          "display_name": "Current focus (member's working priority this cycle)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "next_review_date",
          "description": "",
          "display_name": "Next review date (typically 4 / 8 / 12-week cycle)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "protocol_summary_text",
          "description": "",
          "display_name": "Operator's running narrative across the member's protocols",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "is_active",
          "description": "",
          "display_name": "Currently active",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.commission_agreement",
      "description": "Time-varying commission contract between a property and a travel agent, tour operator, wholesaler or consortium. Carries the rate, kind and flat amount in effect for the period, plus the minimum the agent will accept a payment for. Only one row per (company, property) may be open at a time; binding a new one closes the last.",
      "display_name": "Commission agreement",
      "domain": "commission",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Agreement id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "Effective from",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Effective to (null = currently in effect)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "commission_kind",
          "description": "",
          "display_name": "Commission kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "commission_rate",
          "description": "",
          "display_name": "Commission rate (decimal share of commissionable revenue)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "commission_flat_amount",
          "description": "",
          "display_name": "Per-booking flat commission",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "minimum_payment_amount",
          "description": "",
          "display_name": "Do not pay this agent below this amount",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "tax_treatment",
          "description": "",
          "display_name": "VAT posture of this agent's commission",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Contract notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.adjust_commission",
          "description": "Revise what a commission row owes, keeping the previous figure on the row. Refused once the row has been paid or otherwise settled: a paid commission is corrected by a new row in the next run, never by restating history."
        },
        {
          "api_name": "hospitality.action.hold_commission",
          "description": "Park a commission row so a payment run passes it over — a disputed stay, an agent under review, a figure someone wants to check. Holding is reversible and records why."
        },
        {
          "api_name": "hospitality.action.release_commission_hold",
          "description": "Return a held commission row to `accrued`, where the next payment run will see it."
        },
        {
          "api_name": "hospitality.action.settle_commission_without_payment",
          "description": "Close a commission row without sending money. `prepaid` means the guest already paid the agent net, so paying again would pay twice; `offset_to_ar` settles it against what the agent already owes us; `reconciled` is how a channel row ends when the channel's own invoice arrived and matched; `voided` is for a row accrued in error."
        }
      ],
      "api_name": "hospitality.commission_detail",
      "description": "What one completed stay owes one payee. `settlement_mode` says whether this is a payable the hotel initiates (a travel agent) or an expectation to reconcile against an invoice the payee will send us (an agency-model OTA) — the two look identical on a reservation and must never be paid the same way. The rate and policy that produced the amount are frozen on the row, so a later renegotiation cannot restate a historical liability.",
      "display_name": "Commission detail",
      "domain": "commission",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Detail id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Detail status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "payee_kind",
          "description": "",
          "display_name": "Whether the payee is a company or a channel",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "settlement_mode",
          "description": "",
          "display_name": "Hotel-initiated payable, or an invoice to reconcile",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "accrued_on",
          "description": "",
          "display_name": "Business date the stay completed",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "commissionable_revenue",
          "description": "",
          "display_name": "Revenue the commission was computed on, in property currency",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "commission_amount",
          "description": "",
          "display_name": "Amount owed to the payee",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "commission_kind",
          "description": "",
          "display_name": "Commission kind in force at accrual",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "commission_rate",
          "description": "",
          "display_name": "Commission rate in force at accrual",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "policy_ref",
          "description": "",
          "display_name": "Agreement or channel period the figures were derived from",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "tax_treatment",
          "description": "",
          "display_name": "VAT posture of this amount",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "hold_reason",
          "description": "",
          "display_name": "Why the row is held back from payment",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "settled_reason",
          "description": "",
          "display_name": "Why the row settled without a payment",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "adjusted_from_amount",
          "description": "",
          "display_name": "Amount before the operator adjusted it",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.commission_payment_batch",
      "description": "One payment run over approved commission details. Paying the batch freezes its rows; voiding it releases them back to `approved` for a later run. `settlement_network` records that the batch went to a clearinghouse without this kernel pretending to speak their file format.",
      "display_name": "Commission payment batch",
      "domain": "commission",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Batch id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Batch status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "method",
          "description": "",
          "display_name": "How the batch paid out",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "total_amount",
          "description": "",
          "display_name": "Total paid",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "detail_count",
          "description": "",
          "display_name": "How many details the batch settled",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "reference",
          "description": "",
          "display_name": "Cheque number, transfer reference or clearinghouse batch id",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "paid_at",
          "description": "",
          "display_name": "When the batch paid",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.comp_authority_policy",
      "description": "Property-scoped comp-authority row keyed on (role, comp_category). The recovery handler resolves staff authority by looking up policy rows; the cache on `staff_member.comp_authority_summary` is the hot-path read.",
      "display_name": "Comp authority policy",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Policy id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "role",
          "description": "",
          "display_name": "Role",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "comp_category",
          "description": "",
          "display_name": "Comp category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "limit_per_incident",
          "description": "",
          "display_name": "Limit per incident",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "limit_per_day",
          "description": "",
          "display_name": "Limit per day",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "limit_per_stay",
          "description": "",
          "display_name": "Limit per stay",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "requires_escalation_above",
          "description": "",
          "display_name": "Escalation threshold (soft)",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.execute_management_agreement",
          "description": "Execute an HMA with the target owner company (kind = hotel_owner): creates the `management_agreement` carrying the fee schedule, delegation thresholds and performance-test parameters, linked to the owner. Covered properties join via `add_property_to_management_agreement`. `external_ref` is the idempotency anchor for provisioning replays."
        },
        {
          "api_name": "hospitality.action.open_company_ar_folio",
          "description": "Open the company's A/R account at a property — a `company_ar` folio opened at zero in the given currency, linked through `company_has_ar_folio`. The desk direct-bills a guest folio by taking a `direct_billing` payment on it and posting the receivable here; `settle_folio_to_ar` records the transfer and its due date. A company that already has an open account at the property in that currency is a no-op. Emits `company.ar_folio_opened`."
        },
        {
          "api_name": "hospitality.action.update_company_billing",
          "description": "Edit the target company's billing party: billing address, tax identifier (the VAT number that makes the document usable for their input-tax recovery), registration number and invoicing email — accounts payable, never whoever made the booking. Every parameter is optional and at least one must be passed; omitted fields keep their values. Empty text clears the three scalars, and an address whose every field is blank clears the address; an address that is not blank must carry line 1, city and country. A call whose values all match is an idempotent no-op. The company's code, name and kind are not editable here — those decide who the account *is*, and a rename is not a billing correction. Emits `company.billing_updated` with the changed field names."
        }
      ],
      "api_name": "hospitality.company",
      "description": "A corporate or trade account: corporate negotiated, travel agent, tour operator, wholesaler, group bookers. Used for billing, channel routing, and rate eligibility.",
      "display_name": "Company",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Company code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Company name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Company kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "billing_address",
          "description": "",
          "display_name": "Billing address",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "iata_number",
          "description": "",
          "display_name": "IATA number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "tax_identifier",
          "description": "The company's VAT / tax registration number, as printed on an invoice addressed to it. This is what makes the document usable for their input-tax recovery, which is the whole reason a company wants one.",
          "display_name": "Tax identifier",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "registration_number",
          "description": "Companies-House-style registration number.",
          "display_name": "Company registration number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "invoicing_email",
          "description": "Where this company's invoices are sent — accounts payable, not the booker. Mews keeps the same field for the same reason: a company bill goes to the company's invoicing email, never to whoever made the reservation.",
          "display_name": "Invoicing email",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.cut_over_connector",
          "description": "Cutover night's one decision: the house stops treating the source PMS behind this connector as its system of record. Pauses the connector (`paused_reason` names the cutover; the worker starts no further syncs), stamps `cut_over_at` / `cut_over_by` / `cut_over_summary` on it, and resumes every named `channel_connection` from `paused` to `active` so the dispatcher publishes Aerandil's ARI to the channel manager from the next pass — an already-active conduit is left as it is; one still `pending_mapping`, `suspended` or `closed` refuses the whole gesture, because a cutover onto a conduit that cannot sell is a cutover onto nothing. In-house guests are not re-keyed: the lifecycle streams already checked them in. Internal-runner — invoked only by the switchover route after the shadow comparison reads clean; `preflight` is that comparison's summary, recorded verbatim. Idempotent on a connector already cut over. Emits `connector.cut_over`."
        },
        {
          "api_name": "hospitality.action.pause_connector",
          "description": "Pause an `active` connector — the ingest worker stops starting syncs for it. Reversible (`resume_connector`); `reason` is required and lands on `paused_reason` so the Explorer shows *why* the feed is dark (credential rotation, vendor maintenance). Emits `connector.paused`."
        },
        {
          "api_name": "hospitality.action.repoint_connector_mapping",
          "description": "Point one of a connector's streams at a different mapping definition version (`name@version`) — the `apaleo_charge@2` upgrade path, or a rollback to a prior version after a bad mapping ships. Upserts by mapping *name*: an existing entry for that name moves to the new version, and a name the connector does not yet carry is added, which is how a stream joins a live pipe. Explicit operator move, never implicit: in-flight runs are unaffected (each run pins its `mapping_ref` at start), previously rejected rows are retried under the new mapping on the next sync, and already-loaded rows are re-mapped only when their payload changes — re-processing history is a separate, deliberate backfill. Allowed while active or paused; refused on a retired connector. Emits `connector.mapping_repointed` with the from → to refs."
        },
        {
          "api_name": "hospitality.action.resume_connector",
          "description": "Resume a `paused` connector — clears `paused_reason` and returns it to `active` so the worker schedules syncs again. Emits `connector.resumed`."
        },
        {
          "api_name": "hospitality.action.retire_connector",
          "description": "Retire a connector — terminal; the property stopped using the source system. The row and its sync-run history stay (soft-delete is the ontology-wide contract; lineage on imported objects keeps pointing here). Emits `connector.retired`."
        },
        {
          "api_name": "hospitality.action.roll_back_connector_cutover",
          "description": "The cutover reversed, as safe as the cutover itself: the connector returns to `active` so the worker syncs from the source PMS again, `cut_over_at` / `cut_over_by` / `cut_over_summary` are cleared (the events keep the history), and every conduit the cutover brought live is paused with a reason naming the rollback, so the channel manager is fed by the source PMS once more. Refused on a connector that was never cut over or has been retired. Internal-runner — invoked by the switchover route, which also reports what was booked in Aerandil since the cutover and so is not in the source. Emits `connector.cutover_rolled_back`."
        },
        {
          "api_name": "hospitality.action.start_connector_sync",
          "description": "Open one audited sync run against an `active` connector — creates a `connector_sync_run` in `running` and links it. Called by the ingest worker on schedule, or manually from the Explorer (trigger = manual / backfill). A run brackets one stream, so `mapping_ref` pins which mapping version it loaded under; it defaults to the connector's only mapping when it carries exactly one, and must be given explicitly on a multi-stream connector (there is no principled default among several). Emits `connector.sync_started`."
        }
      ],
      "api_name": "hospitality.connector",
      "description": "The registered instance of one external source-system feed into the ontology (a property's PMS, POS back-end, rostering tool). Identity + lifecycle only: credentials live in the secret store (`secret_ref` is a name, never a value), sync cursors live in the raw staging schema, and the mapping definitions are versioned repo files (`mapping_refs`, one name@version per vendor stream). Lifecycle mutations (pause / resume / retire / start sync) are Actions, so the who-and-when is in the immutable events audit trail.",
      "display_name": "Connector",
      "domain": "connectors",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Connector id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "display_name",
          "description": "",
          "display_name": "Human-readable name shown in the Explorer",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "source_kind",
          "description": "",
          "display_name": "Category of source system",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "provider",
          "description": "",
          "display_name": "Vendor identifier (mews / apaleo / ohip / …)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "mapping_refs",
          "description": "",
          "display_name": "Mapping definition references (name@version per vendor stream, versioned repo files)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Connector lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "secret_ref",
          "description": "",
          "display_name": "Name of the credential in the external secret store (never the value)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "vendor_config",
          "description": "",
          "display_name": "Non-secret vendor settings (base URL, site codes, streams)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "schedule",
          "description": "",
          "display_name": "Intended sync cadence, cron form (informational; the worker owns scheduling)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "registered_by",
          "description": "",
          "display_name": "Principal::stable_id() of the registering principal",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "registered_at",
          "description": "",
          "display_name": "When the connector was registered",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "paused_reason",
          "description": "",
          "display_name": "Why the connector is paused (required while status = paused)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Free-form operator notes",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cut_over_at",
          "description": "",
          "display_name": "When the house cut over from this source (absent while shadowing)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cut_over_by",
          "description": "",
          "display_name": "Principal::stable_id() of who took the cutover decision",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "cut_over_summary",
          "description": "",
          "display_name": "The shadow comparison the cutover was taken on (counts, channel connections activated)",
          "kind": "json",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.complete_connector_sync",
          "description": "Close a `running` sync run with its outcome and counts: records pulled from the source, landed in raw staging, and rejected by ontology validation at the map/load hop. `error_summary` is required when the outcome is `failed`. `cursor_window` records the per-stream from→to window for audit; the live cursors the next run resumes from stay in the staging schema. Emits `connector.sync_completed`."
        }
      ],
      "api_name": "hospitality.connector_sync_run",
      "description": "One audited extraction/load run of a connector: what triggered it, when it ran, how many records were pulled from the source, landed in staging, and rejected by ontology validation at the map/load hop. Run-grain only — per-record state stays in the raw staging schema. The rejected count is customer-facing by design: 'your POS sent 14 rows the ontology refused' is part of the trust story (0037 §8).",
      "display_name": "Connector sync run",
      "domain": "connectors",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Sync run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Sync-run lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "trigger",
          "description": "",
          "display_name": "What initiated this run",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the run started",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "finished_at",
          "description": "",
          "display_name": "When the run reached a terminal status",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "streams",
          "description": "",
          "display_name": "Source streams this run pulled (vendor vocabulary)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "mapping_ref",
          "description": "",
          "display_name": "Mapping definition version used (defaults to the connector's at start)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "records_pulled",
          "description": "",
          "display_name": "Records extracted from the source system",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "records_landed",
          "description": "",
          "display_name": "Records upserted into raw staging",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "records_rejected",
          "description": "",
          "display_name": "Records refused by ontology validation at the map/load hop",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "error_summary",
          "description": "",
          "display_name": "Terminal failure summary (required when status = failed)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cursor_window",
          "description": "",
          "display_name": "Per-stream cursor window this run covered (summary; live cursors live in staging)",
          "kind": "json",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.consent_policy",
      "description": "Tenant-scoped, versioned consent policy. Authorises the `ConsentScope` envelopes embedded on objects and events: its `default_scope` is the deny-by-omission grant applied to any object without its own scope, `purposes_grantable` bounds what a scope may grant, and `min_cohort_size` is the kernel-level k-anonymity floor enforced on derived inference outputs. Versioned per the monotonic-version contract; `verified_by` records DPO sign-off.",
      "display_name": "Consent policy",
      "domain": "consent",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Policy id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "version",
          "description": "",
          "display_name": "Monotonic policy version (a new grant strictly increases it)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "default_scope",
          "description": "",
          "display_name": "Default `ConsentScope` applied to objects without their own scope",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "purposes_grantable",
          "description": "",
          "display_name": "The purposes this policy is permitted to grant",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "min_cohort_size",
          "description": "",
          "display_name": "Kernel-level k-anonymity floor (AWS Clean Rooms default: 5)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "When this policy version takes effect",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "effective_until",
          "description": "",
          "display_name": "When this policy version is superseded",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "authored_by",
          "description": "",
          "display_name": "Principal::stable_id() of the author",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "verified_by",
          "description": "",
          "display_name": "Principal::stable_id() of the DPO sign-off, when verified",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "policy_text_uri",
          "description": "",
          "display_name": "Opaque pointer to the human-readable policy document",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "regulatory_basis",
          "description": "",
          "display_name": "Regulatory regime this policy is written under (GDPR / CCPA / UK DUA …)",
          "kind": "string",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.currency_conversion_rate",
      "description": "A tenant-authored, effective-dated FX rate for converting amounts from one currency to another (units of to_currency per 1 from_currency). Consumers use the latest effective_from at or before the transaction date for the exact directed pair; inverse rates are never derived.",
      "display_name": "Currency conversion rate",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Rate row id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "from_currency",
          "description": "",
          "display_name": "Currency being converted from",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "to_currency",
          "description": "",
          "display_name": "Currency being converted to",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "rate",
          "description": "",
          "display_name": "Units of to_currency per 1 from_currency; must be positive",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "First business date this rate applies to",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "source_note",
          "description": "",
          "display_name": "Where the rate came from (treasury feed reference, manual note)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.demand_forecast",
      "description": "One demand-forecast point: for a property and an arrival night, as of an instant, the rooms and ADR a segment is expected to realise, and — when the forecaster estimated it — the unconstrained demand behind them (`demand_rooms`, which may exceed capacity) and the rooms on the books when the forecast was made. Immutable: a re-forecast is a new point. Every point recorded feeds `forecast_point`, so the night-audit realization sweep grades it against the actual once the night has matured.",
      "display_name": "Demand forecast",
      "domain": "revenue_management",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Forecast point id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "forecast_as_of",
          "description": "",
          "display_name": "When the forecast was made (business time)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "arrival_date",
          "description": "",
          "display_name": "The arrival night forecast",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "segment",
          "description": "",
          "display_name": "Market segment forecast, or `total` for the house as a whole",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Who produced the forecast",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "forecast_rooms",
          "description": "",
          "display_name": "Rooms expected to be sold on the night",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "forecast_adr",
          "description": "",
          "display_name": "Average daily rate expected on the night",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "demand_rooms",
          "description": "",
          "display_name": "Unconstrained demand in rooms, when estimated (may exceed capacity)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "on_the_books",
          "description": "",
          "display_name": "Rooms already on the books for the night when the forecast was made",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "How the forecast was made",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Replay key for the forecasting source (unique per tenant while present)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.end_deposit_policy",
          "description": "Close (or shorten) a deposit policy's effective window: sets `effective_to`. Refuses a date before the policy's effective_from, and refuses a window that would overlap a later policy in the same code's succession. A gap in the timeline is legal — a no-deposit window is a legitimate commercial posture, and the authoring event records who opened it."
        }
      ],
      "api_name": "hospitality.deposit_policy",
      "description": "One property-authored deposit rule: how much of the stay is due as an advance deposit (flat / percent of stay / percent of first night / nights), when it falls due (days after booking or days before arrival), whether an unpaid deposit auto-cancels the reservation at End of Day, and an effective-date window. Reservation creation derives `deposit_required_amount` / `deposit_due_at` from the rule active on the booking date and freezes the ref; rules sharing a `code` form a non-overlapping succession timeline.",
      "display_name": "Deposit policy",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Deposit policy id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Policy code — the rule's identity within the property; rules sharing a code form one succession timeline",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "basis",
          "description": "",
          "display_name": "How the required deposit computes",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "rate",
          "description": "",
          "display_name": "Rate for the percent bases (decimal in [0, 1]; 0.50 = 50%). Required on percent bases, forbidden otherwise",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "flat_amount",
          "description": "",
          "display_name": "Amount for the `flat` basis. Required on flat, forbidden otherwise",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "nights",
          "description": "",
          "display_name": "Night count for the `nights` basis (deposit = that many nights at the quoted rate). Required on nights, forbidden otherwise",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "due_basis",
          "description": "",
          "display_name": "Which anchor the due date is measured from",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "due_days",
          "description": "",
          "display_name": "Day offset from the due anchor (>= 0)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "auto_cancel_on_lapse",
          "description": "",
          "display_name": "Whether the night audit cancels a reservation whose deposit is still unpaid past the due date (OPERA's reservation-type auto-cancel flag). When false the sweep records the lapse and leaves the booking for the operator",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "First booking date the rule applies (inclusive)",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Last booking date the rule applies (inclusive); absent = open-ended",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing description",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.derived_rate",
      "description": "Materialised rate computation snapshot. Resolves a rate_plan + rate_per_night BAR anchor + derivation into a concrete effective_rate at a moment in time. Immutable after write per design-spec §B.2 — supersedence is via a fresh row with `superseded_at` set on the prior row.",
      "display_name": "Derived rate",
      "domain": "distribution",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Derived rate id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_at",
          "description": "",
          "display_name": "When this snapshot was computed",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "effective_rate",
          "description": "",
          "display_name": "Computed rate",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "bar_anchor_amount_at_compute",
          "description": "",
          "display_name": "BAR anchor in effect at compute time (snapshot)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "derivation_shape",
          "description": "",
          "display_name": "Derivation shape at compute time",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "modifier_value",
          "description": "",
          "display_name": "Modifier applied to the anchor (multiplier or absolute delta)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "superseded_at",
          "description": "",
          "display_name": "Superseded by a later snapshot at",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.design_dna",
      "description": "A visual / spatial / service-tonal identity that connects venue brands or properties — the Sydell hotel-as-curator aesthetic across NoMad / Ace / LINE; the Soho House material palette and lighting language across House restaurants and Cowshed retail. Reused via N:N links (`property_has_design_dna`) and N:1 from venue brand (`venue_brand_has_design_dna`).",
      "display_name": "Design DNA",
      "domain": "brand_venue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Design DNA code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Design DNA name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "material_palette",
          "description": "",
          "display_name": "Material palette",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "typography_family",
          "description": "",
          "display_name": "Typography family",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "photography_direction",
          "description": "",
          "display_name": "Photography direction",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "service_register_default",
          "description": "",
          "display_name": "Service register default (mirrors proposal 0002 SURFACE_REGISTERS)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "lead_designer_ref",
          "description": "",
          "display_name": "Lead designer / firm (typed reference to a `company` with `kind = designer`)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "creative_director",
          "description": "",
          "display_name": "Creative director",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "house_style_guide_url",
          "description": "",
          "display_name": "House style guide URL",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.close_desk_session",
          "description": "End the shift when the desk signs out. Sets status closed and `closed_at`; a closed session mints no further token. Idempotent: closing a closed session is a no-op without an event. Emits `desk_session.closed` on the transition."
        },
        {
          "api_name": "hospitality.action.record_desk_step_up",
          "description": "Record that the person at the desk re-proved their passcode — unlocking an idle desk, or stepping up before a sensitive verb. Refuses a closed or spent session and anyone but the person acting. Stamps `last_proven_at` and emits `desk_session.stepped_up` with the instant the proof is honoured until, under that person's principal."
        },
        {
          "api_name": "hospitality.action.switch_desk_session_staff",
          "description": "Hand an open shift to another member of the property's staff, who has just proved their passcode on the warm session — the desk changes hands without anyone signing out. Refuses a closed or spent session, an inactive employee, one from another property, and the person already acting (that is a step-up, not a switch). Re-points `desk_session_acting_staff`, stamps `last_proven_at`, and emits `desk_session.switched` (from, to) under the incoming person's own principal: the hand-over is theirs to answer for."
        }
      ],
      "api_name": "hospitality.desk_session",
      "description": "One shift at the front desk: opened when a member of staff signs in through the identity provider, bound to the property they work at, and handed between that property's desk staff by passcode without signing out. Every hand-over (`desk_session.switched`) and every re-proof of a passcode before a sensitive verb (`desk_session.stepped_up`) is an event on it, under the acting person's own principal. Holds no credential: the bearer tokens are the kernel's, minted per person and bounded by `shift_ends_at`.",
      "display_name": "Desk session",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Session reference",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "Opened at (UTC)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "shift_ends_at",
          "description": "",
          "display_name": "Shift ends at (UTC) — no token minted for the session outlives it",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_proven_at",
          "description": "",
          "display_name": "When the person at the desk last re-proved their passcode",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "hands_changed_at",
          "description": "",
          "display_name": "When the desk last changed hands — opened, or a colleague switched in. A step-up on the same person's session does not move it; bearers minted before it are superseded",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "closed_at",
          "description": "",
          "display_name": "Closed at (UTC)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "outlet_id",
          "description": "Set when the session is a till at one of the property's outlets rather than the front desk: a till is held and handed on by the F&B floor's roles (server, F&B manager), the front desk by the front office's, never the one by the other. Absent = the front desk.",
          "display_name": "The outlet whose till the session is",
          "kind": "reference",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.development_pathway_definition",
      "description": "Operator-authored progression ladder for a discipline. `staff_member.pathway_stage_code` references a `code` from the matching definition's `stages` array.",
      "display_name": "Development pathway definition",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Definition id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "discipline",
          "description": "",
          "display_name": "Discipline",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "stages",
          "description": "",
          "display_name": "Ordered pathway stages",
          "kind": "array",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.set_device_status",
          "description": "Move a device between `active`, `inactive` (benched — a seasonal terminal in a drawer), and `retired` (terminal state; a retired device cannot come back, register a new one). Only `active` devices may attribute writes. Emits `access.device_status_set.recorded`."
        }
      ],
      "api_name": "hospitality.device",
      "description": "A registered piece of local hardware at a property — the attribution and authorisation anchor for writes that originate from a physical machine rather than a person. The first consumer is the charge-to-key surface: `post_charge_by_credential` requires an `active` payment-terminal device, so a tap from an unregistered reader fails closed. `kind` reserves vocabulary for the rest of the hardware estate (key encoders, door locks, kiosks, printers) so the abstraction doesn't churn per product.",
      "display_name": "Device",
      "domain": "access",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Device code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Device name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Hardware kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "vendor",
          "description": "",
          "display_name": "Hardware / acquirer vendor (free text — vendor-specific)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "vendor_device_ref",
          "description": "",
          "display_name": "Vendor-side device identifier (free text — vendor-specific)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "print_secret_hash",
          "description": "A printer's poll credential: the lowercase hex SHA-256 of the secret its Server Direct Print settings carry. Set by `register_device`; never readable.",
          "display_name": "Print secret hash (hex SHA-256)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.dispute_evidence_packet",
      "description": "The evidence the kernel assembled to counter one chargeback: the text fields it would send, the documents it would attach (each pinned to the source object's id and version), and the evidence the argument wanted but the house could not supply. Built from what the kernel already holds — folio, frozen cancellation terms, stay record, 3DS outcome — so nobody at the property gathers or uploads anything. A person counters or accepts; the one submission the kernel makes itself is a packet with no gaps still unsent 48 hours before the deadline (move 13, 2026-10-22: the night audit's sweep, `submitted_by = system`).",
      "display_name": "Dispute evidence packet",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Packet id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "template",
          "description": "",
          "display_name": "Which argument the packet makes — chosen from the dispute reason and what happened to the booking. Recorded so a later change to the templates cannot rewrite what this packet said.",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "packet_version",
          "description": "",
          "display_name": "1 for the first assembly on a case; each re-assembly bumps it and supersedes the unsent packet before it",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "assembled_at",
          "description": "",
          "display_name": "Assembled at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "fields",
          "description": "",
          "display_name": "The text evidence, keyed by the processor's evidence name (`customer_name`, `service_date`, `cancellation_policy_disclosure`, …). Generated from the objects, never written freehand, so every sentence can be checked against the packet's own sources.",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "documents",
          "description": "",
          "display_name": "The files the packet attaches, one per evidence type, each naming the object and version it is rendered from",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "gaps",
          "description": "",
          "display_name": "Evidence the argument wanted and the kernel could not supply — shown to the desk before it counters, so a weak case is visible as one",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "submitted_at",
          "description": "",
          "display_name": "When the processor took the packet to the issuer",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "gaps_acknowledged",
          "description": "",
          "display_name": "Whether the house submitted knowing the packet's gaps — required when there are any",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "gaps_acknowledged_by",
          "description": "",
          "display_name": "Who acknowledged the gaps and submitted (the principal)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "submitted_by",
          "description": "",
          "display_name": "Who sent the packet to the processor: the principal, or `system` when the night audit's sweep submitted a gap-free packet 48 hours before the deadline",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "refusal",
          "description": "",
          "display_name": "The processor's words when it refused this packet — a refused packet is not sent again; assembling another is the retry",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "files",
          "description": "",
          "display_name": "The files uploaded for this packet, one per evidence type, as the processor's file ids — recorded when the packet is marked `sending`, before the dispute update",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "sending_at",
          "description": "",
          "display_name": "When the packet's dispute update was sent to the processor (its idempotency key is remembered for 24 hours from then)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "enhanced_evidence",
          "description": "Visa Compelling Evidence 3.0, as the processor takes it (Stripe `evidence[enhanced_evidence][visa_compelling_evidence_3]`): the disputed booking's identifiers and two prior undisputed charges on the same card, 120 to 364 days before it, each matching the disputed one on two of the purchase IP, the device and the guest's email (at least one of them the IP or the device). Each prior names the payment and version it was read from, checked again before the packet is sent. Absent when the case is not Visa card-absent fraud, when the processor's own prior transactions already qualify, or when two were not found (a gap says how many were).",
          "display_name": "Visa CE 3.0 section",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "page_count",
          "description": "",
          "display_name": "The pages the packet's documents came to when they were rendered for the processor — the networks' limit is counted on these (fewer than 50 in all; 19 for Mastercard)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "byte_count",
          "description": "",
          "display_name": "The bytes the packet's documents came to when they were rendered for the processor (4.5 MB at most in all)",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.decide_erasure_request",
          "description": "Record the reviewer's decision on a pending erasure request: approved (the hard delete was performed; `links_removed` records the cascade) or rejected (e.g. an Art. 17(3) retention carve-out applies). Single-shot — only a pending request can be decided."
        }
      ],
      "api_name": "hospitality.erasure_request",
      "description": "Durable audit record for an Article 17 right-to-erasure demand: the target object, the requester, the reviewer's decision, and what the hard delete removed. Outlives the object it targets — the target row is erased, the compliance trail is not.",
      "display_name": "Erasure request",
      "domain": "erasure_requests",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Erasure-request id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "object_type",
          "description": "",
          "display_name": "Target object type",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "object_id",
          "description": "",
          "display_name": "Target object id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Request reason",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "requested_by",
          "description": "",
          "display_name": "Requesting principal",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "decided_by",
          "description": "",
          "display_name": "Deciding principal",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "decision_note",
          "description": "",
          "display_name": "Reviewer's note",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "Decision recorded at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "links_removed",
          "description": "",
          "display_name": "Links cascade-deleted at erasure",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.external_consortium",
      "description": "A soft-tier consortium (LHW, SLH, Preferred, Relais & Châteaux, Virtuoso) — properties opt in via the `loyalty_program_for_consortium` link. The consortium runs its own central operations and member identity; the loyalty_program at the property level points back here. Tenant-scoped because the consortium is a tenant-wide entity from Aerandil's perspective; cross-tenant canonicalisation (two Aerandil tenants both running LHW Leaders Club from the same consortium identity) defers per design-spec §G loyalty open questions — until then, each tenant carries its own `external_consortium` row.",
      "display_name": "External consortium",
      "domain": "loyalty",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Consortium id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Consortium code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Consortium name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Consortium kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "home_office_address",
          "description": "",
          "display_name": "Home office address",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "website_url",
          "description": "",
          "display_name": "Website URL",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "founded_year",
          "description": "",
          "display_name": "Year founded",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.external_record_reference",
      "description": "Opaque pointer to a record held in an external source-of-truth system (clinical, legal, financial-audit, regulatory). Aerandil holds the reference, never the record itself; the structural contract is enforced by the absence of a payload field.",
      "display_name": "External record reference",
      "domain": "external_records",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Reference id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "domain",
          "description": "",
          "display_name": "External system domain",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "opaque_id",
          "description": "",
          "display_name": "Opaque external id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "record_kind",
          "description": "",
          "display_name": "Record kind in the external system",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "source_updated_at",
          "description": "",
          "display_name": "Source system last-updated clock",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_fault_type",
          "description": "Take a fault type out of its catalogue for good. Stamps retired_at and the reason; its code stays taken. No new order is raised from it; orders already raised keep their words and their edge to it. Retiring a retired fault type is a no-op. Emits `fault_type.retired`."
        }
      ],
      "api_name": "hospitality.fault_type",
      "description": "A kind of fault a house keeps in its catalogue — \"no hot water\", \"TV remote dead\", \"lift stuck between floors\" (hubOS's failure catalogue). Keyed by `code` within its catalogue: a brand's (`fault_type_at_brand`, shared by the brand's houses) or one house's (`fault_type_at_property`); `define_fault_type` with a code the catalogue holds re-states it in place. It carries the `title` an order raised from it is given, the room `element` it is in, the `category` (the trade that mends it) and the `severity` it starts at — `create_work_order` and `create_public_area_work_order` take `fault_type_id` and fill each one the call does not state. `active`, or `retired` for good (the code stays taken).",
      "display_name": "Fault type",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "The catalogue's code for it — unique within the catalogue",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "title",
          "description": "",
          "display_name": "The title an order raised from it is given",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "element",
          "description": "",
          "display_name": "The room element the fault is in (hubOS's icon), when it is in one",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "The work-order category an order raised from it starts with — the trade that mends it",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "severity",
          "description": "",
          "display_name": "The severity an order raised from it starts at",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Notes for whoever raises or mends it",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "active, or retired (final)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "defined_at",
          "description": "",
          "display_name": "When it was first put in the catalogue",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When it was retired",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "retire_reason",
          "description": "",
          "display_name": "Why it was retired",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.approve_federated_analysis_definition",
          "description": "Record the privacy / governance approval outcome for a definition. A thin wrapper around proposal 0020 approval decisions — the domain event remains explicit. Sets the definition status to `approved`, `reviewing` (changes_requested), or `revoked` (rejected). Records the approval gate reference via the `federated_definition_reviewed_by_gate` link (proposal §8.4)."
        },
        {
          "api_name": "hospitality.action.start_federated_run",
          "description": "Start a run for an approved definition and cohort. Freezes the definition version and cohort snapshot, resolves eligible active participants, validates thresholds before collection, reserves privacy budget, and creates the run with status `collecting`. Idempotent on `run_key`. Validators (commit 5): `run_definition_must_be_approved`, `run_cohort_must_be_approved`, `run_input_window_end_le_now_unless_forecast`, `run_minimum_participants_before_collection`, `run_privacy_budget_reservation_must_succeed`."
        }
      ],
      "api_name": "hospitality.federated_analysis_definition",
      "description": "Versioned, reviewed computation plan. Names input object types, input field allowlist, forbidden field classes, local feature contract, local code reference / digest, local execution requirements, algorithm family, aggregation method, contribution bounds, privacy mechanisms, epsilon / delta per run, cohort policy, output schema, and required model-card / datasheet / threat-model / DPIA references. Immutable once approved (proposal 0021 §4.2 / §7).",
      "display_name": "Federated analysis definition",
      "domain": "federated_intelligence",
      "interfaces": [
        "hospitality.interface.approvable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Definition id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "program_api_name",
          "description": "",
          "display_name": "Stable reference to the program",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "definition_api_name",
          "description": "",
          "display_name": "Stable slug within program",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "version",
          "description": "",
          "display_name": "Semver-like; immutable once approved",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "analysis_kind",
          "description": "",
          "display_name": "Analytics / learning / PSI / clean-room / benchmark-export",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "objective",
          "description": "",
          "display_name": "What question the run answers",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "input_object_types",
          "description": "",
          "display_name": "Aerandil object types eligible for local reads",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "input_field_allowlist",
          "description": "",
          "display_name": "Fully qualified field paths or named derived features",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "forbidden_field_classes",
          "description": "",
          "display_name": "Defaults include direct_identifier, free_text, payment_secret, etc.",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "local_feature_contract",
          "description": "",
          "display_name": "Bounded schema for local features and aggregate vectors (JSON; typed per-field shape enforced by validators in commit 5)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "local_code_reference",
          "description": "",
          "display_name": "Optional code package / container / runner ref",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "local_code_digest",
          "description": "",
          "display_name": "Required when local_code_reference is set; immutable once approved",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "local_execution_requirements",
          "description": "",
          "display_name": "Runner version, memory / timeout bounds, offline mode, network egress policy (JSON; validator-enforced shape)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "algorithm_family",
          "description": "",
          "display_name": "Algorithm family",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "aggregation_method",
          "description": "",
          "display_name": "Aggregation method",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "weighting_method",
          "description": "",
          "display_name": "How contributions are weighted in aggregation",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "contribution_bounds",
          "description": "",
          "display_name": "Min / max values, clipping norms, per-participant caps (JSON; required for every approved definition)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "privacy_mechanisms",
          "description": "",
          "display_name": "Privacy mechanisms applied — at least cohort_threshold for every cross-tenant output",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "epsilon_per_run",
          "description": "",
          "display_name": "Required if DP applies",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "delta_per_run",
          "description": "",
          "display_name": "Required for approximate DP mechanisms",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "composition_group",
          "description": "",
          "display_name": "Groups related definitions for privacy-budget composition",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "metric_warn_thresholds",
          "description": "",
          "display_name": "Per-metric minimum acceptable validation values (JSON map metric name → number); outputs whose metric_values fall below warn `output-degraded-metrics`.",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "ci_width_warn_threshold",
          "description": "",
          "display_name": "Maximum acceptable confidence-interval width (upper − lower) for this definition's outputs; wider intervals warn `output-confidence-interval-too-wide`. Per-definition because CI widths are unitless across queries.",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "cohort_policy",
          "description": "",
          "display_name": "Minimums and suppression rules; may inherit from program (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "output_schema",
          "description": "",
          "display_name": "Bounded output shape; no dynamic arbitrary tables in v1 (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "output_review_required",
          "description": "",
          "display_name": "True by default for cross-tenant and model outputs",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "model_card_template_ref",
          "description": "",
          "display_name": "Required for model artifacts",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "datasheet_template_ref",
          "description": "",
          "display_name": "Required for benchmark exports and model artifacts",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "threat_model_ref",
          "description": "",
          "display_name": "Required for cross-tenant programs",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dpia_reference",
          "description": "",
          "display_name": "Required when program or definition is high-risk",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "review_notes",
          "description": "",
          "display_name": "Bounded rationale for approval",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "approved_by_principal",
          "description": "",
          "display_name": "Set by approval action",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "approved_at",
          "description": "",
          "display_name": "Set by approval action",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "minimum_local_runner_version",
          "description": "",
          "display_name": "Effective hard floor (max of program + definition pins, frozen at definition publish time)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "recommended_local_runner_version",
          "description": "",
          "display_name": "Effective soft baseline (max of program + definition pins, frozen at definition publish time)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.federated_cohort_definition",
      "description": "Reusable cohort / comp-set policy. Selection rules, geography granularity, time-window rule, minimum thresholds, max- contributor share, stratification fields, suppression rules, and optional excluded-tenants list. A cohort defines who can be compared, not what is computed (proposal 0021 §4.3).",
      "display_name": "Federated cohort definition",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Cohort id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "cohort_api_name",
          "description": "",
          "display_name": "Stable slug",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "program_api_name",
          "description": "",
          "display_name": "Owning program reference",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "display_name",
          "description": "",
          "display_name": "Human-readable name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "cohort_kind",
          "description": "",
          "display_name": "Cohort kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "selection_rules",
          "description": "",
          "display_name": "Typed rules over property / venue / outlet / market metadata (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "geography_granularity",
          "description": "",
          "display_name": "Geography granularity",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "time_window_rule",
          "description": "",
          "display_name": "Rolling or fixed reporting window (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "minimum_tenants",
          "description": "",
          "display_name": "Overrides program default if stricter",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "minimum_properties",
          "description": "",
          "display_name": "Overrides program default if stricter",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "minimum_subjects",
          "description": "",
          "display_name": "Required for person-affecting outputs",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "max_contributor_share",
          "description": "",
          "display_name": "Prevents one participant from dominating output (decimal in [0,1])",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "dropout_tolerance_fraction",
          "description": "",
          "display_name": "0022 §11.4 honest-majority threshold for secure-aggregation runs: the fraction of dispatched participants that may drop out (terminal dispatch failure / envelope expiry) before the run is failed and its privacy-budget reservation released. Decimal in (0,1]; absent means the §11.4 default of 0.5.",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "stratification_fields",
          "description": "",
          "display_name": "Allowed output slices, e.g. market class or property type",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "suppression_rules",
          "description": "",
          "display_name": "What to suppress when thresholds fail (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "excluded_tenants",
          "description": "",
          "display_name": "Rare; conflict / legal exclusions. Not visible to participants unless allowed.",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "approved_at",
          "description": "",
          "display_name": "Set by review",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.federated_contribution",
      "description": "Metadata for one participant's local contribution to a run. Stores hashes, bucketed counts, payload reference / hash, validation warnings, optional attestation reference. **Does not store raw local records, raw guest text, direct identifiers, PAN, PHI, or clinical detail** — those are rejected structurally by the contribution validators (proposal 0021 §4.7 / §7).",
      "display_name": "Federated contribution",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Contribution id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "run_id",
          "description": "",
          "display_name": "Federated run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "participation_id",
          "description": "",
          "display_name": "Participation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "contribution_kind",
          "description": "",
          "display_name": "Encrypted aggregate / noisy aggregate / model update / metrics only / attestation only",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "contribution_slot",
          "description": "",
          "display_name": "Slot discriminator when a definition declares multiple slots; null for the default single-slot path",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "local_code_digest",
          "description": "",
          "display_name": "Must match approved definition unless reviewed override",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "local_runner_version",
          "description": "",
          "display_name": "Runner or agent version",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "input_snapshot_hash",
          "description": "",
          "display_name": "Hash over local eligible input snapshot metadata; never raw data",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "input_window_start",
          "description": "",
          "display_name": "Must match run",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "input_window_end",
          "description": "",
          "display_name": "Must match run",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "contribution_fingerprint",
          "description": "",
          "display_name": "SHA-256 over the contribution ledger payload. Same ledger key with a different fingerprint is rejected.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "local_record_count_bucket",
          "description": "",
          "display_name": "Bucketed record count; exact counts stay local unless reviewed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "local_subject_count_bucket",
          "description": "",
          "display_name": "Required for person-affecting definitions",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "payload_reference",
          "description": "",
          "display_name": "Encrypted payload / clean-room output / secure aggregation share reference. Never the payload body itself.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payload_hash",
          "description": "",
          "display_name": "Required when payload_reference is set",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payload_size_bytes",
          "description": "",
          "display_name": "Safety bound",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "secure_aggregation_mask_ref",
          "description": "",
          "display_name": "Optional protocol metadata",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "epsilon_spent",
          "description": "",
          "display_name": "Local DP spend",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "delta_spent",
          "description": "",
          "display_name": "Local DP spend",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "clipping_applied",
          "description": "",
          "display_name": "Required true for model updates (validator enforced)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "validation_warnings",
          "description": "",
          "display_name": "Local runner warnings (bounded text array)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "attestation_reference",
          "description": "",
          "display_name": "TEE / runner attestation if used",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "submitted_at",
          "description": "",
          "display_name": "Set by action",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "accepted_at",
          "description": "",
          "display_name": "Set by validation",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Bounded",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "minimum_local_runner_version_at_submission",
          "description": "",
          "display_name": "Snapshot of the effective hard floor from the contribution's definition at action time",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "recommended_local_runner_version_at_submission",
          "description": "",
          "display_name": "Snapshot of the effective soft baseline from the contribution's definition at action time",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.allocate_privacy_budget",
          "description": "Create or adjust a privacy budget account. Reducing budget below already-spent amount requires review and sets status `exhausted` rather than rewriting history (proposal §8.7). Validators (commit 5): `privacy_budget_epsilon_required_when_dp`, `privacy_budget_spent_does_not_exceed_total`, `privacy_budget_delta_required_when_mechanism_uses_delta`."
        },
        {
          "api_name": "hospitality.action.deprecate_federated_program",
          "description": "Stop new definitions / runs while preserving historical outputs. Transitions program status to `deprecated` or `retired`. Existing published outputs remain available unless separately quarantined (proposal §8.2)."
        },
        {
          "api_name": "hospitality.action.enroll_federated_participant",
          "description": "Create or activate a participant opt-in record. Validates program status, compatible analysis kinds, legal basis / DPA / consent references, and (when execution mode is agent_installation) the enabled agent installation. Validators (commit 5): `participation_active_requires_legal_basis_and_reference`, `participation_consent_requires_consent_policy_reference`, `participation_active_requires_compatible_analysis_kinds`, `participation_agent_installation_required_when_execution_mode_is_agent`, `participation_cross_tenant_requires_dpa_or_review_waiver`."
        },
        {
          "api_name": "hospitality.action.publish_federated_analysis_definition",
          "description": "Create a new immutable definition version or move a draft into review. Validates field allowlist, forbidden field classes, contribution bounds, privacy mechanisms, and output schema. Freezes local_code_digest, local_feature_contract, and privacy settings for this version. Validators (commit 5): `definition_input_field_allowlist_excludes_forbidden_classes`, `definition_cross_tenant_requires_cohort_threshold`, `definition_person_affecting_requires_dp_or_waiver`, `definition_federated_learning_requires_cards_and_clipping`, `definition_local_code_digest_required_when_reference_set`, `definition_none_threshold_only_internal_portfolio_only`."
        },
        {
          "api_name": "hospitality.action.record_federated_privacy_incident",
          "description": "Record a privacy, security, or disclosure-control incident related to a program, run, contribution, or output. The handler appends an incident event, optionally quarantines outputs, and optionally pauses the program. Per proposal §8.13, the action creates the durable evidence trail but the remediation gate is opened separately through proposal 0020 approval gates."
        }
      ],
      "api_name": "hospitality.federated_intelligence_program",
      "description": "Durable governance envelope for a family of related cross-tenant analyses or model runs. Carries purpose, governance tier, allowed analysis kinds, default minimum cohort rules, default privacy mechanisms, budget periods, legal-basis / DPA / DPIA references, coordinator metadata, and the `review_gate_policy` driving proposal 0020 approval gates. Cross-tenant programs require DPA references and structural minimum cohort floors (proposal 0021 §4.1 / §7).",
      "display_name": "Federated intelligence program",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Program id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "api_name",
          "description": "",
          "display_name": "Stable slug; unique within owner scope",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "display_name",
          "description": "",
          "display_name": "Human-readable name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing summary. No marketing claims.",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "owner_scope",
          "description": "",
          "display_name": "Owner scope: tenant / consortium / aerandil_managed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "owner_ref",
          "description": "",
          "display_name": "Consortium or operator governance reference when owner is not a single tenant",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "purpose",
          "description": "",
          "display_name": "Drives legal / review expectations",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "governance_tier",
          "description": "",
          "display_name": "Governance tier — floor for cohort / privacy validators",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "allowed_analysis_kinds",
          "description": "",
          "display_name": "Analytics first; learning requires review",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "allowed_output_kinds",
          "description": "",
          "display_name": "Benchmark reports, calibration curves, model artifacts, signal packages",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "default_min_tenants",
          "description": "",
          "display_name": "Hard minimum for cross-tenant outputs (>= 3 for cross_tenant_network)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "default_min_properties",
          "description": "",
          "display_name": "Hard minimum for cross-tenant outputs",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "default_min_subjects",
          "description": "",
          "display_name": "Required when output can be person-affecting",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "default_max_contributor_share",
          "description": "",
          "display_name": "No participant may dominate output beyond this share (decimal in [0,1])",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "default_privacy_mechanisms",
          "description": "",
          "display_name": "Default privacy mechanisms applied per definition",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "default_epsilon",
          "description": "",
          "display_name": "Program-level default for DP definitions; null when DP not applicable",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "default_delta",
          "description": "",
          "display_name": "Required when default_epsilon set and mechanism needs delta",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "privacy_budget_period",
          "description": "",
          "display_name": "Default period for budget accounting",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "legal_basis_summary",
          "description": "",
          "display_name": "Bounded summary; full legal document via external ref",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "dpa_reference",
          "description": "",
          "display_name": "DPA / consortium-agreement reference. Required for cross-tenant programs unless reviewer waives.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dpia_reference",
          "description": "",
          "display_name": "Required for high-risk or person-affecting programs",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "coordinator_kind",
          "description": "",
          "display_name": "Coordinator: aerandil / tenant / consortium / third_party_clean_room",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "coordinator_reference",
          "description": "",
          "display_name": "Optional system or contract reference",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "review_gate_policy",
          "description": "",
          "display_name": "Names which definitions and outputs require proposal 0020 approval gates (JSON; per-tier required-review map).",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "participant_benefit_policy",
          "description": "",
          "display_name": "What participating tenants receive in return",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "downstream_use_policy",
          "description": "",
          "display_name": "Allowed uses for outputs; machine-readable policy can follow later",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "Effective from",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Effective to",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "created_by_principal",
          "description": "",
          "display_name": "`Principal::stable_id()` of the proposer",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "approved_at",
          "description": "",
          "display_name": "Set only via approval action",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "minimum_local_runner_version",
          "description": "",
          "display_name": "Hard floor — contributions whose `local_runner_version` parses below this semver are rejected at action time (AWS Lambda deprecation floor)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "recommended_local_runner_version",
          "description": "",
          "display_name": "Soft baseline — contributions whose `local_runner_version` parses below this semver emit `contribution_runner_version_outdated_warns` (Stripe-style API-version upgrade signal)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.publish_federated_output",
          "description": "Publish a reviewed output. Validates run status and output schema, validates review gate completion where required, validates suppression and privacy metadata, sets status to `published`. Validators (commit 5): `output_publish_requires_run_completed_or_output_review`, `output_thresholds_satisfied`, `output_model_artifact_requires_card_and_datasheet`, `output_review_gate_required_when_definition_says_so`, `output_no_named_tenants_in_cross_tenant_output`."
        },
        {
          "api_name": "hospitality.action.quarantine_federated_output",
          "description": "Remove an unsafe output from normal consumption without deleting history. Sets output status `quarantined`, disables reads except for privileged compliance scopes, optionally pauses related definitions / programs, and notifies participants. Since an output may have been read already, quarantine cannot pretend to erase knowledge — it preserves evidence for audit and incident response (proposal §10.7 / §8.12)."
        }
      ],
      "api_name": "hospitality.federated_output",
      "description": "Aggregate artefact from a federated run. Carries cohort summary, privacy guarantee summary, suppression metadata, confidence intervals where applicable, applicability statement, downstream-use restrictions, and (for model artifacts / signal packages) required model card + datasheet refs. Cross-tenant outputs cannot name tenants or properties unless the program is internal portfolio or a consortium policy explicitly permits named publication (proposal 0021 §4.8 / §7).",
      "display_name": "Federated output",
      "domain": "federated_intelligence",
      "interfaces": [
        "hospitality.interface.approvable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Output id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "run_id",
          "description": "",
          "display_name": "Source run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "output_kind",
          "description": "",
          "display_name": "Benchmark report / calibration curve / aggregate metric / model artifact / signal package / governance card",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "output_schema_version",
          "description": "",
          "display_name": "Output schema version",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "metric_values",
          "description": "",
          "display_name": "Bounded aggregate values; no row-level data (JSON)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "artifact_reference",
          "description": "",
          "display_name": "Model artifact, report, or card if stored externally",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "artifact_hash",
          "description": "",
          "display_name": "Required when artifact_reference is set",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "cohort_summary",
          "description": "",
          "display_name": "Buckets; not named participants unless internal portfolio output (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "privacy_guarantee_summary",
          "description": "",
          "display_name": "Mechanisms, epsilon / delta, thresholds, suppression (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "privacy_budget_spend",
          "description": "",
          "display_name": "Actual spend (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "suppression_applied",
          "description": "",
          "display_name": "True if any metric or slice suppressed",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "suppression_reasons",
          "description": "",
          "display_name": "Reasons for suppression",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "noise_parameters",
          "description": "",
          "display_name": "Bounded mechanism metadata (JSON)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "confidence_intervals",
          "description": "",
          "display_name": "Required where output is statistical and meaningful (JSON)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "applicability_statement",
          "description": "",
          "display_name": "What the output can and cannot support",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "downstream_use_restrictions",
          "description": "",
          "display_name": "Downstream use restrictions",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "model_card_reference",
          "description": "",
          "display_name": "Required for model artifacts and signal packages",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "datasheet_reference",
          "description": "",
          "display_name": "Required for model artifacts and benchmark exports",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "published_to_scope",
          "description": "",
          "display_name": "Program owner / participants / public aggregate / specific recipients",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "review_gate_id",
          "description": "",
          "display_name": "Required when output review is required",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "publication_evidence",
          "description": "",
          "display_name": "Durable evidence bundle for review gate, budget spend, cohort threshold, suppression, and artifact hash.",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "published_at",
          "description": "",
          "display_name": "Set by action",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "Required for model artifacts unless waived",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "quarantined_at",
          "description": "",
          "display_name": "Set by quarantine action",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "quarantine_reason",
          "description": "",
          "display_name": "Bounded",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "incident_reference",
          "description": "",
          "display_name": "External incident ref when quarantined",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.withdraw_federated_participant",
          "description": "Pause or withdraw a participant prospectively. Sets withdrawal status and `withdrawal_effective_at`. Future runs whose collection starts after the effective time exclude the participant; already published aggregate outputs are NOT surgically removed (proposal §10.6). Open runs may be re-evaluated when `quarantine_open_runs = true`."
        }
      ],
      "api_name": "hospitality.federated_participation",
      "description": "Opt-in / opt-out record for a tenant, property, portfolio, or venue participating in a program. Carries legal basis, DPA / consent references, allowed analysis kinds, participation- specific exclusions, local execution mode, agent installation reference (when execution mode is agent_installation), credential reference (opaque), optional participant-level contribution cap, output access tier, and prospective withdrawal status. Withdrawal excludes future runs after `withdrawal_effective_at`; already published outputs remain unless quarantined by review (proposal 0021 §4.4 / §10.6).",
      "display_name": "Federated participation",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Participation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "program_api_name",
          "description": "",
          "display_name": "Program joined",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "participant_scope",
          "description": "",
          "display_name": "Tenant / property / portfolio / venue",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "participant_object_ref",
          "description": "",
          "display_name": "Property / venue / portfolio id when scope is not tenant. The concrete typed link lives on the link surface for property and venue scopes (proposal §6).",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "enrolled_by_principal",
          "description": "",
          "display_name": "Principal stable id of enroller (human or admin agent)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "approved_by_principal",
          "description": "",
          "display_name": "Required for active cross-tenant participation",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "legal_basis_kind",
          "description": "",
          "display_name": "Contract / legitimate_interest / consent / public_task / not_personal_data / other_reviewed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "legal_basis_reference",
          "description": "",
          "display_name": "Required except for not_personal_data with reviewer note",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dpa_reference",
          "description": "",
          "display_name": "Required for cross-tenant programs",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "consent_policy_reference",
          "description": "",
          "display_name": "Required if consent is the basis",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "allowed_analysis_kinds",
          "description": "",
          "display_name": "Participant may opt into analytics but not model training",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "excluded_object_types",
          "description": "",
          "display_name": "Tenant-specific exclusions",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "excluded_field_classes",
          "description": "",
          "display_name": "Additional local privacy exclusions",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "local_execution_mode",
          "description": "",
          "display_name": "How local extraction runs",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "agent_installation_id",
          "description": "",
          "display_name": "Required when execution mode is agent_installation",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "credential_reference",
          "description": "",
          "display_name": "Pointer to operator vault or clean-room credential. No secret material — same `credential_reference_no_secret_material` discipline as proposal 0020.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "privacy_budget_account_id",
          "description": "",
          "display_name": "Optional explicit budget account",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "contribution_weight_cap",
          "description": "",
          "display_name": "Participant-specific cap stricter than definition (decimal in [0,1])",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "output_access_level",
          "description": "",
          "display_name": "Output-access tier granted to this participant",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "withdrawal_requested_at",
          "description": "",
          "display_name": "Set when participant withdraws",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "withdrawal_effective_at",
          "description": "",
          "display_name": "Prospective exclusion point",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "withdrawal_reason",
          "description": "",
          "display_name": "Withdrawal reason",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "runner_identity_ref",
          "description": "",
          "display_name": "SPIFFE URI or other identity URI for the runner; matched against the envelope's `participant.runner_identity_ref` at dispatch time (proposal 0022 §8.3).",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "runner_signing_key_ref",
          "description": "",
          "display_name": "Opaque pointer to the runner's Ed25519 verification key in the operator's vault. No key material stored here per `credential_reference_no_secret_material` (proposal 0022 §8.2).",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dispatch_endpoint_url",
          "description": "",
          "display_name": "Participant-published HTTPS URL the proposal 0022 dispatcher POSTs signed envelopes to (§6.1). Absent for SSE-only / pull-based profiles — the dispatcher terminal-fails push dispatches to a participation without one. HTTPS-only; http is admissible solely for loopback receivers (local development), mirroring the webhook-subscription rule.",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.cancel_federated_run",
          "description": "Operator-initiated cancellation of a federated run before output publication. Transitions run status to `cancelled` and releases the reserved privacy budget — except when the run uses local DP and has already accepted at least one contribution, in which case the local spend is non-recoverable (proposal 0021 §9) and the operator must instead drive `record_federated_aggregation_result` with aggregation_status = `failed` to commit the spend. The 4-clause observation gate (Rogers et al. 2016, Privacy Odometers and Filters, Theorem 3.4; Dwork-Roth, Algorithmic Foundations §3.5 Prop 2.1) is enforced before release: (1) no `commit` event for the run's correlation_id, (2) run status ∈ {queued, privacy_review, approved, collecting, aggregating} (pre-output_review), (3) mechanism_family ≠ `local_dp` OR `participants_accepted` = 0, (4) idempotent on the run's correlation_id (the partial unique index on `privacy_budget_event` enforces single-flight release)."
        },
        {
          "api_name": "hospitality.action.expire_federated_run",
          "description": "Past-deadline expiry of a federated run. Settlement logic is identical to `cancel_federated_run` (release the reservation unless local DP spend has already occurred), with the additional precondition that `deadline_at` on the run has passed. Intended for periodic operator sweeps that recover budget from runs that never made it to aggregation. Refuses if the deadline has not yet passed."
        },
        {
          "api_name": "hospitality.action.record_federated_aggregation_result",
          "description": "Finalize aggregation after enough contributions are accepted. Verifies accepted contribution counts and thresholds, commits or releases privacy budget, sets run status to `output_review`, `completed`, `failed`, or `quarantined`, and creates a draft `federated_output` when aggregation succeeded. Validators (commit 5): `run_aggregation_requires_threshold_satisfaction`, `run_aggregation_max_contributor_share_satisfied`."
        },
        {
          "api_name": "hospitality.action.record_federated_contribution",
          "description": "Record one participant contribution metadata and payload reference. Verifies run is collecting and participant eligible, validates code digest, window, payload bounds, and forbidden raw data, validates local privacy spend against reserved budget, and marks the contribution `accepted`, `rejected`, or `suppressed`. Idempotent on `run_id + participation_id + contribution_slot`. Validators (commit 5): `contribution_window_must_match_run`, `contribution_local_code_digest_must_match_definition`, `contribution_payload_size_must_be_bounded`, `contribution_clipping_required_for_model_update`, `contribution_no_raw_payload_inline`."
        }
      ],
      "api_name": "hospitality.federated_run",
      "description": "One execution of an approved analysis definition against a cohort and time window. Pins the definition version, freezes the cohort snapshot and input window, reserves privacy budget, and drives the §5.11 lifecycle. Idempotent on `run_key`; retries with identical parameters return the existing run, retries with different parameters reject as idempotency mismatch (proposal 0021 §4.6 / §9).",
      "display_name": "Federated run",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "program_api_name",
          "description": "",
          "display_name": "Program api name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "definition_api_name",
          "description": "",
          "display_name": "Definition api name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "definition_id",
          "description": "",
          "display_name": "Federated analysis definition object id. Part of the durable idempotency key.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "definition_version",
          "description": "",
          "display_name": "Frozen at start",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "cohort_id",
          "description": "",
          "display_name": "Federated cohort definition object id. Part of the durable idempotency key.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "run_key",
          "description": "",
          "display_name": "Stable idempotency key: program + definition + version + cohort + window",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "trigger_kind",
          "description": "",
          "display_name": "Scheduled / manual / workflow / agent / backfill",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cohort_snapshot",
          "description": "",
          "display_name": "Frozen cohort rules and threshold settings (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "input_window_start",
          "description": "",
          "display_name": "Input window start",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "input_window_end",
          "description": "",
          "display_name": "Input window end",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "request_fingerprint",
          "description": "",
          "display_name": "SHA-256 over canonical run-start request fields. Same run_key with a different fingerprint is rejected.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "participants_expected",
          "description": "",
          "display_name": "Count at run start",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "participants_requested",
          "description": "",
          "display_name": "Count after eligibility checks",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "participants_submitted",
          "description": "",
          "display_name": "Count accepted before aggregation",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "participants_suppressed",
          "description": "",
          "display_name": "Count suppressed / rejected",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "privacy_budget_reserved",
          "description": "",
          "display_name": "Epsilon / delta / account refs reserved for the run (JSON)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "metric_warn_thresholds",
          "description": "",
          "display_name": "Snapshot of the definition's per-metric minimum acceptable validation values at run start (JSON map).",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "ci_width_warn_threshold",
          "description": "",
          "display_name": "Snapshot of the definition's maximum acceptable confidence-interval width at run start.",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "aggregation_protocol",
          "description": "",
          "display_name": "Aggregation protocol",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "secure_aggregation_session_ref",
          "description": "",
          "display_name": "Required for secure aggregation",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "clean_room_job_ref",
          "description": "",
          "display_name": "Required for third-party clean room",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "deadline_at",
          "description": "",
          "display_name": "Contribution deadline",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "Set by action",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "finished_at",
          "description": "",
          "display_name": "Set by aggregation result",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "failure_reason",
          "description": "",
          "display_name": "Bounded",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "correlation_id",
          "description": "",
          "display_name": "Ties events together",
          "kind": "string",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.feedback_label",
      "description": "One of a house's guest-feedback labels — hubOS's quality labels: a `code` and a `name` the house files feedback under (\"breakfast\", \"noise at night\", \"VIP\"), beside the kernel's fixed `category`. Keyed by `code` within the house (`feedback_label_at_property`); `define_feedback_label` with a code the house holds re-states it in place. `active` labels are put on feedback by `set_guest_feedback_labels`; a `retired` one stays on the feedback it was put on and goes on no more, and re-stating its code revives it.",
      "display_name": "Feedback label",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "The house's code for the label — unique within the house",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "The label in words, as the desk and the floor read it",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Active (put on feedback) or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "defined_at",
          "description": "",
          "display_name": "When it was first defined",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When it was retired",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_payment",
          "description": "Apply a payment to a folio."
        },
        {
          "api_name": "hospitality.action.authorise_charge_with_review",
          "description": "Record a payment authorisation event with the fraud-risk assessment gate enforced as a structural precondition. Validators reject: assessment not fresh / suggested, band = block, assessment already linked to another payment, payment token shaped like a PAN, liability-shift basis incoherent with authentication outcome, TRA / low-value amount ceiling exceeded, capture_strategy incoherent with band, folio closed or void. Atomically inserts a Payment row (status = pending for manual_capture_after_review / authorise_only, settled for auto_capture_if_clear) with the decision-provenance trio, and transitions the assessment to applied with applied_action_kind = approve_proceed."
        },
        {
          "api_name": "hospitality.action.close_folio",
          "description": "Close an open folio through the zero-balance discipline — the generic close for folios `check_out` does not own: rebate folios, reopened guest folios, house accounts. Refuses a non-zero balance (settle it first) and a folio whose stay is still in-house (that close belongs to `check_out`). Re-applying to a closed folio is a no-op."
        },
        {
          "api_name": "hospitality.action.issue_invoice",
          "description": "Turn a sealed folio into the numbered document its recipient keeps — a tax invoice, or a credit note when the target is a rebate folio whose original was invoiced. Allocates the next value from the property's series inside this transaction (dense: a failed issue releases the number rather than burning it), snapshots the issuer and the recipient onto the document so a later profile edit cannot rewrite it, copies every charge as a printed line, and derives the per-rate tax bands from the charges' own `tax_application` rows. Accepts two folio states. A `closed` folio at zero balance is the settled stay: nothing is due. An `open` folio already transferred to a company's A/R account (`settle_folio_to_ar` marks and links it but moves no money) is the billed stay: the balance becomes `amount_due`, the folio's `due_at` becomes `due_on`, and issuing SEALS the folio as it goes — which is the only place this verb writes folio state, and why a company's document can be issued at all. Refuses an open folio with no A/R transfer, a void or house folio, a folio whose uncredited invoice already exists, a folio with no charges, a property with no `billing_identity`, a recipient that cannot be resolved, and — above the property's simplified-invoice threshold — a recipient without a name, city, postcode and country. Below that threshold the recipient's details may be omitted (UK £250 under reg 16A, Germany €250 under §33 UStDV), but never for a company: the point of their document is input-tax recovery. `preview_invoice` answers all of the above without allocating anything, so the desk can disable its own button and say why."
        },
        {
          "api_name": "hospitality.action.open_rebate_folio",
          "description": "Open a rebate folio against a sealed bill — the correction path for a folio closed beyond the reopen window, where `reopen_folio` refuses by design. The original is never touched: `reverse_charge` and `refund_payment` route their sealed-folio legs onto the rebate folio, late charges post to it directly, and it settles and closes through the ordinary machinery (`apply_payment`, `settle_folio_to_ar`, `close_folio`). Refuses an open folio (correct it directly), a void folio, and a folio still provably inside the reopen window (`reopen_folio` is that path — one clock per case, no overlap). Re-applying while a rebate folio is already open is a no-op: corrective lines must have exactly one place to land."
        },
        {
          "api_name": "hospitality.action.post_charge",
          "description": "Post a charge line to a folio. `amount` is the charge's base amount — inclusive taxes (VAT-style) ride inside it, while exclusive tax lines and `gratuity` add on top, so the folio debit is amount + Σ(exclusive tax) + gratuity. Each `tax_lines` entry lands as a linked `tax_application` row for fiscal / OTA disclosure; the charge's aggregate `tax` is the sum of every line regardless of inclusivity."
        },
        {
          "api_name": "hospitality.action.reconcile_folio_balance",
          "description": "Recompute the folio's balance from its own lines — the balance brought forward, plus every unvoided, non-package charge at what it debited (base + exclusive tax + gratuity), less every settled payment and refunded leg — and write it back only when the stored figure disagrees, emitting `folio.balance_reconciled.recorded` with the delta and a `reconcile` ledger entry dated today, so drift is observable and a closed day is never rewritten. A folio not yet on the ledger (an import, a row older than the ledger) is brought onto it first with an `opening` entry for its whole balance. Idempotent: a folio that agrees with its lines is left untouched."
        },
        {
          "api_name": "hospitality.action.refresh_folio_aging",
          "description": "Recompute `ar_aging_bucket` from `due_at` and payment history. Idempotent."
        },
        {
          "api_name": "hospitality.action.reopen_folio",
          "description": "Reopen a closed folio so a late charge or a correction can be posted to it. Opens the *bill* only: the stay stays departed and the room stays vacant, which is OPERA's split between Reopen Folio (post-departure, folio-only) and Reinstate (undo the checkout itself). Refuses a `void` folio, refuses a folio closed longer ago than the reopen window — a bill that can be reopened forever is not settled — and refuses a closed folio with no `closed_at`, since the window cannot be established without one. Re-applying to an already-open folio is a no-op. Closing again goes back through the ordinary zero-balance discipline."
        },
        {
          "api_name": "hospitality.action.settle_folio_to_ar",
          "description": "Move an outstanding-balance folio onto a company A/R folio. Sets `ar_transferred_at` and `due_at`. The aging cycle maintains `ar_aging_bucket` thereafter."
        },
        {
          "api_name": "hospitality.action.transfer_booking_hold",
          "description": "Use the card hold the guest placed at booking as the stay's hold, instead of taking a second one at check-in. Targets the stay's folio; every completed `preauthorization` request on the booking behind it whose hold is still uncaptured (`pending`) on the booking's own account moves onto this folio, so the check-out capture takes it against what the stay owes. Ledger-neutral — a pending hold moves no balance. Refused when the folio is closed or void, hangs off no stay, is kept in another currency than the hold, or the booking has no such hold; a hold already on this folio changes nothing, so a repeated call is safe. Emits `payment.booking_hold_transferred.recorded` per hold moved."
        }
      ],
      "api_name": "hospitality.folio",
      "description": "The accounting ledger for a stay. Holds charges and payments; closes when the guest checks out and the balance is zero.",
      "display_name": "Folio",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "number",
          "description": "",
          "display_name": "Folio number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Folio status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Folio kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "window_number",
          "description": "",
          "display_name": "Window number (1..=8)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "balance",
          "description": "",
          "display_name": "Outstanding balance",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "balance_forward",
          "description": "",
          "display_name": "Balance brought forward onto the ledger",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "Opened at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "closed_at",
          "description": "",
          "display_name": "Closed at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "reopened_at",
          "description": "",
          "display_name": "Reopened at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_invoice_number",
          "description": "",
          "display_name": "Operator-facing invoice number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "ar_aging_bucket",
          "description": "",
          "display_name": "A/R aging bucket (projection)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "ar_transferred_at",
          "description": "",
          "display_name": "When folio settled to company A/R",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "due_at",
          "description": "",
          "display_name": "Payment due at",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.folio_routing_rule",
      "description": "Structured contractual logic for routing charges between folios. Encodes 'route all charges except telephone to the master folio at 50% capped at $500/day' shapes that every group / corporate / negotiated contract expresses. The folio handler walks active rules at charge post-time and applies the matching one — lowest `priority` number wins; ties broken by `effective_from`.",
      "display_name": "Folio routing rule",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Rule id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "Effective from",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Effective to",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "applies_to",
          "description": "",
          "display_name": "Applies to",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "charge_code_filter",
          "description": "",
          "display_name": "Charge code filter (applies_to = charge_codes)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "charge_category_filter",
          "description": "",
          "display_name": "Charge category filter (applies_to = charge_categories)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "percentage",
          "description": "",
          "display_name": "Routed fraction (applies_to = percentage)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "cap_amount",
          "description": "",
          "display_name": "Cap on routed total (applies_to = capped_amount)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "priority",
          "description": "",
          "display_name": "Rule priority (lower evaluates first)",
          "kind": "int",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.withdraw_fraud_signal",
          "description": "Take one of this house's contributions back out of Watchtower — a dispute later reversed, an outcome recorded against the wrong card. The contribution moves to `withdrawn` with the reason, and the card rail stops counting its digests in the network's answers. Forward-only: answers the network already gave stand as given. Withdrawing twice is refused. `withdrawn_by` records who took it back: the house (the default), or the platform on its review of a contested answer (`POST /v1/watchtower/contributions/{id}/withdraw`), which only the platform's own rail may say. Emits `fraud_network.contribution_withdrawn`."
        }
      ],
      "api_name": "hospitality.fraud_network_contribution",
      "description": "A confirmed fraud outcome this house offered to Watchtower, the card rail's federated fraud network (proposal 0029): a dispute lost for fraud (`chargeback_lost_fraud`, the issuer's word) or an assessment applied as a block on the house's own evidence (`confirmed_fraud`, the house's word — at another house it sends a hold to review, never blocks it, and it may not rest on the network's own answer), carried as salted one-way fingerprints of the card, the guest's email and phone. Only the digests reach the network, never the identifiers, never the booking; the network never tells another house which house contributed. Recorded by the card rail only, under the house's `fraud_network` consent; `withdraw_fraud_signal` takes it back out (forward-only — answers already given stand), by the house or, on its review of a contested answer, by the platform on the house's behalf (`withdrawn_by`). One per source (`source_reference`, the processor's dispute id or the assessment id).",
      "display_name": "Fraud network contribution",
      "domain": "fraud_network",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Contribution id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "outcome",
          "description": "",
          "display_name": "The confirmed outcome contributed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Whether the contribution stands in the network",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "source_reference",
          "description": "",
          "display_name": "What the outcome was read from — the processor's dispute id or the assessment id; one contribution per source",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "fingerprints",
          "description": "",
          "display_name": "The digests offered, as `[{kind, digest}]` — `kind` one of card / email / phone, `digest` 64 lower-case hex (HMAC-SHA256 under the network salt). Never an identifier",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "salt_epoch",
          "description": "",
          "display_name": "The network salt generation the digests were taken under",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "contributed_at",
          "description": "",
          "display_name": "When the outcome was offered",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "withdrawn_at",
          "description": "",
          "display_name": "When it was taken back out of the network",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "withdrawal_reason",
          "description": "",
          "display_name": "Why it was taken back, in words",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "withdrawn_by",
          "description": "",
          "display_name": "Who took it back — the house, or the platform on its review of a contest, on the house's behalf. Absent on a withdrawal recorded before 2026-11-03, which was the house's",
          "kind": "enum",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.contest_fraud_network_signal",
          "description": "The desk contests what Watchtower answered — the guest disputes it, and the desk believes them. The answer is marked contested with the reason, which puts the contributions behind it before the platform's review (`fraud_network.signal_contested`); the house never learns whose they are. A contested `block` lets the card rail's next answer for the same subject, within the day, take the hold to review instead of refusing it (`desk_override`). Only an answer that matched can be contested, and only once."
        }
      ],
      "api_name": "hospitality.fraud_network_signal",
      "description": "What Watchtower, the card rail's federated fraud network (proposal 0029), answered when this house asked about a card, email or phone before holding a card — frozen as issued; only a contest is ever added. `match_present` says whether a fraud outcome on the rail (contributed within the last two years) carries one of the digests; the count, the breadth of houses behind it and its recency are bucketed, and breadth reads only `below_cohort` until the cohort floor is met — the network never says which house, nor how many. `band_floor` is the lowest risk band the rail lets the hold's assessment carry and `floor_basis` why: a card match blocks only on the house's own loss or on another house's recent issuer-confirmed loss that a second house or the guest's contact corroborates; anything else sends the hold to review. A desk that believes the guest contests the answer (`contest_fraud_network_signal`), which puts it before the platform's review and lets the next answer for the same subject take the hold to review instead (`desk_override`, `override_of`). `fingerprints` keeps the digests the house asked about, so the platform's review of a contest can find the contributions the answer rested on. Recorded by the card rail only.",
      "display_name": "Fraud network signal",
      "domain": "fraud_network",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Signal id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "subject_kind",
          "description": "",
          "display_name": "What the risk read was about",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "subject_id",
          "description": "",
          "display_name": "The subject's id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kinds_queried",
          "description": "",
          "display_name": "The identifier kinds the house could ask about",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "match_present",
          "description": "",
          "display_name": "Whether any confirmed outcome on the network carries one of them",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "matched_kinds",
          "description": "",
          "display_name": "Which identifier kinds matched",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "match_count_bucket",
          "description": "",
          "display_name": "How many matching outcomes, bucketed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "contributor_breadth",
          "description": "",
          "display_name": "Whether the houses behind the match meet the cohort floor",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recency_bucket",
          "description": "",
          "display_name": "How recent the newest matching outcome is",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "band_floor",
          "description": "",
          "display_name": "The lowest band the hold's assessment may carry given this answer",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "floor_basis",
          "description": "",
          "display_name": "Why the answer set that floor — none, contact, card_unconfirmed, card_corroborated, card_own_house or desk_override",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "override_of",
          "description": "",
          "display_name": "The contested answer this one takes to review instead (desk_override)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "contested_at",
          "description": "",
          "display_name": "When the desk contested this answer for the platform's review",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "contest_reason",
          "description": "",
          "display_name": "Why the desk contested it, in words",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "salt_epoch",
          "description": "",
          "display_name": "The network salt generation the read was taken under",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "fingerprints",
          "description": "",
          "display_name": "The digests the house asked about, as `[{kind, digest}]` — one per kind queried, 64 lower-case hex under the network salt, never an identifier — kept so the platform's review of a contest can find the contributions behind the answer (since 2026-11-03; an answer frozen before then carries none)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the network answered",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "When the answer stops being a fair read (24 hours on)",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_fraud_assessment",
          "description": "Apply a suggested fraud_risk_assessment by recording the action class actually taken: approve_proceed / decline_block / step_up_3ds_initiated / step_up_kyc_initiated / decline_with_refund / decline_with_hold / approve_with_manual_review. The diff between recommended and applied action kinds is the audit-grade edit-rate signal (proposal 0015 §9). Rejected if the assessment isn't in `suggested` state, has expired, or violates the `block_band_no_auto_approve` cross-link validator."
        },
        {
          "api_name": "hospitality.action.escalate_via_fraud_assessment",
          "description": "Escalate a suggested fraud_risk_assessment to the chargeback- defence team or risk analyst with structured handoff. Transitions `decision_status` to `escalated`; sets `escalation_reason`; optionally links to a named staff_member."
        },
        {
          "api_name": "hospitality.action.reject_fraud_assessment",
          "description": "Decline a suggested fraud_risk_assessment (treat as false positive). Transitions `decision_status` to `rejected` and records an optional free-text reason for the multi-metric trust-formation instrumentation. Rejected if the assessment isn't in `suggested` state."
        }
      ],
      "api_name": "hospitality.fraud_risk_assessment",
      "description": "A typed fraud-risk assessment issued by an agent (or, less commonly, a human risk analyst logging a manual assessment into the audit trail) against a reservation, payment, charge, person, or loyalty redemption. Mostly-immutable post-issue: the score / band / recommended_action / confidence / factors / reason_codes / rationale / risk_context_snapshot are frozen at write time so the audit trail is what the model saw at decision time, not against today's profile. The decision_*, applied_*, escalation_*, and rejection_* fields mutate exactly once on the transition out of `suggested` — gated by the `decision_status_transitions` cross-link validator. Doubly load-bearing for chargeback-dispute defence: card networks accept frozen-feature evidence in dispute response.",
      "display_name": "Fraud risk assessment",
      "domain": "fraud_detection",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Assessment id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "subject_kind",
          "description": "",
          "display_name": "What kind of object the assessment scored",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "subject_id",
          "description": "",
          "display_name": "Id of the typed object the assessment scored (resolved against `subject_kind`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "reservation_ref",
          "description": "",
          "display_name": "Optional denormalised reservation anchor (set for reservation-scoped assessments)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "person_ref",
          "description": "",
          "display_name": "Optional denormalised person anchor (set when the booking has an identified guest or when the assessment is person-scoped)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "folio_charge_ref",
          "description": "",
          "display_name": "Optional charge anchor (set for charge-scoped assessments at `at_payment` / `at_folio_close` fire-points)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "fire_point",
          "description": "",
          "display_name": "Lifecycle moment the assessment fired at",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "vendor_id",
          "description": "",
          "display_name": "Vendor identity (denormalised from Principal::Agent.vendor)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "vendor_model_version",
          "description": "",
          "display_name": "Vendor model version when exposed (e.g. stripe_radar_v3)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "score",
          "description": "",
          "display_name": "Raw numeric score on the [0, 100] scale",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "score_calibrated",
          "description": "",
          "display_name": "Optional calibrated probability [0, 1] when the model emits one",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "band",
          "description": "",
          "display_name": "Risk band ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recommended_action",
          "description": "",
          "display_name": "Recommended action class",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "confidence",
          "description": "",
          "display_name": "Operator-facing confidence ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "confidence_score",
          "description": "",
          "display_name": "Calibrated probability [0, 1] — set when the model emits one",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "factors_considered",
          "description": "",
          "display_name": "Signal categories the assessment conditioned on",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "reason_codes",
          "description": "",
          "display_name": "Explanation array — each entry carries a code, severity, and signed contribution to the final score (JSON; host populates per documented shape)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "triggered_rules",
          "description": "",
          "display_name": "Custom-rule provenance when the score path went through rules (JSON array of {rule_id, rule_version?, action?})",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "Human-readable explanation surfaced to operators",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "risk_context_snapshot",
          "description": "",
          "display_name": "Frozen context snapshot at issue time (§3.1.1; JSON; host populates per recommended shape)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the agent emitted the assessment",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "After this, decision_status moves to expired",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decision_status",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the assessment transitioned out of suggested",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "decided_by",
          "description": "",
          "display_name": "`principal.stable_id()` of the deciding principal",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "applied_action_kind",
          "description": "",
          "display_name": "Action class actually taken on `applied` transition",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "applied_action_correlation_id",
          "description": "",
          "display_name": "correlation_id of the chained action's ApplyActionResponse (set when apply chained into a typed action)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "escalation_reason",
          "description": "",
          "display_name": "Reason recorded on escalation",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "escalation_target_staff_ref",
          "description": "",
          "display_name": "Optional explicit staff assignment on escalation",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Free-text reason on reject (gold-standard decision-log pattern)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "superseded_by_assessment_ref",
          "description": "",
          "display_name": "Newer assessment that replaces this one (set on supersedence)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "policy_outcomes",
          "description": "",
          "display_name": "Compliance flags observed during scoring",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "coverage_guaranteed",
          "description": "",
          "display_name": "True when the vendor's chargeback-guarantee terms cover this decision (per-decision, not per-vendor)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "coverage_terms_uri",
          "description": "",
          "display_name": "Optional pointer to the vendor's coverage terms applicable to this decision",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "feature_payload_hash",
          "description": "",
          "display_name": "SHA-256 of the canonical JSON of the input features (tamper- evidence anchor for chargeback-dispute defence)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.change_function_event_status",
          "description": "Move an event between `tentative`, `definite` and `cancelled`. `actual` is not a move — `record_function_event_actuals` sets it with the count. Cancelling an event cancels its unposted banquet event order with it; a posted order cannot be cancelled, so neither can its event."
        },
        {
          "api_name": "hospitality.action.draft_banquet_event_order",
          "description": "Open the banquet event order for an event: a draft at revision 0 under the banquet outlet its charges will post through, priced in one currency, with the setup, service, AV and billing notes. An explicit `folio_id` fixes where the charges go; without one, a group's event posts to its block's master folio. One order per event; a cancelled event refuses."
        },
        {
          "api_name": "hospitality.action.record_function_event_actuals",
          "description": "After the function: record the actual attendance (and the set count, if not yet given) and move the event to `actual` — the status the banquet posting reads, because the billable count is the higher of the guarantee and the actual. Only a `definite` event can be actualised."
        },
        {
          "api_name": "hospitality.action.reschedule_function_event",
          "description": "Move an event: another day, another window, another space, another setup style — any subset. The diary and capacity checks run again over the result; a `do_not_move` event refuses a space change; an `actual` or `cancelled` event refuses outright. A window given without setup or teardown times drops the old ones."
        },
        {
          "api_name": "hospitality.action.set_function_event_counts",
          "description": "Set the expected, guaranteed or set attendance (any subset). The guaranteed number may be lowered only before `guarantee_due_at`; after it, it can only rise — the contract's rule. A count above the space's capacity in the event's setup style is refused. Refused on an `actual` or `cancelled` event."
        }
      ],
      "api_name": "hospitality.function_event",
      "description": "One booked use of a function space — a meeting, a reception, a dinner — on a date, in a window, in a setup style, with the expected / guaranteed / set / actual attendance. `tentative` holds the diary and commits nothing; `definite` is contracted; `actual` is recorded after the function with the attendance the banquet charges bill on; `cancelled` is terminal. The events of a group hang off its block through `function_event_for_block`.",
      "display_name": "Function event",
      "domain": "catering",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Event id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Event name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "event_type",
          "description": "",
          "display_name": "What kind of function",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Event status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "event_date",
          "description": "",
          "display_name": "The property-local day the event is on",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "start_at",
          "description": "",
          "display_name": "Doors open",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "end_at",
          "description": "",
          "display_name": "Doors close",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "setup_from",
          "description": "",
          "display_name": "When the setup crew may have the room (before start_at)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "teardown_until",
          "description": "",
          "display_name": "When the room must be clear again (after end_at)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "setup_style",
          "description": "",
          "display_name": "How the room is set",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "expected_count",
          "description": "",
          "display_name": "Expected attendance (the forecast number)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "guaranteed_count",
          "description": "",
          "display_name": "Guaranteed attendance — the contracted minimum the client bills on",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "guarantee_due_at",
          "description": "",
          "display_name": "When the guarantee is due; after it the number may only rise",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "set_count",
          "description": "",
          "display_name": "How many the room was set for",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "actual_count",
          "description": "",
          "display_name": "Actual attendance, recorded after the function",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "rental",
          "description": "",
          "display_name": "Function space rental agreed for the event",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "do_not_move",
          "description": "",
          "display_name": "The client was promised this room; reschedule refuses a space change",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes for the operating departments",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.update_function_space",
          "description": "Change a function space's name, status, capacities or notes. At least one parameter must be given. Retiring or taking a space out of service leaves its booked events standing for the operator to move; the diary refuses new ones."
        }
      ],
      "api_name": "hospitality.function_space",
      "description": "A bookable function room at a property — what the house sells by the hour. `capacities` declares how many the room holds per setup style, and a booking is checked against the style it asks for. A divisible room is one space with sections linked through `function_space_section_of`: an event in the combined room occupies every section, and a booked section blocks the whole.",
      "display_name": "Function space",
      "domain": "catering",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Function space id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Operator-facing code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Display name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Space status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "capacities",
          "description": "",
          "display_name": "Capacity per setup style",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "divisible",
          "description": "",
          "display_name": "Whether the room splits into sections",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "area_sqm",
          "description": "",
          "display_name": "Floor area in square metres",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "location",
          "description": "",
          "display_name": "Where in the house (floor, wing)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Operational notes (access, power, ceiling height)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.add_verified_invocation",
          "description": "Append one human-blessed NL→invocation example to a `grounding_scope`'s verified-invocations repository (the Snowflake verified-query pattern). `verified_by` is stamped from the invoking principal and `verified_at` from now() — provenance is server-observed, not caller-asserted. Rejected on a retired scope. Emits `grounding_scope.invocation_verified`."
        },
        {
          "api_name": "hospitality.action.retire_grounding_scope",
          "description": "Retire a `grounding_scope`: it stops feeding the retrieval context but stays referenceable for audit. Retiring an already-retired scope is rejected. Emits `grounding_scope.retired`."
        }
      ],
      "api_name": "hospitality.grounding_scope",
      "description": "An admin-curated grounding bundle for agents (design D11): a named subset of object types and metrics, synonym/sample-value hints, curation instructions, and a repository of verified invocations — human-blessed NL→action/metric/function examples with `verified_by`/`verified_at` provenance (the Snowflake verified-query pattern). Agent definitions reference one via `grounding_scope_ref`; the retrieval-context lane injects it deterministically, and answers can cite which verified example grounded them. Referential validity of `target_ref` against the registry is enforced by the consuming lane at read time, not frozen at write time — the registry evolves under the scope.",
      "display_name": "Grounding scope",
      "domain": "agents",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Scope id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "scope_key",
          "description": "",
          "display_name": "Stable key agent definitions reference",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "title",
          "description": "",
          "display_name": "Display name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "instructions",
          "description": "",
          "display_name": "Curator instructions injected into the retrieval context",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "object_type_refs",
          "description": "",
          "display_name": "Object-type api-names in scope",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "metric_refs",
          "description": "",
          "display_name": "MetricDef names in scope",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "synonyms",
          "description": "",
          "display_name": "Term→canonical hints + sample values (JSON; curator-owned shape)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "verified_invocations",
          "description": "",
          "display_name": "Human-blessed NL→invocation examples with provenance",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "scope_status",
          "description": "",
          "display_name": "Curation lifecycle",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "defined_by",
          "description": "",
          "display_name": "`principal.stable_id()` of the curator",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "defined_at",
          "description": "",
          "display_name": "When the scope was defined",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.change_block_status",
          "description": "Move a block between `tentative`, `definite` and `cancelled`. Becoming `definite` materialises the hold — every allocated night is taken out of house availability, and a night that cannot cover the allotment refuses the whole transition. Leaving `definite` returns every unpicked room. Going back to `tentative` is refused once anything has been picked up, since live reservations would lose the hold beneath them."
        },
        {
          "api_name": "hospitality.action.open_group_master_folio",
          "description": "Open the block's master folio — a `group_master` folio at the property, opened at zero in the given currency, linked through `block_has_master_folio`. Pickups' folios join it by binding a routing rule whose target is this folio (`bind_folio_routing_rule`), after which charges that rule takes post here instead of on the guest's bill. Refused on a cancelled block; a block that already has a master is a no-op. Emits `group_block.master_folio_opened`."
        },
        {
          "api_name": "hospitality.action.pick_up_block_reservation",
          "description": "Attach a reservation to the block, drawing its nights from the block's own allotment rather than from house availability. Each night moves one room from `blocked` to `sold`, so the booking ceiling is unchanged. Refuses a night the block has no unpicked room left on. Idempotent: a reservation already picked up from this block is a no-op, so a retried batch cannot draw the allotment down twice."
        },
        {
          "api_name": "hospitality.action.release_block",
          "description": "Return rooms the block is holding but nobody picked up to house availability. Without `through_date` every night is released; with it, only nights on or before that date — the shape a rolling cutoff needs. Safe to repeat: the amount released is recomputed from each cell, so a second run releases nothing."
        },
        {
          "api_name": "hospitality.action.set_block_allocation",
          "description": "Set how many rooms of one room type the block holds across a date range — one grid cell per night. On a `definite` block the change moves inventory immediately, maintaining `availability_day.blocked`. Refuses to drop a night's allotment below what has already been picked up."
        }
      ],
      "api_name": "hospitality.group_block",
      "description": "An operational room-block allotment at one property.              `status` decides whether the block holds inventory: a              `definite` block's unpicked remainder is carried in              `availability_day.blocked` and suppresses transient              sale, while `tentative` holds nothing. Pickup moves a              room from the block's hold to `sold` — net zero against              the booking ceiling — and the cutoff fields decide when              rooms nobody took go back to the house.",
      "display_name": "Group block",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Group block id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Block name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Operator-facing block code",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Block status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "start_date",
          "description": "",
          "display_name": "First stay night",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "end_date",
          "description": "",
          "display_name": "Departure date (exclusive of the last stay night)",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "cutoff_mode",
          "description": "",
          "display_name": "Cutoff mode",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cutoff_date",
          "description": "",
          "display_name": "Cutoff date (mode = fixed)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "cutoff_days",
          "description": "",
          "display_name": "Days before each stay night to release it (mode = rolling)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "cancel_returns_to",
          "description": "",
          "display_name": "Where a cancelled pickup's room goes",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "rate_plan_id",
          "description": "",
          "display_name": "Block-private rate plan",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "rooming_list_due",
          "description": "",
          "display_name": "Date the group owes its rooming list",
          "kind": "date",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.group_displacement_analysis",
      "description": "Frozen decision artefact recording the displacement analysis for a group block — Cornell SHA / Sheryl Kimes methodology, carried as audit-grade evidence of the rate floor and decision at the moment they were made. The embedded `forecast_snapshot` pins the transient-room / ADR / occupancy projection at decision time; recomputing against today's forecast undermines the audit trail. Mutation post-write is disallowed by convention — re-decisions create new analyses, not mutations.",
      "display_name": "Group displacement analysis",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Analysis id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "forecast_snapshot",
          "description": "",
          "display_name": "Forecast at decision time",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "variable_cost_per_room",
          "description": "",
          "display_name": "Variable cost per room",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "displaced_room_nights",
          "description": "",
          "display_name": "Displaced transient room nights",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "displaced_contribution",
          "description": "",
          "display_name": "Forfeited transient contribution",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "group_total_revenue",
          "description": "",
          "display_name": "Group total revenue (rooms + ancillary)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "group_variable_cost",
          "description": "",
          "display_name": "Group variable cost",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "minimum_acceptable_rate",
          "description": "",
          "display_name": "Minimum acceptable group ADR (rate floor)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "decision",
          "description": "",
          "display_name": "Decision recorded at the moment of analysis",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "counter_rate",
          "description": "",
          "display_name": "Counter offer ADR (set when decision = counter)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the decision was recorded",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decided_by",
          "description": "",
          "display_name": "Decider principal identifier (matches Principal::stable_id)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "decision_notes",
          "description": "",
          "display_name": "Decision notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.convert_group_request_to_reservation",
          "description": "Attach a reservation to a group_request, marking the group request as `confirmed`. Multi-property and multi-room blocks invoke this once per reservation; the action is idempotent on the (group_request, reservation) pair."
        },
        {
          "api_name": "hospitality.action.decline_group_request",
          "description": "Mark a group_request as `lost`. Records the decline reason."
        },
        {
          "api_name": "hospitality.action.quote_group_block",
          "description": "Record a displacement analysis for the target group_request and stamp the operator's decision. The analysis freezes the forecast at decision time; re-decisions create new analysis rows, never mutations."
        }
      ],
      "api_name": "hospitality.group_request",
      "description": "The inquiry stage for a group block — corporate booking, wedding, conference, sports team. Carries estimated F&B / spa / other ancillary revenue so displacement analysis can run against realistic assumptions, not just room nights. Multi-property blocks (a wedding booking rooms across two properties) hang off the same group_request via the M:N `group_request_for_property` link.",
      "display_name": "Group request",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Group request id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "group_size",
          "description": "",
          "display_name": "Estimated attendees",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "group_kind",
          "description": "",
          "display_name": "Group kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "requested_arrival",
          "description": "",
          "display_name": "Requested arrival",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "requested_departure",
          "description": "",
          "display_name": "Requested departure",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "room_block_size",
          "description": "",
          "display_name": "Room block size",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "estimated_fb_revenue",
          "description": "",
          "display_name": "Estimated F&B revenue",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "estimated_spa_revenue",
          "description": "",
          "display_name": "Estimated spa revenue",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "estimated_other_revenue",
          "description": "",
          "display_name": "Estimated other ancillary revenue",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Funnel status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "inquired_at",
          "description": "",
          "display_name": "When the inquiry was received",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.add_check_line",
          "description": "Order one item onto an open check. The line rings a variant — the one named, else the item's only or default variant — at the most specific `item_price` in force for the check's outlet, order kind and daypart on the check's business date, and snapshots that price, the item class's revenue category and the VAT: every tax rule in force for the class (its takeaway class off the premises), rounded per rule to the penny half away from zero, never down. An item with no class, no price in force, two equally specific prices, or a tax class with no rule refuses to ring. The snapshot is the financial record, so menu and rate edits never rewrite a rung sale. Course defaults to the item's `course_default`, then its class's, then 1. `modifiers` are the options chosen, each from a group the item uses: every group's bounds hold (the item's own override on the link beats the group's), and a required group (min above zero) must be answered. An option that adds a variant rings as its own line under this one (check_line_modifies_line) — at the option's price, else the variant's own, free under the `no` prefix — with that item's VAT and revenue category, quantity times this line's; an option with no variant, and any no / light / side prefix, joins this line's kitchen `instructions`. A present `fulfilled_at` lands the line already delivered, for feeds that record completed service rather than drive it (a POS export's lines carry no kitchen lifecycle to replay); live ordering omits it and the line lands `entered` for `fire_course` to pick up. A present `external_ref` routes the create through the `check_line_external_ref` business key, making a crashed-pass retry a replay instead of a duplicate line. The check's totals move with the line. Emits `fnb.line_added` for the line and each modifier line."
        },
        {
          "api_name": "hospitality.action.add_check_payment",
          "description": "Take one tender against an open check — the kernel is the system of record for a check's payments. `amount` is what the payment applies to the check and may not exceed `balance_due`, except in cash: cash `tendered` (default `amount`) above the balance applies the balance and gives the rest back as `change_given`. A `tip` rides only on card, room_charge and charge_to_key and stays out of `paid_total`. A card payment carries the terminal's references (`psp_reference`, `terminal_transaction_id`, `poiid`), never card data. A folio tender is bound server-side, the same on every path: room_charge requires `folio_id` and `reservation_id`, and the folio must be an open guest window of that booking's in-house stay at the check's own property (a house, master or company account, another booking's bill or another hotel's is refused); charge_to_key requires `credential_token`, the tapped key, resolved as `post_charge_by_credential` resolves it — its in-house stay names the folio and the reservation (a client `folio_id` or `reservation_id` that is not the key's is refused) and its per-charge and daily caps bound the payment and its tip; a tap is live, so a `paid_at` more than five minutes from the kernel's clock is refused and its postings are dated when the kernel records them, the clock the daily cap counts by. `guest_id`, the guest the server confirmed, must be the reservation's guest (a sharer signs on their own booking); it is recorded on the payment and on each folio charge as `signed_by`. They post to that open folio as the payment is taken: one charge per revenue category on the check (bucket = the outlet's usali_department, `charge_at_outlet`, `revenue_category`, its share of the VAT as tax applications), apportioned by largest remainder so the parts sum to the payment exactly, and the tip as a separate `gratuity` charge — a pass-through, never revenue — each emitting `charge.posted.recorded`, the payment linked to the charges and the check to the folio. The stay's package allowance is consumed with no prompt and no other tender: when the folio's in-house stay holds a live package component with a `daypart` — the check's own meal period, else an `all_day` credit — due on the check's business date (a breakfast the morning after its night), eaten at this outlet (an alternate outlet, or any when it names none) and with something left, it covers the payment's food and soft-drink shares up to what is left (never alcohol, never the tip): that part is recorded as the payment's `allowance_applied` and as a `package_component_consumption` (allowance and consumed both the amount, `consumed`, with the check's covers) through the one package write, linked to the payment (`consumption_for_check_payment`), whose balance-neutral package line on the folio is one of the payment's postings — and only the rest posts as the charges the guest owes, the covered shares shrunk with their VAT. One component per payment. Every payment records its share of each category and its VAT (`allocation`) over what earlier payments left, so a check paid in parts sums exactly. The payment that brings `balance_due` to zero settles the check — every line must be fulfilled, voided or comped by then — stamping `settled_at` (= `paid_at`), marking a seated table dirty and emitting `fnb.check_settled` with the per-tender breakdown. Business time: `paid_at` defaults to now. A present `external_ref` routes the create through the `check_payment_external_ref` business key, so a replayed payment is a no-op rather than a second payment. A room charge or charge-to-key is held to the stay's posting rules (`set_posting_rules`): a no-post stay refuses it (`no_post: …`), and so does one whose credit — the greater of its credit limit and its live card holds, less its bills' balance and the room still due — the posting and its tip would pass (`no_credit: …`). An override (`override_reason`) taken under a manager's own session lets the refusal through — an override is proven by who is signed in, not by who is named: the authoriser recorded on the payment (`override_kind`, `authorised_by`, `override_reason`) and emitted as `fnb.posting_override` is the session's staff member, and a body `authorised_by` must be that same one (`override_not_authenticated: …`). An override with nothing to override is refused (`nothing_to_override: …`), as is one under a session that is no manager's (`not_a_manager: …`) or none at all (`override_not_authenticated: …`). Emits `fnb.payment_added`."
        },
        {
          "api_name": "hospitality.action.collect_tray",
          "description": "The tray or trolley comes back from the room: the target room-service check's `delivery_status` goes `delivered` → `cleared`, stamping `cleared_at`, and the order leaves the room-service board. Only from `delivered`; the check may still be open. Emits `fnb.tray_collected`."
        },
        {
          "api_name": "hospitality.action.deliver_room_service",
          "description": "An in-room dining order reaches the room: the target room-service check's `delivery_status` goes `dispatched` → `delivered`, stamping `delivered_at` — what the board's lateness stops counting at. Only from `dispatched`. Emits `fnb.room_service_delivered`."
        },
        {
          "api_name": "hospitality.action.dispatch_room_service",
          "description": "An in-room dining order leaves the kitchen: the target room-service check's `delivery_status` goes `taken` → `dispatched`, stamping `dispatched_at`, the `runner_id` who takes it up and its `conveyance` (`tray` or `trolley` — what has to come back down). Only a room_service check, and only from `taken`; the check may be open or already settled. Emits `fnb.room_service_dispatched`."
        },
        {
          "api_name": "hospitality.action.fire_course",
          "description": "Send one course's entered lines to the kitchen — the service-total moment of a round (Simphony). Lines move to `fired`, and one kitchen ticket is cut per prep station represented in the course (Toast coursing model); lines whose item has no station binding land on an unrouted (expo) ticket. A modifier line fires with its parent, on its parent's ticket. Firing an already-fired course again picks up only newly-entered lines. Emits `fnb.course_fired`."
        },
        {
          "api_name": "hospitality.action.settle_check",
          "description": "Close an open check by paying its whole balance with one tender — the convenience over `add_check_payment`, which it records one payment through: `amount` = the check's `balance_due`, `gratuity` the payment's tip, `settlement_reference` its `psp_reference`, `settled_at` its `paid_at`, and `folio_id`, `reservation_id`, `guest_id` and `credential_token` bound exactly as `add_check_payment` binds them — a room charge names its reservation here too, and a charge-to-key its tapped key — and a room charge consumes the stay's package allowance exactly as `add_check_payment`'s does. Every line must be fulfilled, voided, or comped. A check with nothing due (everything voided / comped, or already paid by earlier payments) settles with no tender and no payment. The gratuity rides any tender here — it is the check's discretionary service charge collected with the tender, which a replayed history carries on cash too. A seated table flips to dirty. A room charge or charge-to-key is held to the stay's posting rules, with a signed-in manager's override (`override_reason`), exactly as `add_check_payment` holds it. Re-settling an already-settled check is a no-op."
        }
      ],
      "api_name": "hospitality.guest_check",
      "description": "The order/check entity: opened against a service table (or takeaway / room service) in a meal period on a business date, owned by a server, open until its payments (check_payment) cover what is due — the payment that brings `balance_due` to zero settles it and marks the table dirty (Simphony model). Carries its running totals, kept by every action that moves them: `subtotal` (live lines, modifier lines included, voided and comped out), `tax_total`, `total_due` (subtotal plus add-on tax), `paid_total` (tips excluded), `tip_total` and `balance_due`. Mutates only through the fnb actions.",
      "display_name": "Guest check",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "number",
          "description": "",
          "display_name": "Check number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Check lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "order_kind",
          "description": "",
          "display_name": "How the order reaches the guest",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "daypart",
          "description": "The meal period the check was opened in — the daypart an `item_price` row may be narrowed to (the bar's lunch price) and a package's meal allowance is matched on. Stamped from the outlet's `meal_periods` when a live check names none. Absent = opened outside the outlet's periods, or before it had any, which only matches rows with no daypart.",
          "display_name": "Meal period the check rings in",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "business_date",
          "description": "The hotel date at open (the date the property has open, else its local calendar day) — what every line's price and VAT resolve on, however late in the service it rings. Revenue still reports under the business day of the settle or the folio posting, as the ledger dates it.",
          "display_name": "Business date the check belongs to",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "covers",
          "description": "",
          "display_name": "Covers seated",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "When the check opened",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "opened_by",
          "description": "",
          "display_name": "Principal::stable_id() of the opening principal",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "server_id",
          "description": "",
          "display_name": "Owning server",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "settled_at",
          "description": "",
          "display_name": "When the check settled",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "subtotal",
          "description": "",
          "display_name": "Gross of the live lines (modifier lines included; voided and comped out)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "tax_total",
          "description": "",
          "display_name": "Tax on the live lines, inclusive and add-on",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "total_due",
          "description": "",
          "display_name": "What the guest owes: subtotal plus add-on (non-inclusive) tax",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "paid_total",
          "description": "",
          "display_name": "What the check's payments applied to it, tips excluded",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "tip_total",
          "description": "",
          "display_name": "Tips taken with the check's payments (a pass-through, not revenue)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "balance_due",
          "description": "",
          "display_name": "total_due less paid_total — the payment that brings it to zero settles the check",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "room",
          "description": "The stay's room number when a check opened for an in-house stay (always, for room service), snapshotted for the runner and the board; the stay itself is the `check_for_stay` link.",
          "display_name": "Room number of the stay the check is for",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "promised_at",
          "description": "The time quoted to the guest: what `open_check` was given, else twenty minutes after opening for a breakfast order and thirty for any other. The board marks an order late once it passes undelivered.",
          "display_name": "When the order was promised at the room",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "delivery_status",
          "description": "taken → dispatched → delivered → cleared, each step only from the one before (`dispatch_room_service`, `deliver_room_service`, `collect_tray`). Independent of the check's own open / settled: a tray is collected after the bill is signed.",
          "display_name": "Where the in-room order is on its dispatch sheet",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "dispatched_at",
          "description": "",
          "display_name": "When the order left the kitchen",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "delivered_at",
          "description": "",
          "display_name": "When the order reached the room",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cleared_at",
          "description": "",
          "display_name": "When the tray or trolley was collected",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "runner_id",
          "description": "",
          "display_name": "Who ran the order up",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "conveyance",
          "description": "",
          "display_name": "Tray or trolley",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for replayed / connector-landed checks",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.guest_conversation",
      "description": "Per-guest cross-channel conversation container. Two-level model per the 2026 vendor convergence: this object is the per-guest anchor; per-reservation segmentation is the optional `anchor_reservation_ref` plus the temporal `guest_conversation_anchored_to_reservation` link. Channel lives on `guest_message`, not here. Lifecycle `state` tracks the outermost outcome so operator UIs route on it; opt-out scope is the in-thread TCPA / WhatsApp / GDPR signal accumulator that the outbound-action validators honour.",
      "display_name": "Guest conversation",
      "domain": "messaging",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Conversation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "person_ref",
          "description": "",
          "display_name": "The guest (person) this conversation belongs to",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "When the conversation opened (first message timestamp)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_message_at",
          "description": "",
          "display_name": "Most recent message timestamp (denormalised, §B.1)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_message_at_computed_at",
          "description": "",
          "display_name": "Refresh-discipline pair for `last_message_at`",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "state",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "state_reason",
          "description": "",
          "display_name": "Optional short reason on terminal state transitions",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "anchor_reservation_ref",
          "description": "",
          "display_name": "Optional reservation anchor (pre-arrival / in-house / departing)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "language",
          "description": "",
          "display_name": "BCP-47 tag of the guest's most recently detected reply language",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "message_count",
          "description": "",
          "display_name": "Count of messages in this conversation (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "inbound_count",
          "description": "",
          "display_name": "Count of inbound messages (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "triage_count",
          "description": "",
          "display_name": "Count of triages issued in this conversation (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "escalation_count",
          "description": "",
          "display_name": "Count of triages that escalated (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "message_count_computed_at",
          "description": "",
          "display_name": "Refresh pair for the four counters (§B.1 discipline)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "opt_out_scope",
          "description": "",
          "display_name": "Opt-out signals received in-thread (TCPA / WhatsApp / GDPR)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "redactions_applied",
          "description": "",
          "display_name": "Aggregated redaction-kind set across the conversation",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "sentiment_trajectory",
          "description": "",
          "display_name": "Rolling sentiment scores [-1.0, 1.0] per message (bounded length)",
          "kind": "array",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.add_guest_feedback_entry",
          "description": "Follow up on a piece of guest feedback. An `internal` entry is the house talking to itself (read on the floor); a `guest` entry is what the guest is told, and is written into the guest's open conversation at the house as a reply — on the channel they last wrote on, behind the same opt-out gate and WhatsApp window a triage's reply obeys, sent by the delivery lane when the channel is one the kernel carries. With no open conversation, or a guest who has said not to be written to, the entry is the record and says why it was not sent. `delegate_to` hands the case to a role (hubOS's delegated profile). An open case moves to `in_progress`; a closed one takes nothing more. The author is the staff member behind the principal, when it is one. Emits `guest_feedback.entry_recorded`."
        },
        {
          "api_name": "hospitality.action.close_guest_feedback",
          "description": "Finish a case: it goes `closed` and takes nothing more. A resolved case, a compliment or a suggestion closes as it is; a complaint closed without being resolved needs the `reason` (the guest withdrew it, could not be reached). Closing a closed case is a no-op. Emits `guest_feedback.closed`."
        },
        {
          "api_name": "hospitality.action.raise_work_order_from_feedback",
          "description": "Raise a work order for the fault a complaint or a suggestion reports — hubOS's fault linked to a complaint. The order is created through `create_work_order`'s own path against `room_id`, or the feedback's stay's room when none is named, and links back (`work_order_answers_feedback`); an open case moves to `in_progress`. The feedback's text is never copied onto the order — it is the guest's, the order is read by engineering — so the title and description are the house's words. Refuses a compliment and a closed case. A complaint may need more than one order; each call raises one. Emits `guest_feedback.work_order_raised`."
        },
        {
          "api_name": "hospitality.action.resolve_guest_feedback",
          "description": "Say what the house did about a complaint or a suggestion: the case goes `resolved` with the resolution and the time. Resolving it again with the same words is a no-op, with other words is refused; a closed case is refused, and so is a compliment — a compliment is closed, not resolved. Emits `guest_feedback.resolved`."
        },
        {
          "api_name": "hospitality.action.set_checkout_satisfaction",
          "description": "Record how satisfied the guest said they were at checkout — hubOS's checkout satisfaction on the quality item — as `satisfaction`, 1 (not at all) to 5 (very). Only once the stay the feedback is about has departed (checked out): refused on feedback about no stay, or about a stay still to come or in house. The same score again writes nothing; another replaces it (the event carries the previous). Emits `guest_feedback.checkout_satisfaction_set`."
        },
        {
          "api_name": "hospitality.action.set_guest_feedback_labels",
          "description": "File a piece of guest feedback under the house's labels — hubOS's quality labels on the quality item. `labels` is the whole set, by code (empty clears it): each an active label of the feedback's house; a retired one the feedback already carries may stay, and none may be added. The feedback's `labels` and its `guest_feedback_has_label` edges move together; the same set again writes nothing. Any status — labelling files the case, it does not follow it up. Emits `guest_feedback.labels_set`."
        }
      ],
      "api_name": "hospitality.guest_feedback",
      "description": "What a guest told the house — a `complaint`, a `compliment` or a `suggestion` (hubOS's guest feedback form) — and where the house has got to with it: `open` when recorded, `in_progress` once someone follows up (an entry on its thread, a work order raised from it), `resolved` with what the house did, `closed` when the case is finished. A complaint names its category. The feedback holds no guest name or contact: the guest (`guest_feedback_for_person`), the stay it is about (`guest_feedback_for_stay`) and the message it came from (`guest_feedback_from_message`) are links, and the guest's own words (`text`) are marked as the guest's. The thread is its `guest_feedback_entry` rows; `delegated_to_role` is the role the case is with. It is filed under the house's own labels (`labels`, `guest_feedback_has_label`) and, once the stay has departed, carries the guest's `checkout_satisfaction` (1–5).",
      "display_name": "Guest feedback",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Guest feedback id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Complaint, compliment or suggestion",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "What it is about (cleanliness, noise, billing, service, food, maintenance, other); required on a complaint",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "text",
          "description": "",
          "display_name": "What the guest said, in their words or the desk's summary of them",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "received_at",
          "description": "",
          "display_name": "When the guest gave it",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "channel",
          "description": "",
          "display_name": "How it reached the house: in person, by phone, a survey, a review, or the messaging channel of the message it came from",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Open, in progress, resolved or closed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "delegated_to_role",
          "description": "",
          "display_name": "The role the case is with (hubOS's delegated profile)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "resolution",
          "description": "",
          "display_name": "What the house did about it",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "resolved_at",
          "description": "",
          "display_name": "When it was resolved",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "closed_at",
          "description": "",
          "display_name": "When the case was closed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "close_reason",
          "description": "",
          "display_name": "Why a complaint was closed without being resolved",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "labels",
          "description": "",
          "display_name": "The codes of the house's labels it is filed under — the same set as its guest_feedback_has_label edges",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "checkout_satisfaction",
          "description": "",
          "display_name": "How satisfied the guest said they were at checkout, 1 (not at all) to 5 (very) — a departed stay's",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "checkout_satisfaction_at",
          "description": "",
          "display_name": "When the checkout satisfaction was recorded",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.guest_feedback_entry",
      "description": "One follow-up on a piece of guest feedback, written for an `internal` audience (the house to itself: `internal_text`, read on the floor) or the `guest` (`guest_text`, marked as the guest's) — hubOS's thread, which keeps the internal and the guest follow-up as separate texts. A guest entry is sent into the guest's open conversation at the house as a reply (`guest_feedback_entry_sent_as` names the message); when there is none, or the guest has said not to be written to, the entry is the record and `undelivered_reason` says why it was not sent. An entry may hand the case to a role (`delegated_to_role`). Written once; the author is the staff member linked by `guest_feedback_entry_by_staff`, or the principal on the entry's event.",
      "display_name": "Guest feedback entry",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Guest feedback entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "audience",
          "description": "",
          "display_name": "Internal (the house to itself) or guest (what the guest was told)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "internal_text",
          "description": "",
          "display_name": "The internal follow-up — set on an internal entry only",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "guest_text",
          "description": "",
          "display_name": "What the guest was told — set on a guest entry only",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "recorded_at",
          "description": "",
          "display_name": "When the follow-up was written",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "delegated_to_role",
          "description": "",
          "display_name": "The role this entry handed the case to",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "undelivered_reason",
          "description": "",
          "display_name": "Why a guest entry was not sent to the guest",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.guest_id_document",
      "description": "A minimal government-ID record captured for statutory guest registration — document type, issuing country, expiry, and a verification result. The raw number / scan is never stored; it stays in an external vault reached via `guest_id_document_references_external_record`.",
      "display_name": "Guest ID document",
      "domain": "people",
      "interfaces": [
        "hospitality.interface.retention_bound"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Document record id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "document_type",
          "description": "",
          "display_name": "Document type",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "issuing_country",
          "description": "",
          "display_name": "Issuing country (ISO 3166-1 alpha-2)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "holder_name",
          "description": "",
          "display_name": "Holder name as printed",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "document_number_last4",
          "description": "",
          "display_name": "Last 4 glyphs of the document number (never the full number)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "expiry_date",
          "description": "",
          "display_name": "Document expiry date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "verification_status",
          "description": "",
          "display_name": "Verification status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "verification_method",
          "description": "",
          "display_name": "Verification method",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "verified_at",
          "description": "",
          "display_name": "Verified at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "captured_at",
          "description": "",
          "display_name": "Captured at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "retain_until",
          "description": "",
          "display_name": "Retain-until (storage-limitation clock)",
          "kind": "date",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.record_guest_message_delivery",
          "description": "**Delivery-worker only.** The worker's steps on a queued outbound message, in order: `sending` claims it before the vendor is called (`delivery_attempts` goes up; a waitlist offer's `claim_token_hash` lands on its entry, or the message is failed when a newer offer has replaced it), then `sent` with the vendor's id (the Twilio MessageSid, the SMTP Message-ID) or `failed` with the reason; `failed` straight from `queued` is a message that was never sendable. A message handed over three times is failed instead of claimed. One already `sent` or `failed` is left as it is — the first outcome recorded is the one that happened; any other move is refused. Emits `guest_message.delivery.recorded`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.retry_guest_message_delivery",
          "description": "Send a failed message again: back to `queued` for the delivery adapter, to the address it was first meant for or — when there was none — the profile's contact as it is now (the desk has just added the number). Refused for a message that is not failed, for one the adapter has already handed over three times (write the guest a new message), and when the guest has since asked the house to stop."
        },
        {
          "api_name": "hospitality.action.triage_guest_message",
          "description": "Emit a typed triage against an inbound `guest_message`. Inserts a `message_triage` row in `suggested` state with an immutable `enrichment_snapshot` captured at issue time. Validators enforce: triage-for-inbound-only, recommended-action-in-operator-allowlist (when recommended_action_kind = chain_typed_action), injection-intent-escalation-only (forces escalate path on injection.suspected), PAN-redaction. Decision lifecycle flows through apply / reject / escalate / expire."
        }
      ],
      "api_name": "hospitality.guest_message",
      "description": "Typed shadow of a single inbound or outbound message. The full payload lives in the external messaging system (Twilio, WhatsApp Business, Mews Conversations, HiJiffy / Canary inbox, OTA Messaging API, voice-IVR transcript store); Aerandil holds the audit-grade decision-time fields plus an optional `external_record_reference` pointer to the full content. PCI-DSS 4.0 hard-validator-enforced: no Luhn-matching digit sequence may appear in `redacted_excerpt`. The `content_hash` is the tamper-evidence anchor.",
      "display_name": "Guest message",
      "domain": "messaging",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Message id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "conversation_ref",
          "description": "",
          "display_name": "The conversation container",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "channel",
          "description": "",
          "display_name": "Communication channel",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "channel_thread_id",
          "description": "",
          "display_name": "External system thread identifier (Booking.com conversation_id, Twilio MessageSid, WhatsApp wamid, etc.)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "direction",
          "description": "",
          "display_name": "Inbound (from guest) or outbound (to guest)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "received_at",
          "description": "",
          "display_name": "When the message arrived (inbound) or was sent (outbound)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "author_kind",
          "description": "",
          "display_name": "Who composed the message (guest / staff_human / staff_agent / system)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "author_principal",
          "description": "",
          "display_name": "`principal.stable_id()` of the author when not the guest",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "redacted_excerpt",
          "description": "",
          "display_name": "PII-stripped excerpt for the audit trail (default cap 280 chars)",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "content_hash",
          "description": "",
          "display_name": "SHA-256 of the original message bytes (tamper-evidence)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "word_count",
          "description": "",
          "display_name": "Optional word count (triage-cost analytics)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "detected_language",
          "description": "",
          "display_name": "BCP-47 — may differ from `conversation.language` (mid-thread switches)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "redactions_applied",
          "description": "",
          "display_name": "Per-message redaction kinds",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "ingress_principal",
          "description": "",
          "display_name": "`principal.stable_id()` of the service or human that recorded the message",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "template_id",
          "description": "",
          "display_name": "WhatsApp Business template id (required outside the 24-hour service window)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "purpose",
          "description": "",
          "display_name": "Why the house wrote: a reply, a service message, marketing, or a waitlist offer",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "delivery_state",
          "description": "",
          "display_name": "Where the kernel's own send stands: queued, sending (claimed by the worker), sent, or failed",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "delivery_address",
          "description": "",
          "display_name": "The phone number or email address the message goes to, resolved when it was recorded",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "vendor_message_id",
          "description": "",
          "display_name": "The delivery vendor's id for the send (Twilio MessageSid, SMTP Message-ID)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "delivery_attempted_at",
          "description": "",
          "display_name": "When the adapter last tried to send it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "delivery_attempts",
          "description": "",
          "display_name": "How many times the adapter has tried to send it",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "delivery_error",
          "description": "",
          "display_name": "Why the last send failed, in words the desk can act on",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "waitlist_entry_ref",
          "description": "",
          "display_name": "The waitlist entry whose offer this message carries; its claim token is minted when the message is sent",
          "kind": "reference",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.housekeeping_briefing",
      "description": "The day's instructions from the head housekeeper to the floor — hubOS's 'indications', per building, user and date. One house (`housekeeping_briefing_at_property`), one business date, one author (`housekeeping_briefing_by`), the text. A second post the same day is a second briefing, not an edit; readers list a day's briefings newest first by `posted_at`.",
      "display_name": "Housekeeping briefing",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Briefing id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "The house's day the briefing is for",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "text",
          "description": "",
          "display_name": "The instructions",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "posted_at",
          "description": "",
          "display_name": "When it was posted",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_housekeeping_credit_rule",
          "description": "Stop a housekeeping credit rule pricing new tasks. Tasks it already priced keep their credits; a room-type rule's task type falls back to the house's rule for every room type, if there is one. Retiring a retired rule is a no-op; authoring its key again (`set_housekeeping_credit_rule`) revives it. Emits `housekeeping_credit_rule.retired`."
        }
      ],
      "api_name": "hospitality.housekeeping_credit_rule",
      "description": "What one kind of housekeeping task is worth at a property (OPERA's housekeeping credits; the unit of hubOS's pacing report): `credits` — the workload unit a supervisor balances attendants' sheets in and an attendant's shift target is counted in — and optionally `standard_minutes`, what the work should take, against which the pacing report measures each cleaning's deviation. Keyed by (task type, room type): a rule naming a room type prices that task type in rooms of that type, a rule naming none prices it in every other room. Task creation stamps the most specific active rule's figures onto the task (`create_housekeeping_task`, `generate_housekeeping_tasks`), so a task keeps what it was worth when minted. Retiring a rule prices nothing new; authoring its key again revives it.",
      "display_name": "Housekeeping credit rule",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Housekeeping credit rule id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "task_type",
          "description": "",
          "display_name": "The task type this rule prices",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "room_type_id",
          "description": "",
          "display_name": "The room type this rule prices the task type in; absent = every room type without a rule of its own",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "credits",
          "description": "",
          "display_name": "Credits one task of this kind is worth (zero or more)",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "standard_minutes",
          "description": "",
          "display_name": "Minutes the work should take (1 to 1440); the pacing report's deviation is measured against it",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Active (prices new tasks) or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the rule was retired",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing description",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.assign_housekeeping_task",
          "description": "Assign (or reassign) the task to an attendant — the edge re-points, so a task has at most one current assignee and the per-attendant task sheet is simply the projection over (assignee, business_date). Refuses a task already complete or skipped. Name `group_id` instead of `staff_member_id` (exactly one, 2026-10-14) to give the task to a worker group — an active housekeeping, public_areas or other group at the task's house, whose `floors` (when it names any) include the floor of the task's room or area; the member who starts it takes it. A task is assigned to a person or a group, never both: each assignment replaces the other edge. Emits `housekeeping.task.assigned`, or `housekeeping.task.assigned_to_group` with the group's members. The task's `assignee_id` / `group_id` follow the edge in the same write (2026-10-15)."
        },
        {
          "api_name": "hospitality.action.block_housekeeping_task",
          "description": "A typed obstacle stands between the attendant and the room — do-not-disturb, guest in room, access refused. The task stays on the board with the reason attached, expecting a retry (start_housekeeping_task) or a deliberate skip; it never silently vanishes into an attendant's memory."
        },
        {
          "api_name": "hospitality.action.complete_housekeeping_task",
          "description": "Mark a housekeeping task complete and update the room's status. Without `resulting_status`, only the unambiguous cascade runs: a departure / vacant_pickup / deep_clean / inspection completed against a dirty or pickup room promotes the cleaning axis to clean, and a stayover / turndown leaves it alone. With `resulting_status`, the attendant states the room's cleaning status at Finish (OPERA's Companion sets it at the same tap) and it is written in the same transaction, through the same refusal the status report applies: a room under an active out-of-order block today cannot have its cleaning status written — return_room_to_service decides. When the most specific active checklist template for the task's type and room type is `required_for_completion`, the task refuses to complete until a result for it is recorded (`record_checklist_result`). On an inspection task, `inspection_rating` (1–5) and `inspection_outcome` record the supervisor's verdict on the inspection and on the cleaning it rated — the most recent completed non-inspection task for the same room and business date, linked by `inspection_rates_task` (refused when there is none) — and a failed outcome mints a high-priority re-clean of the same type (`origin` reclean), linked by `housekeeping_task_from_inspection`. Without `resulting_status`, a passed inspection leaves the room inspected and a failed one dirty. A cleaning never needs an inspection to count. A runner's session may not state the room — neither `resulting_status` nor an inspection's verdict — and is refused (403): a room's condition is the word of the attendant who cleaned it and the supervisor who inspected it; a runner's finish leaves the room to the type-based cascade. A public area's task (2026-10-14) has no room to state: `resulting_status` is refused on it, and a `public_area_inspection`'s verdict rates the area's latest completed clean of the day and re-mints a failed one for the area, as a room's inspection does. A `minibar` or `laundry` task (2026-10-14) may carry what it `consumed` — catalogued materials and quantities. A minibar's are minibar products, consumed from the room's own minibar store or the house's default minibar cart and charged at their sale price (the house's minibar tax rules derived) through post_charge to the bill of the stay that was in the room at `completed_at` — when the minibar was checked, not when a queued finish lands (2026-10-17) — while it is in house; with nobody in house to charge (the room empty at the check, or its guest gone since) the stock is still consumed, uncharged, and `minibar.uncharged.recorded` hands the desk its late charge, naming that stay or the last to leave by the check, never a guest who arrived after it; in a shared room `stay_id` names whose bill (in house, and arrived by the check). A laundry's are issued from the house's default laundry store to the room. Any other task type refuses `consumed`. `self_rating` (2026-10-15, 1–5) is the attendant's own rating of the work, stored on the task beside — never instead of — the supervisor's `inspection_rating`; it states nothing about the room, so a runner may send it; refused on an inspection. A finish is the holder's (2026-10-17): an attendant, runner or engineer who does not hold a task a colleague holds is refused (403), while the housekeeping supervisor and the desk finish it on the holder's behalf; a task nobody holds, or a worker group's, is taken by the staff member who finishes it, as a start takes it (a non-member is refused, 403). `self_rating` is the holder's own word, refused (403) to anyone else — the supervisor, the desk, an agent. `expected_holder_id` names the holder the caller saw (a finish queued on a handset): refused (422) when someone else, a group or nobody holds the task by the time it lands."
        },
        {
          "api_name": "hospitality.action.mark_request_overdue",
          "description": "Stamp `overdue_at` on a request whose due time has passed with the work still open — the effect of the standing `request_overdue` sweep (every five minutes over open tasks with a due time passed and no stamp yet), callable by hand too. Refused when the task has no `due_at` or its time has not passed; a finished or skipped task, or one already stamped, is a no-op. Emits `housekeeping.task.overdue` once, which the `task_overdue` push rides to the holder — the group's members when a group holds it, the house's supervisors when nobody does."
        },
        {
          "api_name": "hospitality.action.reclaim_housekeeping_task",
          "description": "Take an assigned task that has not been started from whoever holds it (hubOS's reclaim): the assignment edge re-points to the acting staff member in one write. Refused once the task is started (in progress, or blocked after a start), finished or skipped; refused on a task nobody holds (start it to take it) and on one held by a worker group (a member takes it by starting it). A supervisor (housekeeping or front-office supervisor, manager on duty, general manager, owner) — or a caller that is not a staff member — may name `assignee_id` to reclaim it onto someone at the task's house; anyone else reclaims only for themself. Reclaiming onto the current holder is a no-op, checked before the state — a Take sent again after the taker started the room lands once. `expected_holder_id` names the holder the caller saw (a Take queued on a handset): refused when someone else holds the task by the time it lands, so a queued Take replays safely. Two Takes or assigns of one task queue on its holder lock. Emits `housekeeping.task.reclaimed` with the previous assignee; the new assignee's handset is nudged as for an assignment."
        },
        {
          "api_name": "hospitality.action.record_checklist_result",
          "description": "Record the ticked checklist against a housekeeping task — the attendant's list at a clean, the supervisor's at an inspection. The template is `template_id`, or (absent) the most specific active template for the task's type and room type; a named template must apply to the task. `items` answer the template's items by `code` (each at most once; a code the template does not have refuses; a `count` item's `value` is a whole number); the `score` is derived — the template's `check` items answered ok ÷ all its `check` items, × 100, rounded half up (none when the template has no checks). The recorder is the staff member behind the session, taken from the principal as `start_housekeeping_task` self-assigns. One write, replay-safe per task × template: an identical repeat writes nothing, even once the task is finished or its template retired or revised; different answers replace this task's earlier result in place while the task is open; a complete or skipped task refuses. Photos attach to the result (`host_kind = checklist_result`). Emits `checklist_result.recorded`."
        },
        {
          "api_name": "hospitality.action.record_housekeeping_task_entry",
          "description": "Add a line to a housekeeping task's thread — a `note` about this piece of work (\"guest asked for feather-free pillows\", \"stain by the window\"), as opposed to the day's briefing to the whole floor. A photo hangs off the line as an attachment (`host_kind = housekeeping_task_entry`). The author is the staff member behind the principal when there is one (a handset session), linked through `housekeeping_task_entry_recorded_by`; `recorded_by` names the principal either way. `recorded_at` defaults to now and takes the handset's own time when its offline queue sends the line later. Refused on a skipped task (its skip_reason says why); a finished task still takes notes. Replay-safe under the apply route's Idempotency-Key; a handset that mints the line's id (`entry_id`, a v4 or v7 UUID — so a photo taken offline can wait for the line) may send it again under a fresh key: the same words on the same task are a replay, anything else under that id is refused (409). Emits `housekeeping.task.entry_recorded` with the kind and the author, never the words."
        },
        {
          "api_name": "hospitality.action.retime_housekeeping_task",
          "description": "Move a request's due time — the desk's \"make it four, not three\". Open work only (pending, in progress or blocked); a finished or skipped task keeps its time. The new `due_at` is held to the house's day as on creation (no earlier than the start of the day before the house's today), and a pending task whose new time falls on another day moves to that day's sheet (`business_date`); a started one keeps its day. A time still to come clears an `overdue_at` the sweep had stamped, so the request can turn overdue again. The same time again is a no-op. Emits `housekeeping.task.retimed` with the old and new times."
        },
        {
          "api_name": "hospitality.action.skip_housekeeping_task",
          "description": "Record work deliberately not done, with the reason — the guest declined service, the room is being retired for the day. Terminal: a skipped task never restarts (mint a fresh one if the work comes back)."
        },
        {
          "api_name": "hospitality.action.start_housekeeping_task",
          "description": "The attendant is in the room: pending / blocked → in_progress, stamping started_at (the cleaning-duration feed) and clearing any block reason — starting a blocked task is the retry after a do-not-disturb. An unassigned task started by a staff-member principal (a handset session) is assigned to that attendant in the same write — picking up the next room needs no supervisor round trip; an already-assigned task is left as assigned (reassignment stays a supervisor verb). A task assigned to a worker group (2026-10-14) is taken by the member who starts it — the group edge re-points to them — and a staff-member session outside the group is refused (403); a caller that is not a staff member starts it and leaves it the group's. A start is the holder's (2026-10-17): an attendant, runner or engineer who does not hold a task a colleague holds is refused (403) — they Take it first — while the housekeeping supervisor and the desk start it on the holder's behalf and the assignment stands. `expected_holder_id` names the holder the caller saw (a start queued on a handset): refused (422) when someone else, a group or nobody holds the task by the time it lands."
        }
      ],
      "api_name": "hospitality.housekeeping_task",
      "description": "A unit of housekeeping work attached to a room: a stayover clean, a departure clean, a deep clean, a turndown, an inspection. Minted by `generate_housekeeping_tasks` (which derives the day's board from occupancy + due-outs and stamps a `generation_key` so re-runs converge) or by `create_housekeeping_task` for ad-hoc work (`origin` says which, and marks a failed inspection's re-clean). Assignment is the `housekeeping_task_assigned_to` edge; the per-attendant task sheet is a projection over (assignee, business_date), never a second object to keep in sync. A public area's work (2026-10-14) is the same object linked `housekeeping_task_for_area` instead of to a room — a task is for a room or an area, never both, and the `public_area_*` types are an area's alone; and a task may be assigned to a `worker_group` (`housekeeping_task_assigned_to_group`) instead of a person, until a member starts it. A task is also a request (2026-10-15): `requested_by` says who asked (a guest's request links the stay, `housekeeping_task_requested_by_stay`), `due_at` when it is due, and it carries a thread of notes (`housekeeping_task_entry`). `area_id`, `assignee_id` and `group_id` copy the area, person and group edges onto the row.",
      "display_name": "Housekeeping task",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Task id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "task_type",
          "description": "",
          "display_name": "Task type",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "requested_at",
          "description": "",
          "display_name": "Requested at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Service date the task belongs to",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "generation_key",
          "description": "",
          "display_name": "Replay key stamped by generate_housekeeping_tasks (hk:{room}:{date}:{type}, or hk:area:{area}:{date} for a public area's daily clean); absent on manually minted tasks",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the attendant entered the room",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "Completed at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "block_reason",
          "description": "",
          "display_name": "Why the task is blocked (do-not-disturb workflow)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "skip_reason",
          "description": "",
          "display_name": "Why the task was deliberately not done",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "priority",
          "description": "",
          "display_name": "Priority",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "credits",
          "description": "",
          "display_name": "Credits the task is worth (zero or more) — stamped at creation from the property's housekeeping credit rule for its task type and room type, or set by hand; absent when the house prices none",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "standard_minutes",
          "description": "",
          "display_name": "Minutes the work should take (1 to 1440), stamped beside credits; the pacing report's deviation is measured against it",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "inspection_rating",
          "description": "",
          "display_name": "The supervisor's score, 1 (poor) to 5 (excellent): on an inspection task, the score it gave; on a cleaning task, the score the latest inspection of it gave",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "inspection_outcome",
          "description": "",
          "display_name": "passed, or failed (the room went back for a re-clean): on an inspection task, its verdict; on a cleaning task, the verdict of the latest inspection of it",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "origin",
          "description": "",
          "display_name": "Where the task came from: generated (the day's board, with a generation_key), manual (create_housekeeping_task), reclean (a failed inspection sent the cleaning back) or from_triage (a triaged guest request). Absent on tasks minted before it was stamped",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "self_rating",
          "description": "",
          "display_name": "The rating the attendant gave their own work at Finish, 1 (poor) to 5 (excellent) — complete tasks only, never an inspection; the supervisor's score is inspection_rating",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "requested_by",
          "description": "",
          "display_name": "Who asked for the work: guest, desk, floor, pms (the house's own state — the generator's room board) or automation (a schedule, an agent or a service). Distinct from origin, which says how the task was minted. Absent on tasks minted before it was stamped",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "due_at",
          "description": "",
          "display_name": "When the work is due — the guest's 'by three o'clock'; absent when the task has no time of its own. Its urgency (overdue, soon, scheduled) is the housekeeping_requests read's",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "area_id",
          "description": "",
          "display_name": "The public area the task is for (its object id) — the housekeeping_task_for_area edge's target; absent on a room's task",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "room_id",
          "description": "",
          "display_name": "The room the task is for (its object id) — the housekeeping_task_for_room edge's target; absent on a public area's task",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "overdue_at",
          "description": "",
          "display_name": "When the request_overdue sweep found the due time passed with the work still open — the overdue push went out then; absent until it does, and cleared by a re-time into the future",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "assignee_id",
          "description": "",
          "display_name": "The staff member who holds the task (their object id) — the housekeeping_task_assigned_to edge's target; absent while nobody or a worker group holds it",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "group_id",
          "description": "",
          "display_name": "The worker group that holds the task (its object id) — the housekeeping_task_assigned_to_group edge's target; absent while a person or nobody holds it",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.housekeeping_task_entry",
      "description": "One line on a housekeeping task's thread — what the attendant, the runner or the supervisor says about this piece of work (\"guest asked for feather-free pillows\", \"stain on the carpet by the window\"), as opposed to the day's briefing to the whole floor. A `note` with its words; a photo hangs off the line as an attachment (`host_kind = housekeeping_task_entry`). Linked to its task (`housekeeping_task_entry_for_task`) and to the staff member who wrote it when one did (`housekeeping_task_entry_recorded_by`); `recorded_by` names the principal either way, and `recorded_at` is the handset's own time when it queued the line offline. Written by `record_housekeeping_task_entry`; append-only — a correction is another line.",
      "display_name": "Housekeeping task entry",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Housekeeping task entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "What the line records: a note",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "text",
          "description": "",
          "display_name": "The words on the line (required for a note)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "recorded_at",
          "description": "",
          "display_name": "When the line was written — the handset's own time when it queued offline",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "recorded_by",
          "description": "",
          "display_name": "The principal that wrote the line (`user:…`, `agent:…`, `service:…`, `system`)",
          "kind": "string",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.incidental_hold_policy",
      "description": "One property's rule for the card hold a check-in suggests: a basis (the allowance per night, the room total plus the allowance per night, the room total plus a flat amount, or a flat amount — OPERA's authorization rules 7, 2, 9 and 5), the allowance, and optionally the reservation guarantee it is for (a prepaid stay holds incidentals only, a card-guaranteed one the room as well). Rules are keyed by `code` within the property; `suggest_incidental_hold` picks the active rule for the reservation's guarantee, falling back to the rule for any guarantee. Since 2026-10-19 a rule may also name a room type, a rate plan and a booking source (OPERA's advanced rules, most specific first), four more of OPERA's formulas (the room alone, per person, a percentage of the room), and the night audit's top-up takes the rule's allowance as what the card must still cover beyond the bill.",
      "display_name": "Incidental hold policy",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Incidental hold policy id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Policy code — the rule's identity within the property",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "guarantee",
          "description": "",
          "display_name": "The reservation guarantee this rule is for; absent = any guarantee without a rule of its own",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "basis",
          "description": "",
          "display_name": "How the suggested hold computes",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "allowance",
          "description": "",
          "display_name": "The per-night allowance (`per_night`, `room_and_per_night`) or the flat amount (`room_plus_flat`, `flat`)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Whether check-ins draw on this rule",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing description",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "room_type_ref",
          "description": "",
          "display_name": "The room type this rule is for; absent = any room type",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "rate_plan_ref",
          "description": "",
          "display_name": "The rate plan this rule is for; absent = any rate plan",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "The booking source this rule is for; absent = any source",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "percent",
          "description": "",
          "display_name": "The percentage of the room total `room_plus_percent` adds (10 = 10%)",
          "kind": "decimal",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.record_ingredient_cost",
          "description": "Record a supplier-invoice cost as a per-property `ingredient_supplier_price` row. Closes the prior open row (`effective_to = new.effective_from`); the new row becomes the active price for the recipe pour-cost rollup. Downstream `refresh_recipe_pour_cost` is a separate flow."
        }
      ],
      "api_name": "hospitality.ingredient",
      "description": "F&B ingredient catalog row (identity + canonical unit + allergen disclosure). Supplier price is property-scoped via the `ingredient_supplier_price` companion per design- spec §B.3; the `refresh_recipe_pour_cost` flow reads the active price row for the recipe's property. Source-side allergen flags roll up into `recipe.allergens`.",
      "display_name": "Ingredient",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Ingredient id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Ingredient code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Ingredient name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "Category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "unit_canonical",
          "description": "",
          "display_name": "Canonical unit",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "par_quantity",
          "description": "",
          "display_name": "Par stock level",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "allergen_flags",
          "description": "",
          "display_name": "Allergen flags (EU 14 list)",
          "kind": "array",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.ingredient_supplier_price",
      "description": "Per-property supplier price for an ingredient. The pour-cost rollup reads the active row (`effective_to IS NULL`) for the recipe's property. Append-only ledger via `effective_from` / `effective_to` — supplier invoices land as fresh rows; the handler closes the prior open row.",
      "display_name": "Ingredient supplier price",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Price id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "ingredient_ref",
          "description": "",
          "display_name": "Ingredient",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "property_ref",
          "description": "",
          "display_name": "Property",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "cost_per_unit_canonical",
          "description": "",
          "display_name": "Cost per canonical unit",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "unit_canonical_at_record",
          "description": "",
          "display_name": "Canonical unit (snapshot at record time)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "supplier_code",
          "description": "",
          "display_name": "Supplier code",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "invoice_reference",
          "description": "",
          "display_name": "Supplier invoice reference",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "received_at",
          "description": "",
          "display_name": "Received at (supplier invoice timestamp)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "Effective from",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Effective to (null = currently in effect)",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.cancel_inspection_round",
          "description": "Abandon an open round, with the `reason`. Its checkpoint results and any faults raised stay. The template's `open_round_id` is cleared and the occurrence is still owed: the template keeps its due date — so the sweep starts a fresh round at once if that date has passed — unless `next_due_on` moves it (the plant room is flooded until Thursday). Cancelling a cancelled round is a no-op; a completed one refuses. Emits `inspection_round.cancelled`."
        },
        {
          "api_name": "hospitality.action.complete_inspection_round",
          "description": "Finish an open round — hubOS's 'finalized by'. Refuses while any of the round's checkpoints has no result, or a recorded one has fewer photos (attachments whose upload landed) than `photo` items, naming them, unless `partial` is true with a `partial_reason` (the walk could not reach them); `observations` are the walker's on the round as a whole. Stamps `completed_at` (the handset's time when it says) and who finished it, clears the template's `open_round_id`, and moves the template on as a maintenance plan moves on: the next due date is the last plus the interval for a `fixed` template (skipping slots a late round overran) or the completion's property-local date plus the interval for a `floating` one. A retired template is not moved. Completing a completed round is a no-op; a cancelled one refuses. Emits `inspection_round.completed`."
        },
        {
          "api_name": "hospitality.action.record_checkpoint",
          "description": "Record one checkpoint of an open round — hubOS's 'submit checkpoint'. `position` names the checkpoint in the round's own list; `items` answer its items by `code` (each at most once; a code it does not have refuses): a `check` needs `ok`, a `reading` its `value`, a `text` its `text`, a `photo` nothing (its photo attaches to the result), a `section` heading takes no answer, and any item may say `ok: false`. A reading with limits that the walker does not judge is judged by them: ok within `min` / `max`, an anomaly outside; the walker's own `ok` always stands. `result_id` is the result's id when the walker mints it (a v4 or v7 UUID — a handset with no signal queues the checkpoint's photo against it): the same id again is this result, an id another result holds is refused as a duplicate, and a checkpoint recorded already under another id is refused naming it. `anomalies` is derived — the items answered not ok — and the round's totals are re-folded from its results. The result is linked to the checkpoint's room or asset and to the staff member behind the session. One write per checkpoint: recording it again replaces the result in place while the round is open, and an identical repeat writes nothing (even after the round has closed). `raise_fault` raises a work order through `create_work_order`'s own path — in the public area `raise_fault.area_id` names (never with a room_id), else against `raise_fault.room_id`, else the checkpoint's room, else the room its asset stands in, and for the checkpoint's asset — linked to the round (`work_order_from_round`) and the checkpoint (`work_order_from_checkpoint`) and counted on both; a fault with the title of one already raised from this checkpoint is that fault, not another. Photos attach to the result (`host_kind = checkpoint_result`). Emits `inspection_round.checkpoint_recorded`."
        }
      ],
      "api_name": "hospitality.inspection_round",
      "description": "One executed walk of a round template (`inspection_round_from_template`) — hubOS's revision. Carries the template's name and family, the due date it answers (`due_on`) and the template's `checkpoints` as they stood when it started, so a revision of the template never changes a walk under way and the record shows the list it was walked against (`template_revision`, the template's revision it copied). Started by hand or by the automation lane when the template falls due (`started_by` names the principal; `inspection_round_started_by` the staff member when one did); each checkpoint is recorded as a `checkpoint_result` (`checkpoint_result_in_round`); `checkpoints_recorded`, `total_anomalies` and `total_faults` are the fold of those results, kept by the verbs. `open` → `completed` (every checkpoint recorded, or `partial` with a reason; moves the template's next due date on) or `cancelled` (with a reason; the occurrence is still owed). Faults raised from it are work orders (`work_order_from_round`).",
      "display_name": "Inspection round",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Inspection round id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "The template's name when the round started",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "family",
          "description": "",
          "display_name": "The template's family when the round started",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "due_on",
          "description": "",
          "display_name": "The due date this round answers — the template's next_due_on when it started",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "checkpoints",
          "description": "",
          "display_name": "The template's checkpoints as they stood when the round started",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "open, completed or cancelled",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the round was started (the handset's own time when it says)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "started_by",
          "description": "",
          "display_name": "The principal that started it (`user:…`, `agent:…`, `service:…`, `system` — the automation lane when the template fell due)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "When the round was finished",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "completed_by",
          "description": "",
          "display_name": "The principal that finished it",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "partial",
          "description": "",
          "display_name": "Finished with checkpoints unrecorded (with partial_reason)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "partial_reason",
          "description": "",
          "display_name": "Why checkpoints were left unrecorded",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "observations",
          "description": "",
          "display_name": "The walker's observations on the round as a whole",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cancelled_at",
          "description": "",
          "display_name": "When the round was cancelled",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancel_reason",
          "description": "",
          "display_name": "Why the round was cancelled",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "checkpoints_recorded",
          "description": "",
          "display_name": "How many of its checkpoints have a result",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "total_anomalies",
          "description": "",
          "display_name": "Anomalies across its checkpoints (items answered not ok)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "total_faults",
          "description": "",
          "display_name": "Work orders raised from its checkpoints",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "template_revision",
          "description": "",
          "display_name": "The template's revision when the round started — the walk the checkpoints were copied from",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.record_invoice_dispatch",
          "description": "Record that an issued document reached its recipient. The fact of a send, not the sending: the HTTP route owns the SMTP round trip, which happens after commit and outside the action transaction, and calls this with the message id it got back. A document may be sent more than once and does not change state by being sent, so this only ever appends an event. A send that failed records nothing — the route surfaces the error to the desk instead."
        }
      ],
      "api_name": "hospitality.invoice",
      "description": "The numbered, sealed document a folio becomes when it is issued — a tax invoice, or a credit note reversing one. Carries snapshots of the issuer, the recipient, every ledger line and the per-rate tax bands, so the document renders from itself and never changes after issue.",
      "display_name": "Invoice",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "number",
          "description": "The legal document number, e.g. `STAN-2026-000123`. Allocated from the property's series inside the issuing transaction and never reissued.",
          "display_name": "Invoice number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "`invoice`, or `credit_note` reversing one.",
          "display_name": "Document kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "series",
          "description": "The sequence this number came from — `{property_id}:{kind}:{yyyy}`. Series are annual and per document kind: France requires numbering that is chronological and continuous within a series, and a credit note sharing the invoice run cannot satisfy that.",
          "display_name": "Number series",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "sequence_value",
          "description": "The bare counter value `number` was rendered from. Kept so a gap in a series is detectable without parsing the formatted number.",
          "display_name": "Sequence value",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "issued_on",
          "description": "The property's business date of issue — an action parameter, never `Utc::now()`. This is the document's date for tax purposes.",
          "display_name": "Issue date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "The instant of issue. Also the renderer's fixed timestamp, which is what makes a re-render byte-identical to the stored PDF.",
          "display_name": "Issued at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "supply_from",
          "description": "First day of the period supplied — the stay's arrival. Printed as the Leistungszeitraum.",
          "display_name": "Supply from",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "supply_to",
          "description": "Last day of the period supplied — the stay's departure, which is also the UK basic tax point for a completed stay.",
          "display_name": "Supply to",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "due_on",
          "description": "Only set when `amount_due` is above zero — the company-billed case.",
          "display_name": "Payment due",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "net_total",
          "description": "",
          "display_name": "Net total",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tax_total",
          "description": "",
          "display_name": "Tax total",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "gross_total",
          "description": "",
          "display_name": "Gross total",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "amount_paid",
          "description": "",
          "display_name": "Amount paid",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "amount_due",
          "description": "Zero on a settled stay; the transferred balance on a folio settled to a company's A/R account.",
          "display_name": "Amount due",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tax_summary",
          "description": "Per-rate bands, derived from the charges' own `tax_application` rows — the rule's rate, not a recomputation.",
          "display_name": "Tax bands",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "lines",
          "description": "The printed lines, snapshotted at issue.",
          "display_name": "Ledger lines",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "issuer",
          "description": "The property's legal identity, as it printed.",
          "display_name": "Issuer",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "recipient",
          "description": "Who the document was addressed to, as it printed.",
          "display_name": "Recipient",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "recipient_kind",
          "description": "Unmarked twin of `recipient.kind`, so the board can filter company documents without reading the PII snapshot.",
          "display_name": "Recipient kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "simplified",
          "description": "Issued under the small-amount rule (UK £250, DE €250) without recipient details. Never true for a company recipient — the whole point of their document is input-tax recovery.",
          "display_name": "Simplified",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "language",
          "description": "BCP 47 tag the document was rendered in.",
          "display_name": "Language",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "content_hash",
          "description": "SHA-256 over the canonical snapshot. With `previous_hash` this chains issued documents, which is what the modern fiscal regimes actually check — they put integrity on a chain over issued records rather than on the integer being dense.",
          "display_name": "Content hash",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "previous_hash",
          "description": "The previous issued document's `content_hash` — one chain per property, spanning invoices AND credit notes together, absent only for the property's first. Deliberately NOT per series: Spain's Orden HAC/1177/2024 art. 7(c) requires a system to produce \"una unica cadena\", and art. 7(d) that the chain carry both the alta and the anulacion records; Basque TicketBAI chains to the previous issued invoice regardless of series. The *number* series stays per kind; the chain is one strand. Backwards here makes Veri-factu a rebuild rather than a configuration.",
          "display_name": "Previous content hash",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "render_version",
          "description": "Which renderer produced the stored PDF. A later engine upgrade may change bytes, so a re-render under a different version is a copy, not the original.",
          "display_name": "Renderer version",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "note",
          "description": "",
          "display_name": "Note",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.item_class",
      "description": "The behaviour bundle a menu item inherits: the tax class its VAT resolves through (with an optional takeaway override — cold food is zero-rated off the premises, VAT Notice 709/1 §4), the revenue category a room charge posts to the folio by (food / soft drinks / alcohol / other), the default course, and whether discounts and service charge apply.",
      "display_name": "Item class",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Class code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Class name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "tax_class",
          "description": "The tax class code the item's tax rules target (`tax_rule.tax_class`) when it is eaten on the premises — dine-in, in-room, breakfast. A class with no rule in force refuses to ring rather than selling at an unknown rate; a zero-rated class carries an explicit 0% rule.",
          "display_name": "Tax class",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "takeaway_tax_class",
          "description": "The tax class used instead when the item leaves the premises (takeaway, delivery). Absent = the same as `tax_class` — hot food and drinks are standard-rated either way.",
          "display_name": "Takeaway tax class",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "revenue_category",
          "description": "",
          "display_name": "Revenue category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "course_default",
          "description": "",
          "display_name": "Default course number for items of this class",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "discountable",
          "description": "",
          "display_name": "Discounts apply",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "service_chargeable",
          "description": "",
          "display_name": "Service charge applies",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing description",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.item_price",
      "description": "One menu price for a variant (price_for_variant), optionally narrowed to an outlet (price_at_outlet), an order kind (room-service pricing), a daypart (the bar's lunch price) and an effective window (a price rise from a date). A sale takes the most specific row in force — outlet beats any outlet, then order kind, then daypart — and two equally specific rows refuse to ring. `amount` is the price the guest sees: VAT-inclusive wherever the tax rule is inclusive (UK), net where it adds on.",
      "display_name": "Item price",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Price id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Menu price",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "order_kind",
          "description": "",
          "display_name": "Only for this order kind (absent = any)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "daypart",
          "description": "",
          "display_name": "Only in this daypart (absent = any)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "First business date the price applies (inclusive; absent = always)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Last business date the price applies (inclusive; absent = open-ended)",
          "kind": "date",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.bump_ticket",
          "description": "Station-side fulfilment: the ticket leaves the board (`bumped`, timestamped) and its lines move to `fulfilled`. Voided / comped lines on the ticket are left as they are. Idempotent on an already-bumped ticket. Emits `fnb.ticket_bumped`."
        },
        {
          "api_name": "hospitality.action.recall_ticket",
          "description": "Re-open a bumped ticket (Toast recall/unfulfill — bumped by mistake, or the pass rejected the plate). The ticket returns to the board as `recalled` and its fulfilled lines drop back to `fired`; bumping again re-fulfils. Emits `fnb.ticket_recalled`."
        }
      ],
      "api_name": "hospitality.kitchen_ticket",
      "description": "The fulfilment view of one course's fired lines at one prep station — fire_course cuts one ticket per station per course (Toast coursing model). fired → bumped, with recall re-opening a bumped ticket; expo in-prep/ready states are bump-bar UI flow, not record states. Bump screens tail the fnb.* events on /v1/events/stream; the all-day aggregate is a render over the same events.",
      "display_name": "Kitchen ticket",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Ticket id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Ticket lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "course_number",
          "description": "",
          "display_name": "Course",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "fired_at",
          "description": "",
          "display_name": "When the ticket hit the board",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "bumped_at",
          "description": "",
          "display_name": "When the ticket bumped (most recent bump)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "recalled_at",
          "description": "",
          "display_name": "When the ticket was recalled (most recent recall)",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.void_labour_cost",
          "description": "Void the target labour ledger entry — terminal, the amount stays immutable as the audit record. Emits `labour.cost_voided.decided` so the `daily_labour` projection folds the amount back out."
        }
      ],
      "api_name": "hospitality.labour_ledger_entry",
      "description": "One posted labour-cost amount — an auditable business record (provenance, void trail, optional link to staff and shift) exactly like a revenue charge. Aerandil records what the payroll system of record posted; it never computes pay. Multi-day pay-period imports post to `business_date` un-prorated in v1 (the accrual rule is the payroll system's); `period_start` / `period_end` preserve the source window.",
      "display_name": "Labour ledger entry",
      "domain": "labour",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "USALI labour-cost kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "department",
          "description": "",
          "display_name": "USALI 12 Schedule 15 department",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "period_start",
          "description": "",
          "display_name": "Pay-period start",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "period_end",
          "description": "",
          "display_name": "Pay-period end",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Source",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "External ref",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "posted_at",
          "description": "",
          "display_name": "Posted at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "voided",
          "description": "",
          "display_name": "Voided",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.longevity_assessment",
      "description": "The opaque binding to an external clinical record for a member's longevity / diagnostic assessment. **Structural payload-empty contract** — no biomarker fields, no lab-value fields, no diagnostic-detail fields, ever. The clinical detail lives in the external clinical record (referenced via the mandatory `external_record_reference` link in commit 6); Aerandil holds only the opaque pointer. HIPAA / GDPR Article 9 special-category data stays in the source-of-truth system; Aerandil is structurally not a covered entity for this data.",
      "display_name": "Longevity assessment",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Assessment id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "assessed_at",
          "description": "",
          "display_name": "When the assessment was administered",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "summary_tags",
          "description": "",
          "display_name": "Operator-defined free-text tags (no clinical detail)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "modalities_tested_ref",
          "description": "",
          "display_name": "Modalities covered (informational; outcomes live in external record)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Operator's non-clinical observations (no biomarker / lab-value detail)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.claim_lost_found_item",
          "description": "A guest has asked for a found item: link them as its claimant and mark it `claimed` (`claimed_at`); it waits to be handed back or sent. Name the guest (`person_id`) or a stay of the item's house (`stay_id`), whose reservation's guest is the claimant — so the floor can claim for the guest in a room without reading the guest's name. A claim by the same person again is a no-op; a claim by someone else re-points the claimant. A returned or disposed item is refused. Emits `lost_found_item.claimed`."
        },
        {
          "api_name": "hospitality.action.dispose_lost_found_item",
          "description": "Stop keeping a found item: `disposed` (terminal) with when, how (`method`: donated, destroyed, handed to the police, released to the finder, other) and why (`reason` — \"unclaimed past the 90-day hold\"). Disposing of it again is a no-op; a returned item is refused. Emits `lost_found_item.disposed`."
        },
        {
          "api_name": "hospitality.action.return_lost_found_item",
          "description": "Hand a found item back, or send it, to the person it belongs to — `person_id`, or the claimant when none is named. The item is `returned` (terminal) with the time and the person linked. Returning it again to the same person is a no-op, to anyone else is refused; a disposed item is refused, and so is an unclaimed item with no person named. Emits `lost_found_item.returned`."
        },
        {
          "api_name": "hospitality.action.store_lost_found_item",
          "description": "Put a found item away, or move it, saying where (`storage_location`). A found item becomes `stored`; a claimed one stays `claimed` with its new place. Storing it again in the same place is a no-op; a returned or disposed item is refused. Emits `lost_found_item.stored`."
        }
      ],
      "api_name": "hospitality.lost_found_item",
      "description": "Something left behind and found at the house. `found` where it was found — a room (`lost_found_item_in_room`) or a named place (`found_location`) — and by whom (`lost_found_item_found_by`, the staff member behind the handset); `stored` with where it is kept; `claimed` once a guest asks for it (`lost_found_item_claimed_by`); `returned` to the person it went to (`lost_found_item_returned_to`, `returned_at`); or `disposed` — donated, destroyed, handed to the police or released to the finder — with when and why. `returned` and `disposed` are terminal. The `description` says what the thing is, never whose: the owner is a link. The `id` is the tag written on the bag.",
      "display_name": "Lost & found item",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Item tag (written on the bag)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "What it is — colour, make, distinguishing marks; not whose",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "What kind of thing it is (what a claim is matched on, and how it is kept)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "found_at",
          "description": "",
          "display_name": "When it was found",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "found_location",
          "description": "",
          "display_name": "Where it was found, when not in a room (\"lobby bar\", \"pool deck\")",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Found, stored, claimed, returned or disposed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "storage_location",
          "description": "",
          "display_name": "Where it is kept (\"HK office shelf 3\", \"front-office safe\")",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "claimed_at",
          "description": "",
          "display_name": "When a guest asked for it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "returned_at",
          "description": "",
          "display_name": "When it was handed back or sent",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "disposed_at",
          "description": "",
          "display_name": "When the house stopped keeping it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "disposal_method",
          "description": "",
          "display_name": "Donated, destroyed, handed to the police, released to the finder, or other",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "disposal_reason",
          "description": "",
          "display_name": "Why it was not kept any longer (\"unclaimed past the 90-day hold\")",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.loyalty_program",
      "description": "A loyalty program — chain-points (Marriott Bonvoy), consortium-soft (LHW Leaders Club), membership-club (Soho House — defers to proposal 0005), or recognition-only (Aman Inner Circle). The four-archetype `program_type` is labelling over more granular axes (qualification cycle, expiry, redemption, `has_points_subsystem`, `is_published_to_member`); the schema admits configurations between archetypes — Preferred I Prefer = `consortium_soft + has_points_subsystem`; SLH-Hilton = `consortium_soft AND chain_points`. Recognition is always-on across all archetypes — the `program_type` does not gate it.",
      "display_name": "Loyalty program",
      "domain": "loyalty",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Program id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Program code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Program name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "program_type",
          "description": "",
          "display_name": "Program type (labelling)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "has_points_subsystem",
          "description": "",
          "display_name": "Carries a points-liability subsystem",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "lifetime_status_enabled",
          "description": "",
          "display_name": "Lifetime tiers offered (Marriott Lifetime Platinum etc.)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "expiry_rule",
          "description": "",
          "display_name": "Expiry rule",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "expiry_value_months",
          "description": "",
          "display_name": "Expiry threshold (months; required when expiry_rule != none)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "tier_qualification_policy",
          "description": "",
          "display_name": "Tier qualification policy",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "redemption_blackout_policy",
          "description": "",
          "display_name": "Redemption blackout policy (gated)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "family_pooling_policy",
          "description": "",
          "display_name": "Family pooling policy (gated)",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "allowed_channels",
          "description": "",
          "display_name": "Allowed booking channels (SLH-Hilton-style allow-list)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "co_brand_card_partners",
          "description": "",
          "display_name": "Co-brand card partners (free-text until points_partner ships)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "Effective from",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Effective to (null when current)",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.raise_maintenance_work_order",
          "description": "Raise a scheduled plan's work order — the automation sweep's effect when `next_due_on` comes, or the desk pulling it forward. Creates the order through `create_work_order`'s own path against the plan's room (and for the plan's asset, when it names one), scheduled for the due date and stamped with the plan's id (`maintenance_plan_id`), links it to the plan, and marks the plan `raised` until the order closes. A no-op on a plan already raised (one open order per plan, so a repeated firing raises nothing); refuses a retired plan. Emits `maintenance_plan.raised`."
        },
        {
          "api_name": "hospitality.action.retire_maintenance_plan",
          "description": "Stop a plan: it raises nothing more. An order it already raised stays open for the engineer to finish or void. Retiring a retired plan is a no-op."
        }
      ],
      "api_name": "hospitality.maintenance_plan",
      "description": "A room's preventive-maintenance cadence: the work order to raise (title, category, severity), how often — `fixed` to the calendar or `floating` from the last completion, every `interval_days` — and when it is next due. Holds state only; the automation lane is its clock: a sweep over plans whose due instant has come — `next_due_at`, the house's own midnight starting `next_due_on` — raises the work order (`raise_maintenance_work_order`, the plan goes `raised`), and a monitor on that order reaching complete or void moves the plan on (`advance_maintenance_plan`, back to `scheduled` with the next due date). One open order per plan at a time.",
      "display_name": "Maintenance plan",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Maintenance plan id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "title",
          "description": "",
          "display_name": "What the raised work order says to do",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Detail carried onto each raised work order",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "Category of the raised work orders",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "severity",
          "description": "",
          "display_name": "Severity of the raised work orders",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cadence",
          "description": "",
          "display_name": "Fixed to the calendar, or floating from the last completion",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "interval_days",
          "description": "",
          "display_name": "Days between occurrences",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "next_due_on",
          "description": "",
          "display_name": "The date the next work order is due",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "next_due_at",
          "description": "",
          "display_name": "The instant the house's day reaches next_due_on — its local midnight in the property's timezone (UTC midnight when it has none); what the raise sweep compares with the lane's clock, written beside next_due_on by the plan's verbs",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Scheduled (waiting for its date), raised (its order is open), or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "last_raised_at",
          "description": "",
          "display_name": "When the plan last raised a work order",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "last_completed_on",
          "description": "",
          "display_name": "The property-local date its last work order was completed",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the plan was retired",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.add_property_to_management_agreement",
          "description": "Bring one property under the target HMA's coverage (the Strand-pattern portfolio agreement covers several hotels; the Condor/PHR pattern covers one). Refuses if the agreement is not active; the OneToMany link refuses a property already covered elsewhere."
        },
        {
          "api_name": "hospitality.action.terminate_management_agreement",
          "description": "End the target HMA — the recorded kind carries why (performance-test failure, sale of the property, for-cause default, or without-cause). Terminal; the agreement row stays as the audit record of the terms that governed."
        }
      ],
      "api_name": "hospitality.management_agreement",
      "description": "A hotel management agreement (HMA): one owner company engages the operator to run one or more properties. Carries the terms that make the relationship inspectable — fee schedule, delegation thresholds, and the two-prong performance test — but never computes a fee: computed amounts post as `brand_operator_cost_entry` rows (counterparty = operator) and the monthly `owner_statement` records what was reported. Field defaults calibrate from executed EDGAR-filed HMAs and the HVS contract surveys (synth/docs/archetype4-research.md).",
      "display_name": "Management agreement",
      "domain": "management",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Agreement code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Agreement name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "commenced_on",
          "description": "",
          "display_name": "Commencement date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "initial_term_years",
          "description": "",
          "display_name": "Initial term (years; third-party norm 5–10, brand-manager 20+)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "base_fee_pct_revenue",
          "description": "",
          "display_name": "Base management fee as a fraction of total operating revenue (survey band 0.02–0.04, 0.03 the mode)",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "incentive_kind",
          "description": "",
          "display_name": "Incentive-fee structure",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "incentive_pct",
          "description": "",
          "display_name": "Incentive fee fraction (of AGOP above priority, or of the attainment-scaled base — per incentive_kind)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "owner_priority_return_pct",
          "description": "",
          "display_name": "Owner's priority return hurdle as a fraction of investment (survey band 0.08–0.12); incentive_kind = pct_agop_over_priority only",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "accounting_fee_monthly",
          "description": "",
          "display_name": "Centralised-accounting charge per month (Chatham disclosure: $1,200–1,500)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "revenue_management_fee_monthly",
          "description": "",
          "display_name": "Centralised revenue-management charge per month (Chatham disclosure: $1,000)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "capex_approval_threshold",
          "description": "",
          "display_name": "Spend above this needs owner approval (PHR: $25k contracts, $100k major renovations)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "contract_approval_threshold",
          "description": "",
          "display_name": "Contract value above this (or term > 1 year) needs owner approval",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "budget_line_flex_pct",
          "description": "",
          "display_name": "Per-line-item overspend the operator may commit without approval (Condor: 0.10)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "budget_aggregate_flex_pct",
          "description": "",
          "display_name": "Aggregate budget overspend ceiling without approval (Condor: 0.05)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "noi_budget_floor_pct",
          "description": "",
          "display_name": "Performance test prong 1: minimum actual/budgeted NOI (Strand & Condor: 0.90)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "revpar_index_floor_pct",
          "description": "",
          "display_name": "Performance test prong 2: minimum RevPAR index vs prior year (Strand: 1.00 per-hotel / 0.95 portfolio; Condor: 0.90)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "cure_window_months",
          "description": "",
          "display_name": "Months the operator has to cure a performance failure",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "radius_miles",
          "description": "",
          "display_name": "Area-of-protection radius — no competing managed hotel within it (Strand: 5; Condor: 3)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "terminated_on",
          "description": "",
          "display_name": "Termination effective date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "termination_kind",
          "description": "",
          "display_name": "Why the agreement ended",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "termination_reason",
          "description": "",
          "display_name": "Free-text termination note",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "External ref",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_material",
          "description": "Take a material out of the catalogue for good — discontinued, replaced. Stamps retired_at and the reason; its code stays taken. Nothing more of it is received; what the stores still hold may be issued, consumed and counted down. Retiring a retired material is a no-op. Retired by the role whose family it is (2026-10-17): an engineer's session the spare parts and consumables, a housekeeping supervisor's the rest (403)."
        }
      ],
      "api_name": "hospitality.material",
      "description": "A part or a product a house keeps — a spare part, a consumable, an amenity, linen, a minibar product, a laundry item, a cleaning product (hubOS's materials, laundry products and minibar families). Keyed by `code` within its catalogue: a brand's (`material_at_brand`, shared by the brand's houses) or one house's (`material_at_property`); `define_material` with a code the catalogue holds re-states it in place. Counted in its `unit`, which is fixed once any store holds it. A minibar product carries the `sale_price` a minibar task charges the in-house guest. `active`, or `retired` for good (the code stays taken). How much each store holds is its `stock_level` rows.",
      "display_name": "Material",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "The catalogue's code for it — unique within the catalogue",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "What it is, in words (\"Cola 330 ml\", \"GU10 lamp 5 W\")",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "family",
          "description": "",
          "display_name": "What kind of thing it is: spare_part, consumable, amenity, linen, minibar_product, laundry_item, cleaning_product, other",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "unit",
          "description": "",
          "display_name": "The unit every quantity of it is counted in",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "sale_price",
          "description": "",
          "display_name": "What one unit sells for — what a minibar task charges the guest per unit",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Notes on it",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "active, or retired (final)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "defined_at",
          "description": "",
          "display_name": "When it was first put in the catalogue",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When it was retired",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "retire_reason",
          "description": "",
          "display_name": "Why it was retired — discontinued, replaced",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.book_modality_session",
          "description": "Book a wellness-modality session for the target membership. For clinical modalities (`requires_medical_licensing = true`), the matching `external_record_reference_id` parameter is mandatory at action-time — the handler rejects the booking otherwise. Non-clinical modalities (cold/heat/contrast therapy, resistance training, recovery, mindfulness, sleep work) book without the clinical-seam parameter."
        },
        {
          "api_name": "hospitality.action.cancel_membership",
          "description": "Terminate the membership. Structural change only — the refund / settlement consequence is recorded separately via Aerandil's existing charge / payment model. Closes the current `membership_at_tier` link, marks the membership as `terminated`, and pauses billing on the subscription."
        },
        {
          "api_name": "hospitality.action.change_tier",
          "description": "Transition the target membership to a new tier. Supersedes the existing `membership_at_tier` link (sets `effective_to`) and creates a new link with `effective_from`. Dues adjustment is handled via the optional subscription amount parameter; the refund / proration consequence depends on the tier's `prorates_on_change` flag."
        },
        {
          "api_name": "hospitality.action.freeze_membership",
          "description": "Pause the membership. Sets `access_status = frozen` on the membership; when the operator's freeze includes a billing pause, also sets the subscription's `suspension_state` (with `suspension_started_at`)."
        },
        {
          "api_name": "hospitality.action.prescribe_protocol",
          "description": "Prescribe a wellness protocol for the target membership. Creates the protocol row, links it to the membership, and attaches the typed modality references via `protocol_references_modalities` (a single protocol may reference multiple modalities — HBOT 2×/week + contrast bathing 3×/week + resistance training daily)."
        },
        {
          "api_name": "hospitality.action.record_assessment",
          "description": "Record a longevity assessment binding for the target membership. Creates a `longevity_assessment` row (no clinical detail in payload — structural contract per commit 5) and links it to the mandatory `external_record_reference` that holds the actual record. The HIPAA / GDPR Article 9 boundary is structural: Aerandil holds only the opaque pointer."
        },
        {
          "api_name": "hospitality.action.transfer_legacy_membership",
          "description": "Transfer a legacy-lifetime membership to a successor person. Only valid when the underlying tier has `transferable = true` (Aman Founders' legacy shape). Updates `person_ref` on the membership, refreshes the `membership_for_person` link, and tags the membership's dependency_kind = transferred_legacy to preserve the original-Founder lineage in audit."
        },
        {
          "api_name": "hospitality.action.unfreeze_membership",
          "description": "Resume the membership. Inverse of `freeze_membership`. Sets `access_status = active` and clears the subscription's `suspension_state` (to `none`)."
        }
      ],
      "api_name": "hospitality.membership",
      "description": "The individual access-bearing record. Distinct from `membership_account` because the account is the durable identity (one per primary member) and the membership is the access entitlement (one per primary + N per dependents). The Mindbody Premier factoring of Membership vs Pricing Option (commit 3's subscription) carries the distinction. One operator-journey from Saint Haven member → Saint Haven Founders → Saint Haven Founders + Equinox Hotel partner-tier is three rows; the underlying `membership_account` row is one.",
      "display_name": "Membership",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Membership id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "dependency_kind",
          "description": "",
          "display_name": "Relationship to the account (primary / spouse / child / registered / legacy)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "enrolment_kind",
          "description": "",
          "display_name": "How the membership came to exist",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "access_status",
          "description": "",
          "display_name": "Access status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "person_ref",
          "description": "",
          "display_name": "Person who exercises the membership (typed reference)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "access_credentials_text",
          "description": "",
          "display_name": "Access-control credential identifier (free text — vendor-specific)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Operator notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.add_dependent_membership",
          "description": "Mint a dependent `membership` row under the target account. The Aman Founders registered-companions case and the Saint Haven / Equinox Hotel spouse+child shape both use this action. Dependency validation (Aman Founders cap of four registered members per account; family-rate pricing tier on the existing subscription) lives in the handler."
        }
      ],
      "api_name": "hospitality.membership_account",
      "description": "The durable identity that survives every membership transition — upgrade, downgrade, freeze, cancellation, renewal. One per primary member; dependents (spouse, children, registered companions) attach under it as separate `membership` rows (commit 2). Distinct from the individual access-bearing `membership` because the account identity is what the operator tracks for billing (one subscription per account in commit 3), recognition (the long arc of the member-property relationship), and clinical-record binding (assessments attach to an individual membership, but the family unit lives on the account). The Mindbody Premier factoring of Pricing Option vs Membership vs Autopay vs Contract is the load-bearing reference for why account and access record are separate primitives.",
      "display_name": "Membership account",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Account id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "member_number",
          "description": "",
          "display_name": "Operator-side member number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "joined_at",
          "description": "",
          "display_name": "When the primary member first enrolled",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Account status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "primary_person_ref",
          "description": "",
          "display_name": "Primary member (typed reference to person)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "home_property_ref",
          "description": "",
          "display_name": "Home property — where the member primarily exercises access",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "referred_by_text",
          "description": "",
          "display_name": "Referrer (free text — referrer may not be in the system)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "marketing_consent_inherits_from_person",
          "description": "",
          "display_name": "Marketing consent inherits from primary person (GDPR / CCPA)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Operator notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.membership_initiation",
      "description": "The one-time-charge artefact for tiers that carry a non-recurring initiation fee. Aman Founders' $200K, Saint Haven and Soho House joining fees, country-club initiation. Distinct from `membership_subscription` because the initiation is a non-recurring contract obligation with its own transferability rules (Aman Founders' legacy-lifetime transfers on Founder death; most other operators' initiations are non-transferable) and its own IFRS 15 / ASC 606 deferred-revenue treatment.",
      "display_name": "Membership initiation",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Initiation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Initiation amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency (ISO 4217)",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "paid_at",
          "description": "",
          "display_name": "When the initiation was settled",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "transferable",
          "description": "",
          "display_name": "Initiation transfers to a legacy successor",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "legacy_lifetime",
          "description": "",
          "display_name": "Grants lifetime membership independent of subscription lapses",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "amortisation_method",
          "description": "",
          "display_name": "Deferred-revenue treatment intent (IFRS 15 / ASC 606)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "amortisation_years",
          "description": "",
          "display_name": "Amortisation years (required when amortisation_method = straight_line_years)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Operator notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.renew_membership",
          "description": "Advance the subscription's next billing date and optionally transition cadence. The dues charge that results flows through Aerandil's existing `charge` model attached to the appropriate USALI outlet (per proposal 0001); this action records the contract-side advancement, not the charge posting itself."
        }
      ],
      "api_name": "hospitality.membership_subscription",
      "description": "The recurring billing primitive. Distinct from `membership` because the subscription is the contract+autopay shape (Mindbody Premier's separation), not the access entitlement. One member who downgrades from monthly to quarterly billing keeps the same membership but transitions to a new subscription row. The subscription attaches to the account, not to an individual membership — a family rate spans the primary + dependent memberships under one contract.",
      "display_name": "Membership subscription",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Subscription id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Recurring amount per cycle",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency (ISO 4217)",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "cadence",
          "description": "",
          "display_name": "Billing cadence",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cadence_value",
          "description": "",
          "display_name": "Cadence value (for multi_year, the number of years; else 1)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "start_date",
          "description": "",
          "display_name": "Subscription start",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "auto_renew",
          "description": "",
          "display_name": "Auto-renew (autopay flag)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "expected_end_date",
          "description": "",
          "display_name": "Expected end (null for open-ended autopay)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "next_billing_date",
          "description": "",
          "display_name": "Next scheduled charge date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "suspension_state",
          "description": "",
          "display_name": "Suspension state (typed reason)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "suspension_started_at",
          "description": "",
          "display_name": "When suspension started (required when suspension_state != none)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "contract_terms_text",
          "description": "",
          "display_name": "Contract-terms snapshot at signing (design-spec §B.2)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.membership_tier",
      "description": "A tier definition that admits Saint Haven (quota_ladder), Soho House Every House vs Local House (scope_set), RH Members (discount_bundle), and Aman Founders (recognition_band) without per-operator extension. The `tier_kind` enum is the load-bearing distinguisher; the shape-specific fields (`quota_size`, `prorates_on_change`, `transferable`, `legacy_initiation`) become meaningful only when the tier_kind calls for them.",
      "display_name": "Membership tier",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Tier code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Tier name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "tier_kind",
          "description": "",
          "display_name": "Tier kind (load-bearing — see enum docs)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "is_active",
          "description": "",
          "display_name": "Currently offered (sunset tiers coexist with active tiers)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "quota_size",
          "description": "",
          "display_name": "Quota size (required when tier_kind = quota_ladder)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "quota_remaining",
          "description": "",
          "display_name": "Quota remaining (denormalised projection)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "quota_remaining_computed_at",
          "description": "",
          "display_name": "When quota_remaining was last computed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "prorates_on_change",
          "description": "",
          "display_name": "Prorates dues on mid-cycle tier change",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "proration_method",
          "description": "",
          "display_name": "Proration method (required when prorates_on_change = true)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "transferable",
          "description": "",
          "display_name": "Tier survives original member's death / departure (Aman Founders legacy)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "legacy_initiation",
          "description": "",
          "display_name": "Tier carries a non-recurring initiation fee (see commit 3 membership_initiation)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "refund_policy_text",
          "description": "",
          "display_name": "Refund policy snapshot at tier creation (design-spec §B.2)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.menu",
      "description": "An orderable menu attached to an outlet, with a daypart and a draft/active/retired lifecycle. Deliberately unversioned: check_line.unit_price snapshots the price at order time, so menu edits never rewrite financial history (proposal 0031 §Ratified design 2).",
      "display_name": "Menu",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Menu id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Menu name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "daypart",
          "description": "",
          "display_name": "When the menu runs",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Menu lifecycle status",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.menu_item",
      "description": "The catalogue master of one dish or drink, shared by every outlet that sells it: names, its item class (menu_item_in_class → VAT, revenue category, course), the reporting group menu engineering reads, and its allergens. What gets priced and ordered is a menu_item_variant (variant_of_menu_item); where it appears is menu_item_in_section; an optional recipe binding (menu_item_uses_recipe) carries COGS. Distinct from service_item, the venue-level catalogue row.",
      "display_name": "Menu item",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Item code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Item name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "short_name",
          "description": "What a till key and a kitchen ticket show when the full name will not fit.",
          "display_name": "Short name",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Guest-facing description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "reporting_group",
          "description": "The operator's own grouping for menu engineering and sales reports (Starters, Grills, Red wine) — Simphony's family group. The revenue split itself comes from the item class.",
          "display_name": "Reporting group",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "allergens",
          "description": "The item's declared allergens (Food Information Regulations 2014). Absent = not yet declared, which the till treats as unknown; an empty list declares none. A recipe-bound item takes its recipe's rollup instead.",
          "display_name": "Allergens (EU 14)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "course_default",
          "description": "",
          "display_name": "Course number override (else the class's)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "orderable_alone",
          "description": "False for an item that is only ever added as a modifier option (extra bacon, a side sauce) and so has no till key of its own.",
          "display_name": "Orderable on its own",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Orderable",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.menu_item_variant",
      "description": "The priced, orderable unit of a menu item — a 125 / 175 / 250 ml glass, a bottle, a half or full portion. Every item has at least one; a single-size item has one default variant. Prices attach here (price_for_variant), and a check line records the variant it sold.",
      "display_name": "Menu item variant",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Variant id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Variant name (Regular, 175 ml, Bottle)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "measure",
          "description": "The quantity served, as the menu states it (175 ml, 25 ml, 8 oz) — wine and spirits must be offered in specified quantities.",
          "display_name": "Measure",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "is_default",
          "description": "The variant a till key or a feed line naming only the item rings. An item with one variant uses it whatever this says.",
          "display_name": "Default variant",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "sort_order",
          "description": "",
          "display_name": "Order on the till",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Orderable",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.menu_section",
      "description": "A heading on a menu (Starters, Grills, Wine by the glass) — what the till screen's category keys and the printed menu group items by. Belongs to one menu (menu_section_of_menu); items join it through menu_item_in_section, so one item can sit on the breakfast menu and the room-service menu at once.",
      "display_name": "Menu section",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Section id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Section name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "sort_order",
          "description": "",
          "display_name": "Order on the menu",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.close_merchant_account",
          "description": "**Operator only.** Stop settling the property's card payments to this account. Terminal: the row stays as the record of where earlier payments went, and the property either opens a new account or takes payments on the platform's own again. Does not touch the account at the processor, where its balance and disputes still live. Emits `merchant_account.closed.recorded`."
        },
        {
          "api_name": "hospitality.action.record_external_account_state",
          "description": "**Card-rail only.** Mirror the state of the bank accounts a merchant account pays out to, read back from the processor's list of them on `account.external_account.created` / `.updated` / `.deleted` (and on a `payout.paid` while paused) — after a failed payout the processor disables the bank account, and nothing pays out in its currency until the hotel fixes or replaces it. `disabled_ids` are the default bank accounts the processor has disabled; `external_account_disabled` is derived from them. A view older than the one recorded is ignored. Recorded on a closed account too: its last payouts still need a bank. Emits `merchant_account.external_account.recorded` when something changed."
        },
        {
          "api_name": "hospitality.action.record_merchant_account_card_export",
          "description": "**Card-rail only.** Record that the platform operator pulled this account's stored cards for the hotel's next processor (the Leaving policy): when, how many cards and Stripe Customers, in which shape, who asked, and what the export could not carry cleanly — retired cards still on those Customers at the processor, and Customers withheld because another merchant may draw on a card they hold. Stamps `cards_exported_at` and `cards_exported_count` on the account. Not refused on a closed account: a hotel that has left still owns its guests' cards. Emits `merchant_account.cards.exported`."
        },
        {
          "api_name": "hospitality.action.record_merchant_account_onboarding_link",
          "description": "**Card-rail only.** Record the hotel's current onboarding refresh link: the SHA-256 of its one-use secret and when it stops working. Minted by the operator's onboarding-link route (replacing any earlier link) and moved on by each use of the public refresh route, which names the secret it spent (`replaces_nonce_hash`) — refused when that is no longer the current one, so two uses of one link mint one fresh Stripe link between them. Refused on a closed account. Emits `merchant_account.onboarding_link.recorded` (never the hash)."
        },
        {
          "api_name": "hospitality.action.record_merchant_account_rejection",
          "description": "**Card-rail only.** Record that the processor accepted the platform's rejection of a closed merchant account — which it admits only once the account's balance has paid out. Refused on an account that is not closed; a second record is a no-op. Emits `merchant_account.rejected.recorded`."
        },
        {
          "api_name": "hospitality.action.record_merchant_account_state",
          "description": "**Card-rail only.** Mirror the processor's view of an account's verification: whether it may take charges and receive payouts, whether the hotel has finished onboarding, what is still owed, and the account's own payout schedule (interval, delay, anchor). `status` is derived from it — `active` when charges are enabled, `restricted` when the details are in but charges are not, `onboarding` otherwise. A view older than the one already recorded is ignored (webhooks arrive out of order), as is any view of a closed account. Emits `merchant_account.state_recorded.recorded` when something changed, and `merchant_account.payout_schedule.changed` — with the old schedule and the new — when the processor moved a schedule the row already held; the old values stay on the row (`payout_*_before`, `payout_schedule_changed_at`)."
        },
        {
          "api_name": "hospitality.action.record_payout",
          "description": "**Card-rail only.** Mirror a processor payout from the hotel's merchant account. Upserts by `psp_reference` (Stripe `po_…`), so every event about one payout lands on one row. `status` moves forward only — `pending`, `in_transit`, `paid` — bar the one move back the processor documents: `failed` after `paid`. `reconciliation_status` follows the processor (`in_progress` while it itemises the payout, `not_started` once it has; a manual payout is `not_applicable`) and never undoes the ledger's own `completed`. Emits `payout.recorded` when the row is new or moved, and `payout.failed` (with the processor's code and words) on the move into `failed`."
        },
        {
          "api_name": "hospitality.action.record_platform_ledger_lines",
          "description": "**Card-rail only.** Record balance transactions on the platform's own processor balance (Stripe's `txn_…`) that belong to the target merchant account, each with the ledger payment or chargeback it came from when the worker resolved one. Idempotent by `psp_reference`: a line already recorded is left as it is, unless it was unmatched and now carries a payment or a case. Each line's `occurred_on` is the house's calendar day of the processor's `occurred_at`, computed here. A payment or case must sit at the account's house; a line's amount, fee and net must share a currency."
        },
        {
          "api_name": "hospitality.action.set_merchant_account_fee",
          "description": "**Operator only.** Replace the platform's fee schedule on the account's card payments — for each card class, online and card-present, the basis points and fixed fee the hotel pays beside the processor's published rate. Applies to money captured from now on, including holds already placed. Refused on a closed account. Emits `merchant_account.fee_set.recorded`."
        }
      ],
      "api_name": "hospitality.merchant_account",
      "description": "A property's own account at the payment processor, opened under the platform's (Stripe Connect). While one is `active`, the property's card payments settle to it — the hotel is the merchant the guest's statement names, its payouts go to its own bank — and the platform's fee is taken from each payment. A property with no open account takes card payments on the platform's own account. Verification is the processor's: the account is `onboarding` until the hotel has submitted its details, then `active` or `restricted` as the processor decides.",
      "display_name": "Merchant account",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Merchant account id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status, derived from the processor's verification",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "provider",
          "description": "",
          "display_name": "Processor that holds the account (`stripe`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "provider_account_ref",
          "description": "",
          "display_name": "The processor's id for the account (Stripe `acct_…`). An identifier, not a credential: nothing can be done with it without the platform's own key.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "country",
          "description": "",
          "display_name": "Country the hotel trades in (ISO 3166-1 alpha-2) — fixed at opening, because the processor fixes it too",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "charges_enabled",
          "description": "",
          "display_name": "Whether the processor lets the account take card payments",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "payouts_enabled",
          "description": "",
          "display_name": "Whether the processor will pay the account's balance out to the hotel's bank",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "details_submitted",
          "description": "",
          "display_name": "Whether the hotel has finished the processor's onboarding form",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "requirements_due",
          "description": "",
          "display_name": "What the processor still needs from the hotel, in its own vocabulary (`external_account`, `individual.verification.document`, …). Empty when nothing is owed.",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "disabled_reason",
          "description": "",
          "display_name": "The processor's reason the account cannot take charges, when it gives one",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "fee_schedule",
          "description": "",
          "display_name": "The platform's fee on each card payment settled to this account, by the card's class: one row for each of `domestic_standard`, `domestic_premium`, `regional` and `international`, online and card-present — the basis points and fixed fee the hotel pays (`bps`, `fixed`) beside the processor's own published rate for that class (`processor_bps`, `processor_fixed`), which is what the platform's estimate of the processor's fee uses. Taken when the money is captured, on the amount captured, rounded down. The fixed fees' currency is the schedule's: a charge in another currency carries no fixed fee.",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "fee_conversion_bps",
          "description": "",
          "display_name": "The processor's currency-conversion fee in basis points (Stripe: 2%), counted in the estimate of the processor's fee on a charge in a currency other than the schedule's",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "When the account was opened",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "state_observed_at",
          "description": "",
          "display_name": "When the processor's view below was taken — the watermark an out-of-order webhook is checked against",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "closed_at",
          "description": "",
          "display_name": "When the account was closed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_account_disabled",
          "description": "",
          "display_name": "Whether the processor has disabled the bank account it pays out to (after a failed payout); no payout lands until the hotel replaces it",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "external_account_disabled_reason",
          "description": "",
          "display_name": "The processor's status for that bank account (`errored`, `verification_failed`) when it is disabled",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "external_account_observed_at",
          "description": "",
          "display_name": "When the bank account's state was read — the watermark an out-of-order webhook is checked against",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_account_disabled_ids",
          "description": "",
          "display_name": "The processor's ids of the default bank accounts it has disabled (`ba_…`, one per currency at most); `external_account_disabled` is whether there are any",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "rejected_at",
          "description": "",
          "display_name": "When the processor accepted the platform's rejection of a closed account — the end of its life there, which the processor admits only at a zero balance",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "onboarding_link_nonce_hash",
          "description": "",
          "display_name": "SHA-256 of the one-use secret in the hotel's current onboarding refresh link — the link that mints a fresh Stripe onboarding link without a sign-in",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "onboarding_link_expires_at",
          "description": "",
          "display_name": "When the hotel's current onboarding refresh link stops working",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "payout_interval",
          "description": "",
          "display_name": "How often the processor pays the balance out, in its own words (Stripe: `daily`, `weekly`, `monthly`, or `manual` — paid out only on request)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payout_delay_days",
          "description": "",
          "display_name": "How many days the processor holds a payment before it can be paid out (Stripe's `delay_days`; calendar or business days by the account's country)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "payout_anchor",
          "description": "",
          "display_name": "The day a weekly payout goes (`monday` … `sunday`) or a monthly one (its day of the month, `1` … `31`); absent for daily and manual payouts",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payout_schedule_changed_at",
          "description": "",
          "display_name": "When the processor last changed this account's payout schedule (the first schedule read is a report, not a change)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "payout_interval_before",
          "description": "",
          "display_name": "The payout interval before the last change, if it had one",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payout_delay_days_before",
          "description": "",
          "display_name": "The payout delay before the last change, if it had one",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "payout_anchor_before",
          "description": "",
          "display_name": "The weekly or monthly anchor before the last change, if it had one",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "cards_exported_at",
          "description": "",
          "display_name": "When the platform operator last pulled this account's stored cards for the hotel's next processor (the Leaving policy's card export)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cards_exported_count",
          "description": "",
          "display_name": "How many stored cards that export carried — the cards that name this account, are active and are on the processor",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_message_triage",
          "description": "Apply a suggested message_triage. Fan-out on recommended_action_kind: `auto_respond` chains an outbound record_guest_message; `chain_typed_action` records the chained_action_correlation_id (the host application invokes the action); `silent_acknowledge` marks applied with no outbound. Optional `modified_params` lets the operator accept the strategy but tweak the chained action's parameters (the modify-affordance per Dietvorst 2018). Rejected if the triage isn't in `suggested` state, has expired, or recommended_action_kind is escalate_to_staff / no_action."
        },
        {
          "api_name": "hospitality.action.escalate_via_triage",
          "description": "Escalate a suggested message_triage to staff with a structured handoff payload. Transitions `decision_status` to `escalated`; sets `escalation_reason`; optionally links to a named staff_member. Updates conversation state to `escalated`. The only legal terminal transition for injection.suspected triages."
        },
        {
          "api_name": "hospitality.action.reject_message_triage",
          "description": "Decline a suggested message_triage. Transitions `decision_status` to `rejected` and records an optional free-text reason for the multi-metric trust-formation instrumentation (proposal 0014 §10.7). Rejected if the triage isn't in `suggested` state."
        }
      ],
      "api_name": "hospitality.message_triage",
      "description": "A typed triage decision issued by an agent (or, less commonly, a human operator logging a manual triage into the audit trail) against a single inbound `guest_message`. Mostly-immutable post-issue: the intent / sentiment / urgency / factors / rationale / enrichment_snapshot are frozen at write time so the audit trail is the rationale at decision time, not against today's profile. The decision_* and chained_* fields mutate exactly once on the transition out of `suggested` (apply / reject / escalate / expire / supersede) — that single mutation is gated by the `decision_status_transitions` cross-link validator.",
      "display_name": "Message triage",
      "domain": "messaging",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Triage id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "message_ref",
          "description": "",
          "display_name": "The inbound `guest_message` this triage classifies",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "conversation_ref",
          "description": "",
          "display_name": "Denormalised conversation ref (for per-conversation triage queries)",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "intent_primary",
          "description": "",
          "display_name": "Leaf intent from MESSAGE_INTENTS (§3.3.2)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "intent_alternatives",
          "description": "",
          "display_name": "Top-k alternative intents with scores (optional)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "intent_confidence",
          "description": "",
          "display_name": "Operator-facing confidence ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "intent_confidence_score",
          "description": "",
          "display_name": "Calibrated probability (0..=1) — set when the model emits one",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "sentiment",
          "description": "",
          "display_name": "Sentiment ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "sentiment_score",
          "description": "",
          "display_name": "Calibrated sentiment score [-1.0, 1.0]",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "urgency",
          "description": "",
          "display_name": "Urgency classification",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "factors_considered",
          "description": "",
          "display_name": "Signals the classifier conditioned on",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "recommended_action_kind",
          "description": "",
          "display_name": "Discriminator for the apply path",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recommended_action_ref",
          "description": "",
          "display_name": "API name of the chained action (when kind = chain_typed_action)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "recommended_action_params",
          "description": "",
          "display_name": "Proposed parameters for the chained action (JSON object; validated at apply time)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "recommended_response_text",
          "description": "",
          "display_name": "Drafted reply text (when kind = auto_respond)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "Human-readable explanation surfaced to operators",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "enrichment_snapshot",
          "description": "",
          "display_name": "Frozen context snapshot at issue time (§3.3.1; JSON; host populates)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the agent emitted the triage",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "After this, decision_status moves to expired",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decision_status",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the triage transitioned out of suggested",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "decided_by",
          "description": "",
          "display_name": "`principal.stable_id()` of the deciding principal",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "chained_action_correlation_id",
          "description": "",
          "display_name": "correlation_id of the chained action's ApplyActionResponse (set on `applied`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "escalation_reason",
          "description": "",
          "display_name": "Reason recorded on escalation",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Free-text reason on reject (gold-standard decision-log pattern)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "policy_outcomes",
          "description": "",
          "display_name": "Compliance flags observed during triage",
          "kind": "array",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.certify_metric",
          "description": "Record operator sign-off on a `draft` definition: status → `certified`, `certified_at` / `certified_by` stamped from the acting principal. Idempotent on an already-certified version; a deprecated version cannot be re-certified (revise to a new version instead). Certifying an organisation-wide metric may route through a 0020 approval gate by operator policy; the action itself is gate-agnostic. Emits `metric.certified`."
        },
        {
          "api_name": "hospitality.action.deprecate_metric",
          "description": "Retire the targeted definition version: status → `deprecated`, terminal for this version — it returns only via `revise_metric` at a strictly greater version. Idempotent on an already-deprecated version. The optional `reason` lands in the event payload for the audit trail. Emits `metric.deprecated`."
        },
        {
          "api_name": "hospitality.action.revise_metric",
          "description": "Produce the next version of the targeted metric definition: version bumps by one, status returns to `draft`, and any prior certification is cleared — a revised definition must be re-certified (no silent un-deprecation either; revising a deprecated metric is exactly how it comes back). Omitted parameters keep their current values; supplied reference lists are re-validated against the Registry. The prior version stays in the object's event history. Emits `metric.revised`."
        }
      ],
      "api_name": "hospitality.metric_definition",
      "description": "A governed, versioned business-metric definition: the disambiguating prose (`description` / `formula_expression`) plus a structured reference list over registered object types and fields, so the definition cannot silently reference a field that doesn't exist. Versioned per `name` and superseded in place (prior versions live in the event history); `certify_metric` records operator sign-off, and a deprecated version only returns via `revise_metric` at a strictly greater version. The executable complement for kernel-conformed metrics lives in the MetricDef registry (`/v1/metrics/{name}`); this object is the certification layer NL-analytics and agents ground against.",
      "display_name": "Metric definition",
      "domain": "catalogue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Definition id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Metric name (snake_case; matches a registry metric when one exists)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "display_name",
          "description": "",
          "display_name": "Display name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Disambiguating prose — the definition operators sign off on",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "version",
          "description": "",
          "display_name": "Monotonic version per name (revise_metric bumps it)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Certification lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "grain",
          "description": "",
          "display_name": "Organisational-unit × period grain the metric is defined over",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "unit",
          "description": "",
          "display_name": "Unit the metric's value carries",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "formula_expression",
          "description": "",
          "display_name": "Canonical formula expression (governed prose, not executed)",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "referenced_object_types",
          "description": "",
          "display_name": "Registered object-type names the formula reads",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "referenced_fields",
          "description": "",
          "display_name": "Registered `type.property` fields the formula reads",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "owner",
          "description": "",
          "display_name": "Accountable team / principal for the definition",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "authored_by",
          "description": "",
          "display_name": "Principal::stable_id() of the defining / revising principal",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "certified_at",
          "description": "",
          "display_name": "When this version was certified (set by certify_metric)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "certified_by",
          "description": "",
          "display_name": "Principal::stable_id() of the certifying sign-off",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.modifier_group",
      "description": "A choice an item asks for — How cooked, Choose a side, Add extras, or a set menu's Choose your main. Items attach groups through menu_item_uses_modifier_group (which may override the group's bounds for that item); options belong to it through modifier_option_in_group.",
      "display_name": "Modifier group",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Group code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Group name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "min_select",
          "description": "",
          "display_name": "Least options the guest must choose",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "max_select",
          "description": "",
          "display_name": "Most options the guest may choose (absent = no limit)",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.modifier_option",
      "description": "One choice inside a modifier group. When it adds something sellable it points at that variant (modifier_option_adds_variant) and is priced at `price` if set, else at the variant's own price; with no variant it is an instruction to the kitchen (medium rare, no ice) and costs nothing.",
      "display_name": "Modifier option",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Option id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Option name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "price",
          "description": "What the option adds when chosen, if different from the variant's own price — a side of fries is £4.50, fries instead of salad £0, a set menu's steak supplement £8.",
          "display_name": "Price as an option",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "is_default",
          "description": "",
          "display_name": "Chosen by default",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "sort_order",
          "description": "",
          "display_name": "Order within the group",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.night_audit_run",
      "description": "A single execution of the night audit cycle for one business date. Results are immutable on completion; re-runs create a new `NightAuditRun`. The status enum tracks lifecycle so a stuck or partial run is visible.",
      "display_name": "Night audit run",
      "domain": "audit",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "Started at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "Completed at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "posted_room_revenue",
          "description": "",
          "display_name": "Posted room revenue",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "posted_room_tax",
          "description": "",
          "display_name": "Tax the run derived and posted alongside the room charges (inclusive and exclusive lines alike); absent when the posting spanned currencies",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "package_adjustments_posted",
          "description": "",
          "display_name": "Unconsumed package allowances the run settled as profit",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "holds_released",
          "description": "",
          "display_name": "Expired courtesy holds the deposit sweep cancelled",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "deposit_lapse_cancellations",
          "description": "",
          "display_name": "Lapsed unpaid deposits the sweep auto-cancelled per policy",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "deposit_lapses_flagged",
          "description": "",
          "display_name": "Deposit lapses recorded for operator disposition",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "block_rooms_released",
          "description": "",
          "display_name": "Group-block room-nights the cutoff sweep returned to the house",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "commission_details_accrued",
          "description": "",
          "display_name": "Commission rows the run accrued on stays that completed",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "waitlist_entries_expired",
          "description": "",
          "display_name": "Waitlist entries the run aged out (departure + 2 business days)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "visitor_keys_stripped",
          "description": "",
          "display_name": "Visitor keys the run stripped from offers, turnaways and reservations whose arrival night was past the property's grace",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "payment_cards_expired",
          "description": "",
          "display_name": "Cards on file the run marked `expired` — active cards whose expiry month ended before the audited date",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "payment_requests_expired",
          "description": "",
          "display_name": "Payment requests the run expired — pending requests whose `expires_at` fell on or before the audited date (absent when none)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "open_cashier_sessions",
          "description": "",
          "display_name": "Cashier sessions still open at the close (exception, never a gate)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "exception_count",
          "description": "",
          "display_name": "Exception count",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.no_show_posting_rule",
      "description": "One property's no-show posting rule for one reservation guarantee (OPERA's Post Revenue To No Shows): when the night audit marks a booking of that guarantee a no-show, it posts `all_nights` (every booked night's rate, as booked), `first_night` (the first night of every room), `deposit_only` (forfeits the deposit the house holds, nothing when it holds none) or `none`. Postings land on the booking's own account through the posting verbs, attributed to the rule's `outlet_id`; a deposit already on that account offsets them. A rule with no guarantee is the house default for every guarantee without a rule of its own; a house with no rule posts nothing. A booking a rule covers is posted by the rule alone — the frozen cancellation-policy fee step (`post_no_show_fees`) skips it, so one no-show is posted once.",
      "display_name": "No-show posting rule",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "No-show posting rule id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "guarantee",
          "description": "",
          "display_name": "The reservation guarantee this rule is for; absent = the house default for any guarantee without a rule of its own",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "basis",
          "description": "",
          "display_name": "What the audit posts for a no-show this rule covers",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "outlet_id",
          "description": "",
          "display_name": "The outlet every posting is attributed to (OPERA's single no-show posting transaction code); required unless the basis is `none`",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Whether the night audit applies this rule",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing description",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.set_outlet_meal_periods",
          "description": "Set when each meal runs at the outlet, replacing the whole list: each period a `daypart`, a `start_time` and `end_time` in 24-hour HH:MM on the house's wall clock, and optionally its `days_of_week` (absent = every day), so a weekday breakfast and a weekend one are two rows. An `end_time` before the start runs past midnight and belongs to the day it started; the same start and end is a whole day from that time. Periods may not overlap, and that is refused naming both; gaps are allowed. `open_check` stamps a live check with the period in force unless it is told otherwise; in a gap the check carries no meal, and none is ever refused for its hours. An empty list clears the hours. The same list again changes nothing."
        }
      ],
      "api_name": "hospitality.outlet",
      "description": "A USALI-conformant revenue-bearing unit. Distinct from `venue` (the physical space) because outlets are revenue-reporting categories: the rooms outlet has no venue; a banquets-and-restaurant venue can host two revenue centers (one outlet each). `usali_department` is the chart-of-accounts classification; `kind` is the operational classification — orthogonal axes, since an outlet's accounting schedule and its operational shape do not always coincide (a third-party-licensed restaurant books as `other_operated` but operates as `restaurant`).",
      "display_name": "Outlet",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Outlet code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Outlet name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "usali_department",
          "description": "",
          "display_name": "USALI 12 department",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "usali_revenue_center_label",
          "description": "",
          "display_name": "USALI revenue center label",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Operational kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "meal_periods",
          "description": "The outlet's meal periods (Simphony's serving periods, Toast's services): each a `daypart`, 24-hour `start_time` and `end_time` on the house's wall clock, and its `days_of_week`. `open_check` stamps a live check with the one in force. Written by `set_outlet_meal_periods`, which refuses an overlap.",
          "display_name": "When each meal runs here",
          "kind": "array",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.owner_statement",
      "description": "One month's owner package for one property: the USALI summary figures, the fee computation, and the budget-vs-actual-vs-prior-year snapshot, submitted by the operator inside the HMA's reporting window (contract band: 8 business days to 20 calendar days after month-end) and approved by the owner through an approval gate (the statement implements `approvable`; separation-of-duties keeps the submitter out of the deciding set). A changes_requested statement is answered by a new revision, not an edit — statements are reporting records.",
      "display_name": "Owner statement",
      "domain": "management",
      "interfaces": [
        "hospitality.interface.approvable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Statement id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "period_start",
          "description": "",
          "display_name": "Reporting-period start",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "period_end",
          "description": "",
          "display_name": "Reporting-period end",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "due_by",
          "description": "",
          "display_name": "Contractual delivery deadline for the period",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "revision",
          "description": "",
          "display_name": "Revision number (1 = first submission for the period)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "total_revenue",
          "description": "",
          "display_name": "Total operating revenue for the period",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "gop",
          "description": "",
          "display_name": "Gross operating profit (pre-management-fee, per USALI)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "base_fee",
          "description": "",
          "display_name": "Base management fee for the period",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "incentive_fee",
          "description": "",
          "display_name": "Incentive management fee for the period (accrual or true-up)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "noi",
          "description": "",
          "display_name": "Net operating income for the period",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "variance_snapshot",
          "description": "",
          "display_name": "Budget-vs-actual-vs-prior-year comparison (the Strand §9.02 columns) as reported, JSON",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "submitted_at",
          "description": "",
          "display_name": "When the operator submitted",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the owner's gate decided",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "External ref",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.supersede_package",
          "description": "Mint a new immutable `package_version` against the target package. The previous active version transitions to `superseded`; existing `package_sale` rows retain their frozen pin and are not re-priced."
        }
      ],
      "api_name": "hospitality.package",
      "description": "A sellable bundled offering (room + dinner + spa retreat). The catalog identity / lineage object: `code` and `name` are stable across versions; wrapper pricing and allocation rule live exclusively on `package_version` rows linked via `package_has_version` / `package_has_active_version` (proposal 0024; the legacy package-level price fields were dropped in the Phase B flip). A sale freezes the specific version it was priced against via the `package_sale_uses_package_version` link plus the existing `package_sale.package_version_snapshot` JSON. All pricing writes go through `supersede_package`.",
      "display_name": "Package",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Package code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Package name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.package_component",
      "description": "One amenity inside a package: dinner-for-two, daily breakfast, an arrival massage, a $50 spa credit. Promoted to a first-class object (not link-metadata) because allocation rules, frequencies, allowances, and alternate-outlet routings don't fit on a link's edge metadata without violating Aerandil's links-carry-no-payload principle.",
      "display_name": "Package component",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Component id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "allocation_kind",
          "description": "",
          "display_name": "How the allowance scales",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "frequency",
          "description": "",
          "display_name": "When during the stay the component is available",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "allowance_amount",
          "description": "",
          "display_name": "Notional contribution per occurrence",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "sort_order",
          "description": "",
          "display_name": "Display order within package",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "daypart",
          "description": "Set, the component is an F&B allowance: a room charge at one of its alternate outlets (any outlet of the house when it names none) consumes it from a check in this meal period — `all_day` is a credit for any meal. Absent, it is not food (a spa credit, parking) and the till never touches it. A `breakfast` or `brunch` allowance is eaten the morning after its night (OPERA's Post Next Day): the breakfast roster, the till and the night audit's allowance sweep all read it on the day after the night that carries it.",
          "display_name": "Meal period the allowance is eaten in",
          "kind": "enum",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.package_component_consumption",
      "description": "Append-only ledger row tying a `charge` to a `package_sale` + `package_component` on a specific business date. Opera's package_profit / package_loss reconciliation pattern, made queryable: per-night per-component the operator can see what was allowed, what was consumed, and the reconciliation outcome. Mutation of this row after insert is rejected — it is event-shaped, mirroring the event log's append-only contract.",
      "display_name": "Package component consumption",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Consumption id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "allowance_amount",
          "description": "",
          "display_name": "What the package allotted",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "consumed_amount",
          "description": "",
          "display_name": "What the guest actually consumed",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "reconciliation_kind",
          "description": "",
          "display_name": "Reconciliation outcome",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recorded_at",
          "description": "",
          "display_name": "When the consumption was recorded",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "covers",
          "description": "How many of the stay's entitled covers this row accounts for — the breakfast roster's tick-off, or the covers of the check a room charge consumed it from. What the roster counts as taken. Absent on rows that are not a meal.",
          "display_name": "Covers this consumption fed",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.consume_package_component",
          "description": "Record one consumption of a package component on a business date and post the corresponding charge. The reconciliation outcome (`consumed` / `profit` / `loss` / `refunded`) is derived from the consumed vs. allowance amounts."
        },
        {
          "api_name": "hospitality.action.post_package_profit_loss",
          "description": "Post the package's wrapper-vs-components reconciliation charge at night-audit close (Opera package_profit / package_loss). The resulting charge carries `package_attribution_kind = package_discount_adjustment`."
        }
      ],
      "api_name": "hospitality.package_sale",
      "description": "A reservation-attached instance of a package. Freezes the package definition at sale time via `package_version_snapshot` so guests cannot be re-priced when the catalog evolves — the snapshot is the structural snapshot half of the design-spec §B.2 structured-policy + immutable-snapshot pattern. `contracted_price` is the amount the guest agreed to pay for the wrapper; `state` tracks the lifecycle from pending through consumed / refunded / cancelled.",
      "display_name": "Package sale",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Sale id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "contracted_price",
          "description": "",
          "display_name": "Wrapper price at sale time",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "package_version_snapshot",
          "description": "",
          "display_name": "Frozen package version at sale time",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "package_version_ref",
          "description": "",
          "display_name": "Frozen pin to the `package_version` row this sale was priced against (proposal 0024)",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "package_ref",
          "description": "",
          "display_name": "Convenience reference to the parent `package` lineage (proposal 0024)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "state",
          "description": "",
          "display_name": "Sale state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "assembled_at",
          "description": "",
          "display_name": "When the sale was assembled",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "allowance_draws",
          "description": "Counts up by one with every consumption written through the package write (a room charge's allowance, a roster tick-off, an operator's consume). The write is the sale's version check: a recorder that weighed what was left of a day's allowance writes the sale at the version it read before summing the day's rows, so a concurrent draw on the same sale turns the second into a version conflict instead of two draws on one remainder.",
          "display_name": "Consumptions recorded against the sale",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "allowance_drawn_at",
          "description": "",
          "display_name": "When a consumption last drew on the sale",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.package_version",
      "description": "Immutable per-version content row for a `package`. Carries the wrapper price + allocation rule that were in force when the version was minted, plus a frozen content snapshot of the components that participated. The active version is the one with `version_status = active` and `effective_to IS NULL`; the operational handle is the `package_has_active_version` 1:1 link. A `package_sale` pins to a specific row via `package_sale_uses_package_version` so cross-version revenue analytics (`GROUP BY package_version_id`) are queryable.",
      "display_name": "Package version",
      "domain": "revenue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Version id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "version_number",
          "description": "",
          "display_name": "Monotonic version number (1, 2, 3, ...) per package",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "contracted_price",
          "description": "",
          "display_name": "Wrapper price for this version",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "allocation_rule",
          "description": "",
          "display_name": "Allocation rule for this version",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "content_snapshot",
          "description": "",
          "display_name": "Frozen component identities + per-version overrides at version-mint time (opaque to the engine)",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "When this version became active",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "When this version was superseded (null while active)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "version_status",
          "description": "",
          "display_name": "Per-version status (active | superseded | deprecated)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "supersede_reason",
          "description": "",
          "display_name": "Operator note explaining the supersedence (e.g. 'Q3 seasonal repricing'). Free text; not parsed.",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "supersedes_version_id",
          "description": "",
          "display_name": "Prior version this one replaces (null on v1; set on every subsequent supersede). Self-reference; the link surface (`package_version_supersedes`) carries the same edge for graph walks.",
          "kind": "reference",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.participant_dispatch_attempt",
      "description": "One row per envelope the coordinator sent to a participant runner. Records the envelope id, the parent run, the participation (federated) or workflow step (workflow), attempt counters, last HTTP outcome, last failure reason, dead-letter reference, and lifecycle status. The dispatcher worker that fills this table lands with the runner / dispatcher implementation proposal; this proposal lands only the durable state shape per 0022 §17.1.",
      "display_name": "Participant dispatch attempt",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Dispatch attempt id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "envelope_id",
          "description": "",
          "display_name": "CloudEvents `id` carried by this dispatch (UUID v7). Reused on retry; same envelope_id across attempts of one logical dispatch.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "dispatch_kind",
          "description": "",
          "display_name": "CloudEvents `type` discriminator — `federated_contribution_request`, `workflow_step_invocation`, `federated_output_quarantine`, etc. per 0022 §5.5 / §7.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "target_run_id",
          "description": "",
          "display_name": "Parent `federated_run.id` or `workflow_run.id`. The link surface carries the typed traversal; this column is the denormalised id for fast lookup. Exactly one of the two parent-run link types is populated per row.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "target_participation_id",
          "description": "",
          "display_name": "`federated_participation.id` for federated dispatches; null for workflow agent-installation dispatches.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "target_agent_installation_id",
          "description": "",
          "display_name": "`agent_installation.id` (business id) for `workflow_step_invocation` dispatches — the endpoint resolver reads the installation's `dispatch_endpoint_url` and the receiver verifies participant signatures against its `runner_public_keys`. Null for participation-anchored (federated) dispatch kinds.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "target_step_run_id",
          "description": "",
          "display_name": "`workflow_step_run` object uuid for `workflow_step_invocation` dispatches — the receiver's completion binding advances this step via `record_workflow_step_attempt` / `record_workflow_step_result` when the participant submits its result. Null for federated kinds.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "aerandil_correlation_id",
          "description": "",
          "display_name": "Aerandil-domain correlation id copied from the parent workflow / federated run at row-creation time. Surfaced into `delivery_failed.coordinator.aerandil_correlation_id` (proposal 0022 §7.4) so the audit envelope can join back to the parent run without re-reading it. Distinct from W3C `traceparent` / `tracestate` (HTTP transport-level tracing) — this is the Aerandil domain id, not an OpenTelemetry span id.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "target_subject",
          "description": "",
          "display_name": "CloudEvents `subject` attribute the envelope carried. Composite string per 0022 §5.4 — `<run_id>/<participation_id>/<contribution_slot>` for federated, `<run_id>/<step_key>/<attempt_id>` for workflow; `<output_id>/<participation_id>` for §7.5 quarantine notices.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "dispatch_payload",
          "description": "",
          "display_name": "Kind-specific §7 data fragment the dispatcher folds into the outbound envelope's data block at send time — e.g. the §7.5 `quarantine` object for `federated_output_quarantine` rows (output id, incident id, quarantine reason, operator actions). Null for kinds whose data block is assembled entirely from the denormalised columns.",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "attempt_count",
          "description": "",
          "display_name": "Number of HTTP / SSE attempts made so far. Starts at 1 on first dispatch; the dispatcher increments per retry. 0022 §11.2 caps this at 7 by default.",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "first_attempt_at",
          "description": "",
          "display_name": "Wall-clock time of the first dispatch attempt.",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_attempt_at",
          "description": "",
          "display_name": "Wall-clock time of the most recent dispatch attempt.",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_http_status",
          "description": "",
          "display_name": "HTTP status code from the most recent attempt. Null until the first attempt completes with a parseable response; set on every attempt afterwards.",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "last_failure_reason",
          "description": "",
          "display_name": "Free-text failure reason from the most recent failed attempt — transport error, signature verification failure, expired envelope, etc. Cleared on a successful delivery.",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "next_attempt_at",
          "description": "",
          "display_name": "Dispatcher retry timer. Null / absent means immediately claimable; a future value parks the row until that time.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "envelope_expires_at",
          "description": "",
          "display_name": "Hard deadline copied from `data.idempotency.envelope_expires_at`. The dispatcher stops retrying and marks the attempt `expired` after this time.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "claim_expires_at",
          "description": "",
          "display_name": "Visibility lease for a dispatcher worker that has claimed this pending row. Once elapsed, another worker may claim it after a crash or deploy.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "dead_letter_reference",
          "description": "",
          "display_name": "Opaque pointer to the dead-letter record once retries are exhausted. The dead-letter store choice is implementation-specific per 0022 §4.2 / §16.2 open question 7 — this is just the reference.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status — pending → acknowledged → delivered (success), or → failed (retries exhausted) / expired (past `envelope_expires_at`).",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.capture_authorised_charge",
          "description": "Record the terminal capture transition on a previously-authorised Payment. Validators reject: status != pending, review_state in {declined, expired}, payment_method_token shaped like a PAN, folio closed/void. Atomically transitions status = pending -> settled and review_state = held -> approved (when previously held). Captures the full authorised amount unless `captured_amount` names a smaller one — the guest who spent less than the hold — in which case the payment's amount becomes what was taken. Multi-capture stays deferred per proposal 0018 §1."
        },
        {
          "api_name": "hospitality.action.record_external_refund",
          "description": "Mirror a vendor-sourced refund as a pending negative-amount Payment leg against the original settled payment it returns money from — the refund twin of record_external_payment. Replay-safe via `external_ref` (the payment business key): a second call with the same key is a no-op. Validators reject: target not settled, target already a refund leg, target on no folio or a void folio, zero amount, currency mismatch, and a leg that would take the refunded total past the original. A closed folio is accepted (the vendor's bill may already be sealed). The amount is taken as a magnitude, since vendors disagree on the sign of a refund. The dispute block and the one-pending-leg rule stay with refund_payment: they guard a refund being sent, and this one already was. The outcome lands via resolve_payment_refund."
        },
        {
          "api_name": "hospitality.action.record_fraud_warning",
          "description": "**Card-rail only.** Record on a payment an issuer's early fraud warning and what the card rail did about it: the processor's id for the warning (`psp_fraud_warning_reference`), when the issuer raised it (`warned_at`), the processor's time of this report (`observed_at`, the watermark), the kind of fraud labelled (`fraud_type`), whether the processor still calls it actionable, and the response (`hold_voided`, `refunded`, `desk_review`, `not_actionable`) with its reason in words. The stay phase is read off the booking behind the payment, never supplied, and the booking is flagged (`fraud_warning_at`, `fraud_warning_response`) for the desk. A released hold must already be voided; a refund must be a line after the stay with a refund leg against it. The response is recorded once per warning: a later report about the same warning moves only `actionable` and the watermark, an older one changes nothing, and a different warning on the same payment replaces it. Emits `payment.fraud_warning_received` once per warning. A refund leg cannot carry a warning. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_hold_release",
          "description": "**Card-rail only.** Record what the card rail did with a pending card hold that a departure decided: `released` (voided at the processor), `captured` (`captured_amount` taken, the rest let go — a partial capture releases the remainder), `lapsed` (the issuer dropped it at its own deadline; voided `authorisation_expired`), or `unresolved` (the processor would neither void nor capture it and reading it back did not say why — the hold stays pending, marked `hold_outcome = unresolved` with the processor's `detail`, for the desk to settle; a later outcome resolves it). The bill may be closed: check-out closed it, and the hold is still the stay's. A capture credits the bill by what was taken. Recording the same outcome twice changes nothing. Emits `payment.hold_released` with what was held, taken and let go, the outcome and the hold's purpose, or `payment.hold_unresolved`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_page_fee",
          "description": "**Card-rail only.** Record on a payment a hosted page took the fee the page ended on, once the card it was paid with is known: the fee schedule's quote for that card (`platform_fee` — what the platform kept — `processor_fee_estimate`, `fee_class`, `card_present`, all four required), what of the link's ceiling the processor refunded to the hotel (`platform_fee_refunded`, zero when nothing was) and how the true-up came out (`fee_true_up`: `refunded`, `not_due` or `refused`), with the processor's refund id or its words for a refusal. Every amount is in the payment's currency; the fee kept and the fee refunded together never pass what was paid; only a `refunded` true-up refunds anything, and it names what. Recorded once: the same record again changes nothing, a different one is refused. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_payment_dispute",
          "description": "Mirror a chargeback's current state from the processor onto the payment it contests. Upserts on `psp_dispute_reference`: the first call mints the dispute and links it, later calls advance it, so every processor event about one case routes here and replay is a no-op. **The kernel does not own this state machine.** The card network decides a dispute and the processor reports it, so there is deliberately no guard on which transition is legal — `lost` to `won` really happens (Stripe's \"late win\", where an issuer moves money outside the normal lifecycle months later), and refusing it would leave the kernel insisting on a balance the bank disagrees with. What *is* guarded is staleness. Webhook delivery is not ordered, so `observed_at` is checked against the case's `last_observed_at` and an older view is dropped rather than allowed to regress a decided case. Money is already gone by the time the first call lands: the network debits the disputed amount plus a non-refundable fee when the case opens. `funds_reinstated_at` is the only field that says it came back. **A lost case reaches the ledger.** On the move into `lost` the payment is reversed on its bill: a settled refund leg (`refund_reason = chargeback_lost`, capped at what the payment still has unrefunded) lands beside the original, on its folio (or the open rebate folio of a sealed one, or the booking), and the folio's balance moves by it — the money the guest took back is owed again. Once per case. The network's fee (`dispute_fee`, net of what a win returned) is recorded on the case, never on the guest's bill: it is the merchant's cost, and on a hotel's own account the card rail passes it to the hotel at cost. A late win (`lost` to `won`) fails the leg and puts the balance back. **An inquiry escalated to a chargeback** (an inquiry state to `needs_response`) takes evidence again: the case records `escalated_at`, moves the inquiry's `evidence_submitted_at` to `inquiry_evidence_submitted_at` and keeps the processor's count then as `inquiry_submission_count` (the chargeback's own submission is counted above it), and a packet submitted for the inquiry is kept as `superseded_by_escalation`. The CE 3.0 facts (`card_brand`, `network_reason_code`, `visa_ce3_status`, `visa_ce3_required_actions`) are mirrored when the view carries them."
        },
        {
          "api_name": "hospitality.action.record_processor_fee",
          "description": "**Card-rail only.** Write on a payment what the processor actually charged for it (`processor_fee`) and what reached the hotel (`net_to_hotel`), computed from the payment's platform ledger lines — never handed in. `processor_fee` is the fee on its `charge` lines (and a partial capture's reversal); `net_to_hotel` is what its transfers carried less the application fee the platform took, net of a transfer reversal or fee refund a refund made. Chargeback lines are the case's and are left out. A side with no line yet is left unset; a payment whose lines move nothing is left as it is."
        },
        {
          "api_name": "hospitality.action.refund_payment",
          "description": "Record a refund request as a negative-amount Payment leg linked to the original settled payment via `payment_refunds_payment`. Validators reject: target not settled, target already a refund leg, target not linked to one folio, refund_amount non-positive, currency mismatch, concurrent pending refund leg, remaining refundable balance exceeded, missing reason, reason = other without note, missing correlation id. Atomically inserts a refund Payment (payment_kind = refund, amount = -refund_amount, method copied from the original, status = pending unless refund_initial_status = refunded for synchronous PSP-confirmed refunds), writes folio_has_payment and payment_refunds_payment links, and (for refunded initial status) sets refund_resolved_at = received_at. v1 admits partial and multiple-partial refunds against the same original payment; unreferenced refunds / stand-alone credits are deferred per proposal 0019 §1 and §10.5. Money leaves the house, so agent principals cannot apply it directly: an agent stages it via `propose_action` and a human confirms (agent_confirm_required)."
        },
        {
          "api_name": "hospitality.action.resolve_payment_refund",
          "description": "Record the terminal outcome of a pending refund leg. Validators reject: target not a refund leg (payment_kind != refund), target not in status = pending, refund_status not in {refunded, failed, voided}, refund_status = failed without refund_failure_reason, refund_trace_reference_type / refund_trace_reference incoherence. Atomically transitions Payment.status = pending → refunded | failed | voided, sets refund_resolved_at, and updates psp_refund_reference / refund_trace_reference / refund_trace_reference_type / refund_failure_reason when provided. Failed / voided legs release refundable balance for retry per proposal 0019 §10.10 (retry creates a new leg with a new refund_correlation_id, never mutates the failed row back to pending)."
        },
        {
          "api_name": "hospitality.action.resolve_payment_review",
          "description": "The house's decision on a hold held for review: a payment the card rail placed with `capture_strategy = manual_capture_after_review` (`review_state = held`) because its assessment recommended review. `approved` keeps the hold to be captured at check-out as any other (`POST /v1/fraud-risk-assessments/{assessment_id}/keep`); `declined` is the desk's block, after which the hold is released (`void_authorisation`, through `POST /v1/fraud-risk-assessments/{assessment_id}/block`) and may be given to Watchtower as the house's confirmed fraud. Neither captures anything, which is the difference from the capture path's own `held → approved`: a hotel's hold is captured days after the decision, as a Radar Review closes apart from the PaymentIntent. Only a caller cleared to read `internal.restricted` decides, because the assessment behind the hold is marked so: the desk decides only what it can see. The card rail's own verb, not applicable through `/v1/actions/{action}/apply`. Rejected unless the payment is `pending` and `review_state = held`."
        },
        {
          "api_name": "hospitality.action.void_authorisation",
          "description": "Record the terminal void transition on a previously-authorised Payment. Validators reject: status != pending, payment_kind = refund (refund legs use resolve_payment_refund per proposal 0019), review_declined without review_state = held, authorisation_expired without authorisation_expires_at <= now, missing void_reason, folio closed/void. Atomically transitions status = pending -> voided plus review_state per void_reason (held -> declined on review_declined; held / not_required -> expired on authorisation_expired; unchanged otherwise)."
        }
      ],
      "api_name": "hospitality.payment",
      "description": "A payment applied to a folio. Settles charges. Refunds are recorded as a **separate negative-amount Payment row** linked to the original settled payment via `payment_refunds_payment` and carrying `payment_kind = refund` per proposal 0019. The original settled Payment row stays immutable — `status = settled` is the evidence that money was captured, and partial-refund / multiple-refund state is derived from the linked refund legs rather than by rewriting the source row.",
      "display_name": "Payment",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Payment id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "method",
          "description": "",
          "display_name": "Method",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "received_at",
          "description": "",
          "display_name": "Received at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "reference",
          "description": "",
          "display_name": "Reference",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decision_provider",
          "description": "",
          "display_name": "Fraud-decision vendor (denormalised from the gating assessment's vendor_id; cohort key for chargeback-rate analytics per fraud-vendor)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "decision_provider_case_id",
          "description": "",
          "display_name": "Vendor's case / decision identifier (Stripe Radar evaluation id, Adyen case ref, Riskified decision id, Forter forterDecisionId, Signifyd case id, Sift event id, Kount tran id). The chargeback-dispute exhibit pointer.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "liability_shift_basis",
          "description": "",
          "display_name": "Why a chargeback would shift away from the merchant. Per-decision, not per-vendor.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "authentication_outcome",
          "description": "",
          "display_name": "EMV 3DS 2.2 authentication result on this authorisation. `pending_decoupled` covers the EMVCo 2.2 decoupled async flow (up to 7 days for the result to land).",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "sca_exemption_claimed",
          "description": "",
          "display_name": "PSD2 SCA exemption claimed by the host at authorisation time. The PSP is the source of truth on whether the exemption was actually granted; Aerandil records the claim plus the amount-ceiling sanity validators.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "review_state",
          "description": "",
          "display_name": "Post-network review-hold state. `not_required` for direct-capture flows; `held` for manual_capture_after_review with a held_for_review outcome; `approved` / `declined` / `expired` are terminal states transitioned atomically by the `capture_authorised_charge` and `void_authorisation` companion actions (proposals 0017 §6.2 and 0018 §6).",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "authorisation_expires_at",
          "description": "",
          "display_name": "Scheme's authorisation-validity window (Visa 30 days for MCC 3501-7011 lodging, Mastercard 7 days final auth / 30 days pre-auth, Amex 7 days). Set by the host at authorisation time per their PSP integration; `authorisation_expiry_beyond_scheme_window` soft-warns at > 30 days.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "auth_correlation_id",
          "description": "",
          "display_name": "Host's PSP authorisation reference (Stripe payment_intent_id, Adyen pspReference, Worldpay transactionReference). Per-payment join key for reconciling PSP-side records with the Aerandil audit trail.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payment_method_token",
          "description": "",
          "display_name": "PSP-issued opaque token referencing the cardholder data in the operator's PSP / vault. Must NOT be a Luhn-passing 13-19-digit sequence — `payment_token_no_pan_shape` hard validator enforces PCI-DSS 4.0 (PAN never enters Aerandil).",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "captured_at",
          "description": "",
          "display_name": "Wall-clock timestamp when the capture transitioned `status = pending → settled` via `capture_authorised_charge`. Distinct from `received_at` (set at authorisation time). Null for rows that reached `settled` via the direct-settle `apply_payment` path or historical rows pre-dating proposal 0018.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "voided_at",
          "description": "",
          "display_name": "Wall-clock timestamp when the void transitioned `status = pending → voided` via `void_authorisation`. Populated by the void handler; null otherwise.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "void_reason",
          "description": "",
          "display_name": "Closed reason taxonomy on a voided payment. Stripe-aligned canonical four (`duplicate`, `fraudulent`, `requested_by_customer`, `abandoned`) plus Aerandil-domain hospitality four (`review_declined`, `authorisation_expired`, `reservation_cancelled`, `guest_disputed_pre_arrival`). Required at the handler level for `void_authorisation`; optional on the property because third-party importers may write voided rows without one. PSP-side capture-failure events (Adyen `CAPTURE_FAILED`, Checkout.com capture-declined) are deliberately excluded — see proposal 0018 §1 and §10.1.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "void_note",
          "description": "",
          "display_name": "Free-form human-readable colour on a voided payment, complementing the closed `void_reason` enum. Mirrors Adyen's open `reference` field pattern. Capped at 1024 characters at the handler level.",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "psp_capture_reference",
          "description": "",
          "display_name": "PSP-issued reference to the capture leg, distinct from the auth's `auth_correlation_id`. Stripe `latest_charge.id`; Adyen `/captures` response `pspReference` (a new pspRef distinct from the auth pspRef); Checkout.com `action_id`; Cybersource capture `id`. Per-payment join key for reconciliation with PSP-side records.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "psp_void_reference",
          "description": "",
          "display_name": "PSP-issued reference to the void / cancel / reversal leg. Stripe PaymentIntent state-transition event id; Adyen `/cancels` or `/reversals` response `pspReference`; Checkout.com `action_id`.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "acquirer_reference_number",
          "description": "",
          "display_name": "23-digit ARN (Acquirer Reference Number) assigned post-clearing. The cross-network join key for chargeback investigation; load-bearing at dispute time. Often unavailable at capture time (lands hours later via PSP reconciliation report); populated as a follow-up enrichment when present. Mastercard Banknet Reference and Visa Transaction Identifier (TID, DE 62.2) network references are deferred to a structured sub-object per proposal 0018 §10.2.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payment_kind",
          "description": "",
          "display_name": "Ledger-leg discriminator. `payment` is the original payment authorisation / settlement; `deposit` is a pre-arrival payment taken by `record_deposit_payment` (a refund settling against it is recorded on the booking as deposit returned); `refund` is a negative-amount leg created by `refund_payment` and linked to the original via `payment_refunds_payment`. Unset legacy rows are interpreted as `payment` by handler-level coherence rules. The companion `void_authorisation` action (proposal 0018) is gated to `payment_kind = payment` rows by the `void_requires_payment_kind_payment` validator so refund legs resolve through `resolve_payment_refund` instead.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "refund_requested_at",
          "description": "",
          "display_name": "Timestamp when the host recorded the refund request / PSP accepted the refund request. For refund rows, the required `received_at` mirrors this timestamp so legacy ledger queries that group by `received_at` continue to work without per-leg date logic.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "refund_resolved_at",
          "description": "",
          "display_name": "Timestamp when the refund leg reached a terminal outcome (`status = refunded`, `failed`, or `voided`). Null while the leg is `pending`. Populated by `resolve_payment_refund`, or by `refund_payment` when `refund_initial_status = refunded` for a synchronously-confirmed refund.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "refund_reason",
          "description": "",
          "display_name": "Closed reason taxonomy on a refund leg. Stripe / Adyen canonical three (`duplicate`, `fraudulent`, `requested_by_customer`) plus nine Aerandil-domain hospitality reasons (`reservation_cancelled`, `deposit_return`, `service_recovery`, `overcharge_correction`, `no_show_policy_exception`, `package_component_refund`, `membership_cancellation`, `chargeback_prevention`, `other`), and `chargeback_lost` — the leg `record_payment_dispute` writes when a chargeback is lost, which `refund_payment` never accepts. Required at the handler level for `refund_payment`; optional on the property because third-party importers may write refund rows without one. `other` requires `refund_note` per the `refund_other_requires_note` validator.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "refund_note",
          "description": "",
          "display_name": "Free-form human-readable colour on a refund leg, complementing the closed `refund_reason` enum. Required when `refund_reason = other`; optional otherwise. Capped at 1024 characters at the handler level.",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "refund_correlation_id",
          "description": "",
          "display_name": "Host-side idempotency / reconciliation key for the refund request. Required by `refund_payment` because two legitimate same-amount partial refunds with the same reason can occur against one payment — amount + reason alone cannot key the idempotency cache. If the PSP refund reference is already known synchronously, the host may reuse it here; otherwise the host generates a stable correlation id and stores the later `psp_refund_reference` on resolution.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "psp_refund_reference",
          "description": "",
          "display_name": "PSP-issued refund reference: Stripe `refund.id`, Adyen refund response `pspReference` (a new pspRef distinct from the capture pspRef), Checkout.com refund `action_id`, Worldpay refund event link / reference, Braintree refund transaction id, Cybersource credit id, Mews `RefundId`. Per-leg join key for reconciliation with PSP-side refund records; may be unavailable at request time and populated later by `resolve_payment_refund`.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "refund_trace_reference",
          "description": "",
          "display_name": "Downstream trace reference the cardholder can take to their bank when the refund is not visible on the card. Stripe documents ARN / STAN / RRN for this purpose; often lands days after the refund request via PSP reconciliation. Typed by `refund_trace_reference_type` so the operator knows which network reference to quote — Visa ARN, network STAN, network RRN, or a PSP-internal trace handle.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "refund_trace_reference_type",
          "description": "",
          "display_name": "Type tag on `refund_trace_reference`. `arn` (23-digit Acquirer Reference Number); `stan` (System Trace Audit Number); `rrn` (Retrieval Reference Number); `psp_generated` (PSP-internal handle when the network reference is not yet exposed); `unavailable` (same-day reversals and pre-clearing refunds may have no traceable network reference at all). Kept separate from 0018's `acquirer_reference_number` because refund reversals can lack an ARN while still having STAN / RRN evidence; `refund_trace_reference_type_coherence` enforces the reference / type pairing.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "refund_failure_reason",
          "description": "",
          "display_name": "PSP / network reason for a failed or voided refund leg (e.g. `declined`, `insufficient_funds`, closed account, processor settlement decline, operator cancellation). Required at the handler level when `resolve_payment_refund(refund_status = failed)`; optional otherwise. Free-form because PSPs do not converge on a closed taxonomy of refund-failure codes — Stripe, Adyen, Braintree, and Cybersource each expose their own error vocabulary that the host typically pastes through verbatim.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for connector-landed payments",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "hold_decision",
          "description": "Set on a pending card hold when its stay checks out: `release` (the card rail voids it), `capture` (the bill still owed money — the rail takes that from the hold, which lets the rest go), or `keep` (the house's post-stay window is on; the hold stays for charges found after departure until `hold_kept_until`). Absent on a hold no departure has decided.",
          "display_name": "What check-out decided for this hold",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "hold_decided_at",
          "description": "",
          "display_name": "When check-out decided this hold",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "hold_kept_until",
          "description": "",
          "display_name": "Until when a kept hold is kept for post-stay charges",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "hold_outcome",
          "description": "",
          "display_name": "What became of a decided hold at the processor: released, captured, lapsed, or unresolved (still pending; the desk settles it)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "hold_outcome_detail",
          "description": "",
          "display_name": "The processor's words when a hold's outcome is unresolved",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "hold_purpose",
          "description": "Set on a card hold when it is placed: `booking` (the guarantee before arrival — the pay page's hold, or a desk hold on the booking's own account), `check_in` (the desk's hold on the stay's folio), `top_up`, `re_placement` (the card rail renewing a booking hold before the issuer lapses it), `incidentals`, `no_show`, `post_stay`, or `other`. A hold carried from the booking onto the stay at check-in keeps `booking`. Absent on a payment taken in one step.",
          "display_name": "Why this hold was placed",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "processor_fee",
          "description": "Stripe's actual processing fee on the payment's charge, read off the platform's balance transaction for it (its `fee`; a partial capture's reversal line counted in) by `record_processor_fee`. On a destination charge the fee is the platform's, recouped through the application fee — `processor_fee` beside `processor_fee_estimate` is the variance the pricing decision reports per account per month; the hotel is never trued up. In the platform's balance currency.",
          "display_name": "What the processor charged for this payment",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "net_to_hotel",
          "description": "What the transfer to the hotel carried less the application fee the platform took back, net of any transfer reversal or fee refund a refund made — the platform's balance transactions for the payment, folded by `record_processor_fee`. Equals the hotel's own payout line for the charge (amount less fee). Chargeback movements are the case's (`platform_dispute_cost`), not counted here.",
          "display_name": "What reached the hotel's own account for this payment",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "platform_fee",
          "description": "The platform's fee on this card payment as the account's fee schedule priced it for the card's class (`fee_class`, `card_present`) — the processor's `application_fee_amount`, set when the money was taken, on what was taken. Absent on a payment that settled to the platform's own account.",
          "display_name": "The platform's fee taken from this payment",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "processor_fee_estimate",
          "description": "The processor's published rate for the card's class on what was taken (its conversion fee added when the charge's currency is not the schedule's) — the estimate `processor_fee` is read back against. In the charge's currency.",
          "display_name": "The processor's fee for this payment, as the schedule estimates it",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "fee_class",
          "description": "",
          "display_name": "The card's class the platform's fee was priced by — `domestic_standard`, `domestic_premium`, `regional` or `international`, read from the card's issuing country against the account's",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "card_present",
          "description": "",
          "display_name": "Whether the payment was priced as card-present (a terminal) rather than online",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "platform_fee_refunded",
          "description": "What of the platform's fee the card rail refunded to the hotel when the hosted page this payment came from completed: the link fixed its fee at the account's dearest card class (`payment_request.page_fee_ceiling`), and the difference to the class of the card the guest paid with went back as an application-fee refund. `platform_fee` is what the platform kept. Zero when nothing was due or the refund was refused (`fee_true_up`). In the payment's currency.",
          "display_name": "The platform's fee refunded to the hotel",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "fee_true_up",
          "description": "Set by `record_page_fee` on a payment a hosted page took: `refunded` (the difference to the card's own class was refunded), `not_due` (nothing to give back), `refused` (the processor refused the refund; the hotel paid the ceiling, `fee_true_up_detail` says why, and the operator's variance shows it). Absent on every other payment.",
          "display_name": "How the page's fee came out",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "fee_true_up_reference",
          "description": "The processor's id for the application-fee refund the page's true-up made (Stripe `fr_…`).",
          "display_name": "The processor's refund of the platform's fee",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "fee_true_up_detail",
          "description": "",
          "display_name": "The processor's words for a refused fee refund",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "fraud_warning_reference",
          "description": "Stripe's `issfr_…` for the early fraud warning an issuer raised on this payment's charge — the join key for every later report about the same warning.",
          "display_name": "The processor's id for the issuer's early fraud warning",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "fraud_warning_at",
          "description": "Set when an issuer flags this payment as suspected fraud (an early fraud warning: Visa's TC40, Mastercard's SAFE report). About 40% of Visa and Mastercard warnings become fraud disputes when the charge is not refunded, and Visa's VAMP counts the warning itself whatever happens next. Absent on a payment no issuer has flagged.",
          "display_name": "When the issuer's early fraud warning was raised",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "fraud_warning_type",
          "description": "",
          "display_name": "The kind of fraud the issuer labelled",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "fraud_warning_actionable",
          "description": "",
          "display_name": "Whether the processor still calls the warning actionable — not yet disputed and not refunded in full",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "fraud_warning_observed_at",
          "description": "",
          "display_name": "The processor's time of the latest report about the warning on file — the watermark an out-of-order report is checked against",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "fraud_warning_response",
          "description": "Decided once, from the stay, when the warning first arrives: `hold_voided` (an uncaptured hold released before capture — the cardholder cannot dispute it), `refunded` (a captured line after the stay at or under the house's ceiling, refunded in full), `desk_review` (a deposit before arrival, a stay in house, a line above the ceiling, or a release or refund the ledger refused — a person decides), `not_actionable` (already disputed or refunded, or nothing left to act on).",
          "display_name": "What the card rail did about the warning",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "fraud_warning_stay_phase",
          "description": "",
          "display_name": "Where the booking stood when the warning arrived",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "fraud_warning_detail",
          "description": "",
          "display_name": "Why the card rail responded as it did, in words",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.charge_virtual_card",
          "description": "Ask the card rail to charge an OTA virtual card for what its booking owes — the channel's card, charged once the channel's activation date has come at the house (`activation_at`). The standing automation `virtual_card_charge_on_activation` applies it; the desk may too, and may ask again after a refusal. The amount is what the booking owes (`expected_total` less `deposit_paid_amount`), never more than the card was funded with (`funded_amount`), and the funded amount alone when the booking froze no total. Refused for anything but an active `virtual` card, or before its activation instant. A card already `requested` or `charged` changes nothing, so a sweep that fires twice asks once. When there is nothing to charge — the booking was cancelled, nothing says what it owes, or it owes nothing — the card records `refused` with the reason instead. Emits `payment_card.activation_charge_requested.recorded` (the card rail charges it off-session) or `payment_card.activation_charge_refused.recorded`."
        },
        {
          "api_name": "hospitality.action.keep_payment_card_for_booking",
          "description": "The card rail's record that a booking's pay page was paid with a card already on file (the same card fingerprint, under the new Customer a Payment Link makes every time): the card on file is bound to this booking too, and its retention clock moves to the later of its own and the page's, so the copy the page made can be let go at Stripe without the booking losing its guarantee or the card being retired under it. Refuses a card that is not active, carries no off-session consent, or is held by another processor. Internal-runner: it records what the processor's checkout session said, and the webhook is the only caller."
        },
        {
          "api_name": "hospitality.action.record_virtual_card_charge",
          "description": "**Card-rail only.** Record what the card rail's off-session charge of a `requested` virtual card came to. `charged` posts the money as a settled `credit_card` payment carrying the processor's reference, drawn on the card, onto the booking's own account (opened if it has none) and reachable from the booking; the card reads `charged` and names the payment. `refused` records why — a decline, a card with no processor customer — for the desk, who may ask again. Recording the same charge twice changes nothing. Emits `payment_card.activation_charged.recorded` or `payment_card.activation_charge_refused.recorded`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.retire_payment_card",
          "description": "Take a card out of use: `active → removed` when the guest asks or the desk withdraws it, `active → expired` when it simply aged out. Never a deletion — a card that backed a settled payment is chargeback evidence, and `payment_drawn_on_card` has to keep pointing at something. Forgetting the record is the retention clock's job. Validators reject a card that is already terminal."
        }
      ],
      "api_name": "hospitality.payment_card",
      "description": "A tokenised card the property may draw on later — the card-on-file record behind a no-show fee, a post-departure incidental, or a check-in guarantee. Holds the PSP's token and the fragments that identify the card to a human (scheme, last four, expiry); never a PAN. Binds to the person who holds it, or — for an OTA virtual card, which has no cardholder — to the reservation it was minted for; a pay page's card whose payer is not the booking's guest binds to that booking alone.",
      "display_name": "Payment card",
      "domain": "finance",
      "interfaces": [
        "hospitality.interface.retention_bound"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Card record id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "provider",
          "description": "",
          "display_name": "PSP that holds the card (`stripe`, `adyen`). Free-form for the same reason `decision_provider` is: the set of processors is the operator's, not ours.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "provider_card_ref",
          "description": "",
          "display_name": "The PSP's token for this card (Stripe PaymentMethod id, Adyen recurringDetailReference). The only thing that can actually move money, and the reason this object exists — `payment_token_no_pan_shape` refuses anything Luhn-shaped here exactly as it does on `Payment.payment_method_token`.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "provider_customer_ref",
          "description": "",
          "display_name": "The PSP-side customer the card is attached to (Stripe Customer id). Required by the processor before a card can be charged off-session; distinct from `hospitality.person`, which is ours.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "merchant_account_ref",
          "description": "",
          "display_name": "The merchant account — the merchant of record — the card's stored-credential agreement is with: the one whose PSP Customer the card hangs off and the only one that may draw on it (2026-10-22). Absent for a card kept on the platform's own account and for a channel's virtual card.",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "network_fingerprint",
          "description": "",
          "display_name": "PSP-issued stable identifier for the underlying card number — the same physical card presented by two guests carries the same value. Not derivable back to a PAN, but it *is* a cross-profile card identity, so it carries the token marking: a fraud cohort is a legitimate use, a curiosity is not.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "brand",
          "description": "",
          "display_name": "Card scheme",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "last4",
          "description": "",
          "display_name": "Last four glyphs of the card number — the maximum fragment stored, never the full number. For a wallet card this is the device account number, not the funding card's.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "expiry_month",
          "description": "",
          "display_name": "Expiry month, 1-12",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "expiry_year",
          "description": "",
          "display_name": "Expiry year, four digits",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "funding",
          "description": "",
          "display_name": "What the card draws on, as the scheme reports it. The host maps this onto `Payment.method` at posting time rather than the card carrying the ledger's word for it.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "issuing_country",
          "description": "",
          "display_name": "Card's issuing country (ISO 3166-1 alpha-2). Drives interchange and the domestic/cross-border SCA question.",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "capture_kind",
          "description": "",
          "display_name": "How the card's details reached the PSP. Decides card-present interchange, who carries chargeback liability, and whether the cardholder's presence satisfied SCA — not a provenance note.",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "card_format",
          "description": "",
          "display_name": "Whether the number belongs to a person (`physical`) or to one booking (`virtual` — an OTA-issued single-stay card).",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "wallet",
          "description": "",
          "display_name": "The wallet the card was presented through, when it was. Unset means presented directly.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "off_session_consent",
          "description": "",
          "display_name": "True when the cardholder authenticated with the stated intent that the property may charge this card later. The SCA precondition for a no-show fee or a post-departure incidental: a card saved without it may simply be declined at the moment the guest is unreachable, so the fact is recorded at capture rather than assumed at charge time.",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Usability: `active`, `expired`, or `removed`. Never deleted — a card that backed a settled payment is chargeback evidence.",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "stored_at",
          "description": "",
          "display_name": "When the card was recorded",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_used_at",
          "description": "",
          "display_name": "When a payment was last drawn on this card. Drives the desk's ordering of a guest's cards, and the dormancy half of the retention argument.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "retain_until",
          "description": "",
          "display_name": "Storage-limitation clock the pruner honours — the same discipline `guest_id_document` carries. A card kept past the purpose that justified keeping it is a finding in any PCI or GDPR review.",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "activation_date",
          "description": "",
          "display_name": "For a `virtual` card, the first day the channel lets the house charge it, in the house's calendar",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "activation_at",
          "description": "",
          "display_name": "The instant the house's day reaches `activation_date` (its local midnight) — what the charge automation compares with the clock",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "funded_amount",
          "description": "",
          "display_name": "What the channel loaded the virtual card with — the most the house may charge it",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "activation_charge_status",
          "description": "",
          "display_name": "Where the activation charge stands: `requested` (the card rail has it), `charged` (the payment is on the booking's account) or `refused` (no charge, and why in `activation_charge_refusal`)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "activation_charge_amount",
          "description": "",
          "display_name": "What the activation charge asked the card for",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "activation_charge_attempts",
          "description": "",
          "display_name": "How many times the activation charge has been asked for",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "activation_charge_requested_at",
          "description": "",
          "display_name": "When the activation charge was last asked for",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "activation_charged_at",
          "description": "",
          "display_name": "When the processor took the activation charge",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "activation_payment_id",
          "description": "",
          "display_name": "The payment the activation charge posted to the booking's account",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "activation_charge_refusal",
          "description": "",
          "display_name": "Why the activation charge did not happen — the desk's to resolve",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Where the card's off-session consent was given, when narrower than its cardholder's: `guest_device_page` — on a booking's pay page, whose terms cover the bookings the card is bound to and not the cardholder's others. Unset for a card the desk, an import or a channel stored.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "cardholder_name",
          "description": "",
          "display_name": "The payer's name as the hosted page took it, on a page's card whose payer is not the booking's guest — such a card has no cardholder person and binds to the booking alone",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.assemble_dispute_evidence",
          "description": "Build the evidence packet for a chargeback that is awaiting evidence, from what the kernel already holds: the folio, the cancellation terms frozen at booking, the stay record, the guest, and the payment's 3DS outcome. Picks the argument from the dispute's reason and what happened to the booking (`fraud`, `no_show`, `cancellation`, `duplicate`, `service`), and records every document against the source object's id and version, plus the evidence it wanted and could not find. **Never submits.** A case can be submitted once and countering costs a fee, so that decision stays with a person; this writes a `draft`. (Move 13, 2026-10-22: a draft with no gaps still unsent 48 hours before the deadline is submitted by the night audit's sweep, as `system`.) Re-running it supersedes the unsent packet and bumps `packet_version` — the way to pick up a late folio correction. Refuses: a dispute not awaiting evidence (`needs_response` or `inquiry_needs_response`), a deadline already past, and a case whose evidence has already gone to the issuer."
        },
        {
          "api_name": "hospitality.action.open_dispute_fee_attempt",
          "description": "**Card-rail only.** Write down a retry of a lost chargeback's owed fee before the card rail asks the processor to debit the hotel's balance: `dispute_fee_attempts` counts it, `dispute_fee_attempt_open_at` and `dispute_fee_last_attempt_at` are stamped, and the processor's idempotency key is derived from the attempt's number. An attempt already open is kept as it was, so an interrupted retry reaches the processor as the same request; `record_dispute_fee_recovery` closes it. Refused for a case whose fee is not `owed`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_dispute_evidence_submission",
          "description": "**Card-rail only.** Record what the processor did with the evidence packet (`packet_id`, one of the case's own) the house chose to counter with. `submitted`: the packet is marked `submitted` with `submitted_at` and whether the house acknowledged its gaps (`gaps_acknowledged`, `acknowledged_by`); the case records `evidence_submitted_at`, the processor's `submission_count`, the uploaded `files` (one per evidence type, the processor's file ids), their count and the packet's version, and clears any earlier refusal; any other unsent packet on the case is superseded, since every network takes one submission. Emits `payment_dispute.evidence_submitted`. `refused`: the processor's words (`detail`) are recorded on the case and on the packet, which is not sent again — assembling another packet is the retry. Emits `payment_dispute.evidence_refused`; a packet that was `sending` is a draft again (a refused packet is never sent again). `sending` (F1, 2026-10-19): written before the dispute update goes — the packet is marked `sending` with the uploaded `files` and `sending_at`, the house's acknowledgement of its gaps, and the case `evidence_sending_at`; a retry after a lost answer finishes that packet with the same files under the same key, and no packet is assembled over it until the processor's answer is recorded (either outcome clears the mark). A replay of any outcome changes nothing; a refusal after the case's evidence went to the issuer is ignored. Refuses a packet that is not this case's, one the desk accepted the case instead of countering, and marking `sending` a packet the processor refused or a case whose evidence has gone. Move 12 (2026-10-22): a `submitted` record carries the dispute-fee waiver's certification, decided by the card rail from the facts the kernel holds (`waiver_certified`, and when not, the first condition missed — `waiver_refusal`, `waiver_refusal_condition`; the Radar rating, the account's chargebacks and charges in the rolling year and the cap's figures it rested on), stamped on the case with `waiver_decided_at` and carried on the event; a certification the record itself contradicts (a packet with gaps, a late submission, a reason the packet does not defend) is refused. Move 13: `submitted_by` names who sent the packet (`system` for the night audit's sweep), kept on the packet. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_dispute_fee_recovery",
          "description": "**Card-rail only.** Record what of a lost chargeback's fee the card rail passed to the hotel, beside the network's fee (`dispute_fee`): `recovered` (all of it — off the hotel's transfer and debited from its Stripe balance), `owed` (what neither could take, with the processor's `detail`; a later retry takes it), or `other_currency` (Stripe charged it in another currency than the hotel is paid in; never converted by guesswork), or (N1, 2026-10-19) `written_off` (still owed after the card rail's last retry at the night audit: `owed` names it, recorded as `dispute_fee_written_off`, and the platform carries it), or (F1, 2026-10-19) `needs_review` (a retry whose outcome the card rail could not read back from the processor: parked for a person with `owed` kept on the case, and no retry is opened on it), or (move 12, 2026-10-22) `waived` (the case was certified for the dispute-fee waiver when its evidence was submitted, so the network's fees are not passed through: `waived` names them, recorded as `dispute_fee_waived` with `dispute_fee_waived_at`, nothing is owed and nothing is paid to the hotel; refused for a case that was not certified, and emits `payment_dispute.fee_waived` once). `recovered` says what was passed on. Any outcome closes an open retry (`open_dispute_fee_attempt`). `attempted_at` (an `owed` record of the debit at the chargeback's close) stamps `dispute_fee_last_attempt_at`, so the first retry waits its spacing. Recording the same state twice changes nothing. Emits `payment_dispute.fee_owed` when some is owed (once per amount owed) and `payment_dispute.fee_written_off` once when it is written off. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_dispute_recovery",
          "description": "**Card-rail only.** Record the processor's transfer reversal that pulled a lost chargeback's disputed amount back off the hotel's transfer (`reversal`, Stripe `trr_…`) as the case's `recovery_reversal_reference` — the mark a late win reads to know there is money to give back. The same reversal again changes nothing; a different one is refused (a case is pulled back once). **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_late_win",
          "description": "**Card-rail only.** Record what became of giving the hotel back what a lost chargeback took from it, once the issuer reversed its decision (Stripe's \"late win\"): `transferred` — one transfer to the hotel's account for the disputed amount the loss pulled back, less the platform's fee the processor refunded to the hotel with that reversal (`less_platform_fee`), plus the dispute fee passed on to it (`amount`, of which `fee`), named by `transfer` and made `at`; or `pending_operator` — the processor refused the transfer (a cross-border account, a currency the platform does not hold) or the fee's outcome could not be read back, with the processor's `code` and `detail`: the operator moves the money and the card rail does not ask again. Either way the fee is no longer the hotel's (`dispute_fee_recovery = returned`, nothing left owed, no retry opened); a transfer also nets what it returned out of `dispute_fee_recovered`. Refused on a case not `won`, and once a transfer is recorded for anything but that same transfer, or once the operator settled it by hand. The same record again changes nothing. Emits `payment_dispute.won_late` once per outcome. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_platform_dispute_cost",
          "description": "**Card-rail only.** Write on a chargeback what it cost the platform after everything passed to the hotel (`platform_dispute_cost`): the net of the case's platform ledger lines, negated — the network's withdrawal and fee, a reinstatement on a win, the amount and fee recovered from the hotel, money returned after a late win. Zero when fully passed through; the received fee Stripe never returns is what a won case leaves. Computed, never handed in."
        },
        {
          "api_name": "hospitality.action.settle_dispute_fee_review",
          "description": "**Operator only, through its route** (`POST /v1/payment-disputes/{dispute_id}/fee-review`). Settle a lost case's dispute fee the card rail parked `needs_review` — a retry whose debit could not be read back from the processor, never retried blind. `recovered`: the person found the debit (`reference`, Stripe `py_…`); what was owed is added to `dispute_fee_recovered`. `written_off`: it is not there and the platform carries the fee (`note` says why); what was owed becomes `dispute_fee_written_off`. Either way nothing is owed any more, no retry is opened, and the case keeps who settled it, when, and what they found. Refuses a case that is not parked for review and a write-off without a note; the same settlement again changes nothing, a different one after it is refused. Emits `payment_dispute.fee_review_settled`. Nothing is asked of the processor. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.settle_late_win_by_hand",
          "description": "**The operator's route only.** Record what the platform operator did about a late chargeback win the card rail left for them (`hotel_reinstatement = pending_operator`: the processor refused the transfer back to the hotel, or the fee's outcome was unknown): `transferred_by_hand` — they paid the hotel exactly what the case owed (`hotel_reinstated`), named by the transfer or payout they made (`reference`, Stripe `tr_…` / `po_…`, optional for a bank transfer made outside Stripe); or `written_off` — `amount` (from nothing up to what was owed) went back and the rest is not owed, for the reason in `note` (required). Writes `hotel_reinstatement = settled_by_operator`, the settlement, what went back and what was written off, when and by whom; keeps the processor's refusal beside it. Refused on a case not left for the operator. The same settlement again changes nothing; a different one is refused. Emits `payment_dispute.late_win_settled` once. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        }
      ],
      "api_name": "hospitality.payment_dispute",
      "description": "A chargeback or pre-dispute inquiry raised against a settled payment. Mirrored from the processor, which mirrors the card network — the kernel never decides a dispute's state, it records one. The money moves at `opened_at` (withdrawn) and again at `closed_at` if the case is won; between those the funds are held by the network and the payment cannot be refunded outside the dispute process.",
      "display_name": "Payment dispute",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Dispute record id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "psp_dispute_reference",
          "description": "",
          "display_name": "The processor's own id for the case — Stripe `du_…`, Adyen dispute `pspReference`. The join key for every later event about this dispute, and the only handle that survives into a representment.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Reason",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Disputed amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "opened_at",
          "description": "",
          "display_name": "Opened at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "evidence_due_by",
          "description": "",
          "display_name": "Evidence due by — the issuer's deadline, typically 7 to 21 days. Missing it loses the case by default.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "evidence_submitted_at",
          "description": "",
          "display_name": "Evidence submitted at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "submission_count",
          "description": "",
          "display_name": "How many times evidence has been submitted",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "funds_withdrawn_at",
          "description": "",
          "display_name": "Funds withdrawn at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "funds_reinstated_at",
          "description": "",
          "display_name": "Funds reinstated at — set only when the case is won, and the one field that says the money actually came back rather than the paperwork merely closing.",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "closed_at",
          "description": "",
          "display_name": "Closed at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "last_observed_at",
          "description": "",
          "display_name": "When the processor last said this was the case's state. Webhook delivery is not ordered, so this is the watermark a mirror is checked against: an older view of the case is dropped rather than allowed to regress a decided one.",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "charge_still_refundable",
          "description": "",
          "display_name": "Whether the disputed payment can still be refunded. False for most open chargebacks: refunding outside the dispute process is blocked, and doing it anyway pays the cardholder twice.",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "dispute_fee",
          "description": "What the processor charged for the case, net of any fee it returned — Stripe's dispute fee, read off the dispute's balance transactions (the countered fee comes back on a win; the received fee never does). On a hotel's own account it is passed to the hotel at cost when the case is lost — its own transfer reversal beside the disputed amount, and what the transfer cannot carry debited from the hotel's Stripe balance; `dispute_fee_recovered` says what was. On the platform's, it stays the platform's.",
          "display_name": "Dispute fee",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "dispute_fee_recovery",
          "description": "",
          "display_name": "recovered, owed, other_currency (never converted by guesswork), written_off (the platform carries it after the last retry), needs_review (a retry whose outcome could not be read back from the processor: parked for a person, never retried blind), or returned (the case was won after it was lost: the fee is no longer the hotel's, and what it paid goes back with the disputed amount — see `hotel_reinstatement`), or waived (move 12: the case was certified for the dispute-fee waiver when its evidence was submitted, so the platform did not pass the network's fees through — `dispute_fee_waived`; never a payment to the hotel)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "dispute_fee_recovered",
          "description": "",
          "display_name": "What of the dispute fee the hotel was charged: off its transfer and debited from its Stripe balance",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "dispute_fee_owed",
          "description": "",
          "display_name": "What of the dispute fee could not be taken yet — present only while some is owed",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "dispute_fee_recovery_detail",
          "description": "",
          "display_name": "Stripe's words for why the rest was not taken",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "evidence_packet_version",
          "description": "",
          "display_name": "Which assembly of the evidence packet was submitted (`dispute_evidence_packet.packet_version`)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "evidence_files",
          "description": "",
          "display_name": "The files the submission attached, one per evidence type, as the processor's file ids (Stripe `file_…`, uploaded with purpose `dispute_evidence`)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "evidence_file_count",
          "description": "",
          "display_name": "How many files the submission attached",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "evidence_refusal",
          "description": "",
          "display_name": "The processor's words for the last submission it refused — cleared by a submission it took",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "evidence_refused_at",
          "description": "",
          "display_name": "When the processor refused the last submission",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "has_evidence",
          "description": "",
          "display_name": "Whether the processor holds evidence for the case (Stripe's `evidence_details.has_evidence`: staged or submitted)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "evidence_past_due",
          "description": "",
          "display_name": "Whether the last submission reached the processor after the deadline — its delivery to the issuer is then not guaranteed (Stripe's `evidence_details.past_due`)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "dispute_fee_attempts",
          "description": "",
          "display_name": "How many retries of an owed dispute fee the card rail has opened (the debit at the chargeback's close is not counted)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "dispute_fee_attempt_open_at",
          "description": "",
          "display_name": "Set while a retry is open — written before the processor is asked, cleared when its outcome is recorded; an open retry is asked again under the same key",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "dispute_fee_last_attempt_at",
          "description": "",
          "display_name": "When the card rail last retried the owed fee",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "dispute_fee_written_off",
          "description": "",
          "display_name": "What of the dispute fee was still owed after the last retry and is carried by the platform",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "dispute_fee_written_off_at",
          "description": "",
          "display_name": "When the owed fee was written off",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "evidence_sending_at",
          "description": "",
          "display_name": "Set while an evidence packet's dispute update is on its way to the processor — written before it is sent, cleared when the processor's answer is recorded. Set without `evidence_submitted_at`, the processor may already hold the evidence: submitting again finishes that packet (the same files, the same key) and records its answer",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "platform_dispute_cost",
          "description": "The net of every platform balance transaction for the case — the network's withdrawal of the amount and its fee, a reinstatement on a win, what the card rail recovered from the hotel (the amount's reversal, the fee's reversal or debit), money returned to the hotel after a late win — negated, so a cost is positive and a case fully passed through is zero. Written by `record_platform_dispute_cost` from the case's platform lines. The received fee Stripe never returns is what a won case leaves here.",
          "display_name": "What the case cost the platform after pass-through",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "recovery_reversal_reference",
          "description": "",
          "display_name": "The processor's transfer reversal that pulled the lost case's disputed amount back off the hotel's transfer (Stripe `trr_…`, tagged `aerandil_dispute`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "hotel_reinstatement",
          "description": "",
          "display_name": "After a late win (the case lost, then won): transferred (the hotel was given back the amount pulled from it and the fee it was charged, in one transfer) or pending_operator (the processor refused the transfer, or the fee's outcome could not be read back: the operator moves the money, and the card rail does not ask again) or settled_by_operator (the operator recorded what they did about it — `hotel_reinstatement_settlement`)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "hotel_reinstated",
          "description": "",
          "display_name": "What goes back to the hotel after a late win — the disputed amount the loss pulled back plus the dispute fee passed on to it",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hotel_reinstated_fee",
          "description": "",
          "display_name": "Of `hotel_reinstated`, the dispute fee the hotel had been charged",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hotel_reinstated_at",
          "description": "",
          "display_name": "When the transfer giving the hotel its money back was made — for a late win settled by hand, when the operator recorded it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "reinstatement_transfer_reference",
          "description": "",
          "display_name": "The processor's transfer that gave the hotel its money back (Stripe `tr_…`, tagged `aerandil_late_win`) — or, settled by hand, the operator's own transfer or payout (`tr_…` / `po_…`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "hotel_reinstatement_code",
          "description": "",
          "display_name": "The processor's error code for a transfer it refused (Stripe's `error.code`), when the late win is left for the operator",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "hotel_reinstatement_detail",
          "description": "",
          "display_name": "The processor's words for a transfer it refused, when the late win is left for the operator",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "hotel_reinstated_less_platform_fee",
          "description": "",
          "display_name": "What was netted out of `hotel_reinstated`: the platform's application fee the processor refunded to the hotel with the loss's reversal (read off the processor's fee refunds, never the ledger). The hotel already had it back, so the late win returns the reversal net of it and the hotel ends where it was before the dispute — the charge less the platform's fee",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "card_brand",
          "description": "",
          "display_name": "The disputed card's brand as the processor reports it on the case (Stripe `payment_method_details.card.brand`) — what the evidence's page limit is read by when the payment names no stored card",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "network_reason_code",
          "description": "",
          "display_name": "The card network's own reason code for the case (Stripe `payment_method_details.card.network_reason_code`; Visa `10.4` is card-absent fraud, the code Compelling Evidence 3.0 answers)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "visa_ce3_status",
          "description": "",
          "display_name": "The processor's Visa Compelling Evidence 3.0 status for the case (Stripe `evidence_details.enhanced_eligibility.visa_compelling_evidence_3.status`): requires_action, qualified (the processor's own prior transactions already qualify — the packet leaves them alone), or not_qualified (after a submission: sent as standard evidence)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "visa_ce3_required_actions",
          "description": "",
          "display_name": "What the processor says the case still lacks to qualify for Visa Compelling Evidence 3.0 (Stripe's `required_actions`, verbatim)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "escalated_at",
          "description": "",
          "display_name": "When the processor reported the inquiry escalated to a chargeback after the house had answered it — the inquiry's submission no longer counts against the chargeback's one",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "inquiry_submission_count",
          "description": "",
          "display_name": "The processor's submission count when the inquiry escalated; the chargeback's own submission is counted above it",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "inquiry_evidence_submitted_at",
          "description": "",
          "display_name": "When the house's evidence answered the inquiry, kept when the case escalated and its evidence clock started again",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "dispute_fee_review_reference",
          "description": "",
          "display_name": "The processor's debit a person found when settling a fee the card rail parked for review (Stripe `py_…`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dispute_fee_reviewed_at",
          "description": "",
          "display_name": "When a person settled the fee the card rail had parked for review",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "dispute_fee_reviewed_by",
          "description": "",
          "display_name": "Who settled the parked fee (the principal)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dispute_fee_review_note",
          "description": "",
          "display_name": "What the person who settled the parked fee found",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "hotel_reinstatement_settlement",
          "description": "",
          "display_name": "When a late win the card rail could not give back was settled by the operator (`hotel_reinstatement = settled_by_operator`): transferred_by_hand (they paid the hotel what the case owed, outside the rail) or written_off (some or none of it went back; the rest is not owed, for the reason in the note)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "hotel_reinstatement_written_off",
          "description": "",
          "display_name": "Of what a late win left for the operator owed the hotel, what the operator wrote off rather than paid — `hotel_reinstated` is what did go back",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hotel_reinstatement_settled_by",
          "description": "",
          "display_name": "Who recorded the settlement of a late win left for the operator (the principal)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "hotel_reinstatement_note",
          "description": "",
          "display_name": "The operator's words on how a late win left for them was settled — required for a write-off",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "waiver_certified",
          "description": "",
          "display_name": "Whether the case was certified for the dispute-fee waiver when its evidence was submitted: if it is lost, neither of the network's fees is passed to the hotel. Decided from facts the kernel holds — the packet went without gaps and on time, the reason is one the packet defends, the card was not keyed by hand, Radar rated the payment normal and the desk did not proceed past an elevated rating, the account's chargebacks are under 0.5% of its charges, and the cap has headroom. Cleared when an inquiry escalates (the chargeback is certified at its own submission)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "waiver_decided_at",
          "description": "",
          "display_name": "When the waiver was decided — the submission the certification was stamped with",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "waiver_refusal",
          "description": "",
          "display_name": "Why the case is not covered, in words: the first condition it did not meet",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "waiver_refusal_condition",
          "description": "",
          "display_name": "Which condition the case did not meet first (`WAIVER_REFUSALS`)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "waiver_radar_risk_level",
          "description": "",
          "display_name": "Stripe Radar's rating of the disputed payment at authorisation, as the certification read it (`outcome.risk_level`, verbatim: normal, elevated, highest, not_assessed, unknown) — Stripe's own word, read off the disputed intent's latest charge at certification; never the kernel's assessment",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "waiver_chargebacks_in_year",
          "description": "",
          "display_name": "The account's chargebacks opened in the rolling year the certification counted (this case included; inquiries are not chargebacks)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "waiver_charges_in_year",
          "description": "",
          "display_name": "The account's charges in the rolling year the certification counted — the platform fees Stripe collected on them (`platform_earning` lines)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "waiver_ratio_charge_floor",
          "description": "",
          "display_name": "The charges the rolling year must hold before the chargeback ratio is weighed (1,000): under it, as `waiver_charges_in_year` shows, the ratio was not weighed and cannot be the condition missed",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "waiver_cap_used",
          "description": "What of the account's waiver cap was taken when the case was certified: the fees waived on its cases lost in the rolling year, plus the fees withdrawn so far on its certified cases still open — in the currency of the case's fee",
          "display_name": "What of the account's waiver cap was taken when the case was certified",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "waiver_cap_limit",
          "description": "The account's waiver cap when the case was certified: 25% of the platform fees it paid over the rolling year, net of fees refunded. A case is covered while its own two fees fit under it beside what is taken (`waiver_cap_used` + 2 × the fee received ≤ the cap), or while fewer cases than the floor of five (`waiver_cap_case_floor`) have taken it, whatever the fees",
          "display_name": "The account's waiver cap when the case was certified",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "waiver_cap_cases",
          "description": "",
          "display_name": "How many of the account's cases had taken the cap when this one was certified: waived in the rolling year, or certified and still open",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "waiver_pledged",
          "description": "What a certified case holds of the account's waiver cap while it is open: both of the network's fees as priced at certification — twice the fee Stripe took when the case was received, or twice the last received fee seen at the account when the case carried none. Absent on a case not covered",
          "display_name": "What a certified case holds of the account's waiver cap while open",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "waiver_cap_case_floor",
          "description": "",
          "display_name": "The cap's floor in cases: under it a case is covered whatever the account's fees and its own, from its first live month",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "dispute_fee_waived",
          "description": "",
          "display_name": "What of the network's fees the platform did not pass to the hotel on a certified case it lost — both fees, received and countered. Never paid to the hotel: the platform declined to recover it",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "dispute_fee_waived_at",
          "description": "",
          "display_name": "When the certified case was lost and its fees waived — the rolling year of the cap is counted on it",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.advance_authentication_chase",
          "description": "**Card-rail only.** Take the step a pending authentication's chase is due: past `authentication_due_by`, mark it overdue and tell the house (`payment.authentication_overdue`) — the page stays open and the desk decides; else, at the next reminder's time, queue the page to the guest again at a house with the chase on (`payment.authentication_reminded`) — or, before the page exists, look again in five minutes without spending the reminder. With `page_decline`, record instead that the guest's card was declined on the page (the processor's words): the chase stays pending and says so. With `defer`, the rail could not take the step: it looks again in five minutes. A request that is not a pending chase, or has no step due, changes nothing — the rail's sweep may ask twice. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.cancel_payment_request",
          "description": "Withdraw a payment request. Only a `pending` request can be cancelled (Mews: \"Only payment requests which are in Pending state can be canceled\"); the card rail switches its pay link off. Emits `payment_request.cancelled.recorded`."
        },
        {
          "api_name": "hospitality.action.complete_payment_request",
          "description": "**Card-rail only.** Complete a payment request from the processor's settled outcome — the card rail's webhook verb. A `payment` request records the money through `record_deposit_payment` on the booking (its own account, the deposit ladder) with the processor's reference, in this same transaction; a `preauthorization` names the pending payment the authorise path already recorded (`payment_ref`). Idempotent on the processor reference: a completed request asked again with the same one changes nothing. A request already cancelled or expired still completes — the money moved, and the ledger records what the processor did — flagged `completed_late` for the desk. An authentication chase (`record_authentication_pending`) paid as money lands as what it was for instead — the no-show fee `collected` through `collect_no_show_fee`, the cancellation fee through `collect_cancellation_fee`, a departed stay's late charges through `record_post_stay_collection` — and a chase for a hold completes with the hold the page placed. Emits `payment_request.completed.recorded`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.notify_guest_of_refused_hold",
          "description": "**Card-rail only.** Tell the guest that the card hold they placed on a `preauthorization` request's pay page was not accepted after all: Watchtower answered `block` as the hold was recorded (`fraud_network_signal_ref`, an answer about the request's booking whose floor is `block`), the rail released the hold, and the request stays pending — but the page told the guest it had succeeded. At a house that writes to guests about payments (the pay link's `set_payment_request_messaging`, or for an authentication chase the house's `set_sca_chase`) the request's guest is queued one message in the refusal's own generic words (`guest_message`, never the reason), by email or else SMS under the same consent gate as `message_guest`, with purpose `card_refused`. The request names the answer (`refusal_signal_ref`) and the message (`refusal_notice_ref`, `refusal_notice_at`), or says why none went (`refusal_notice_note`: the house does not send them, no contact, an opt-out, the request no longer pending). Once per request: asked again — a re-delivered webhook — it changes nothing. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_authentication_page_overpaid",
          "description": "**Card-rail only.** Record that an authentication chase's page was paid when what it asked for had already been taken another way — the no-show fee collected by the desk's Try again, the departed stay's bill paid down by its collection — so the page's money has no ledger home: the rail's refund (`refund_reference`), or, when the rail could not give it back, `detail` saying it is left for a person. Written on the chase (`authentication_page_overpaid_at`, the page's intent and the refund, both `payment.token`) and said loudly (`payment.authentication_page_overpaid`). The same page and refund again — a re-delivery — changes nothing. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_payment_request_link",
          "description": "**Card-rail only.** Record the hosted page the card rail minted for a pending payment request — the URL the guest pays on and the processor's handle on it. The rail's own verb: it runs after the request is minted, outside its transaction, because the processor call cannot sit inside one. Refused once the request has left `pending`, so the rail switches a page minted for a request that ended meanwhile straight off. Re-recording the same link is a no-op. At a house that sends pay links (`set_payment_request_messaging`) the link is queued for the booking's guest in the same step — by email, or SMS when the profile holds no email, under the same consent gate as `message_guest` — and the request names the message, or says why none went. `page_fee_ceiling` is the platform's fee the link fixed (in the request's currency, never above its amount), kept as the request's own so the completion can true it up to the card's class. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_payment_request_reauthorisation",
          "description": "**Card-rail only.** Record what the card rail did about a completed preauthorization whose hold is near the issuer's window (`AERANDIL_PREAUTH_REAUTH_DAYS`): with `payment_ref`, the new pending hold it placed on the guest's stored card — the request's `payment_id` moves to it and `reauthorised_at` / `reauthorisation_count` record it; with `refusal_reason`, why it could not (no stored card carrying off-session consent, a decline), stamped `reauthorisation_refused_at` for the desk. Exactly one of the two. The new hold must be pending and carry its processor reference; naming the hold the request already names changes nothing. Emits `payment_request.reauthorised.recorded` or `payment_request.reauthorisation_refused.recorded`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.resend_authentication_chase",
          "description": "Send a pending authentication's page to the guest again — the desk's Re-send. Whatever the house's chase rule, because the desk asked: by email, else SMS, under the same consent gate as `message_guest`'s service messages. Refused when the request is not a pending chase, the rail has not minted its page yet, the guest cannot be written to (the reason says why), or it was re-sent less than ten minutes ago (`chase_resent_recently`)."
        }
      ],
      "api_name": "hospitality.payment_request",
      "description": "The house asking a guest for money against one booking, with a pay link (Mews's payment request): an `amount`, a `reason` (deposit, prepayment, fee, a missing or declined card), a `kind` (`payment` takes the money, `preauthorization` places a hold captured later) and a required `expires_at`. `pending` until the guest pays on the card rail's hosted page (`completed`, with the payment it produced), the house withdraws it (`cancelled`, only from pending) or the night audit finds it past `expires_at` (`expired`). A completed payment lands on the booking's own account through `record_deposit_payment`, so the deposit engine's status ladder moves; a completed preauthorization is the authorise path's pending payment. Money that moves after a request was cancelled or expired is still recorded — the request completes `late` — because a ledger that refuses a settled payment disagrees with the processor.",
      "display_name": "Payment request",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Payment request id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "What the guest is asked to pay (or to have held)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Why the house is asking",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "`payment` takes the money; `preauthorization` holds it for capture later",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "pending → completed / cancelled / expired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "When the request lapses; the night audit closing on or after this instant's date expires it while it is still pending",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "requested_at",
          "description": "",
          "display_name": "When the house asked",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "note",
          "description": "",
          "display_name": "What the guest reads beside the amount (the pay page's line item)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "pay_url",
          "description": "",
          "display_name": "The card rail's hosted page the guest pays on — absent until the rail has minted it",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "psp_link_reference",
          "description": "",
          "display_name": "The processor's handle on the hosted page (a Stripe Payment Link id), so the rail can switch it off when the request ends",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payment_id",
          "description": "",
          "display_name": "The payment the completed request produced",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "When the processor settled it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "completed_late",
          "description": "",
          "display_name": "True when the money arrived after the request was cancelled or had expired — recorded, and worth the desk's look",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "cancelled_at",
          "description": "",
          "display_name": "When the house withdrew it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancellation_reason",
          "description": "",
          "display_name": "Why the house withdrew it",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "expired_at",
          "description": "",
          "display_name": "When the night audit expired it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "guest_notified_at",
          "description": "",
          "display_name": "When the pay link was queued for the guest",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "guest_notice_ref",
          "description": "",
          "display_name": "The outbound message that carries the pay link to the guest",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "guest_notice_note",
          "description": "",
          "display_name": "Why the pay link was not sent to the guest, when it was not",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "refusal_signal_ref",
          "description": "",
          "display_name": "The network's answer that refused the hold the guest's card placed on the pay page",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "refusal_notice_at",
          "description": "",
          "display_name": "When the guest was queued a message that their card was not accepted",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "refusal_notice_ref",
          "description": "",
          "display_name": "The outbound message that tells the guest their card was not accepted",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "refusal_notice_note",
          "description": "",
          "display_name": "Why the guest was not told their card was not accepted, when they were not",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "reauthorised_at",
          "description": "",
          "display_name": "When the card rail last re-placed the hold before the issuer lapsed it",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "reauthorisation_count",
          "description": "",
          "display_name": "How many times the hold has been re-placed",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "reauthorisation_refused_at",
          "description": "",
          "display_name": "When the card rail last tried to re-place the hold and could not",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "reauthorisation_refusal",
          "description": "",
          "display_name": "Why the hold could not be re-placed (no stored card, a decline) — it lapses at the issuer, and the desk takes a new guarantee",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "reuse_terms",
          "description": "",
          "display_name": "The card terms the pay page showed the guest — what they agreed to when the page saved their card for later charges by the house",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "authentication_for",
          "description": "",
          "display_name": "What the off-session attempt the issuer wants confirmed was for",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "authentication_stay_ref",
          "description": "",
          "display_name": "The departed stay whose late charges the guest is asked to confirm",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "stuck_psp_reference",
          "description": "",
          "display_name": "The processor's intent the issuer refused to complete without the cardholder (a Stripe PaymentIntent left `requires_payment_method`); cancelled once the page is paid",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "authentication_detail",
          "description": "",
          "display_name": "The issuer's or the processor's words for the demand",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "authentication_due_by",
          "description": "",
          "display_name": "The house's deadline for the guest to confirm (`set_sca_chase`); past it, the house is told",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "authentication_reminders",
          "description": "",
          "display_name": "How many reminders the house sends before the deadline",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "authentication_reminders_sent",
          "description": "",
          "display_name": "How many reminders have been taken so far",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "authentication_next_chase_at",
          "description": "",
          "display_name": "When the card rail next looks at the chase — the next reminder, else the deadline; cleared once the deadline has passed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "authentication_overdue_at",
          "description": "",
          "display_name": "When the deadline passed with the guest not done and the house was told",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "authentication_page_declined_at",
          "description": "",
          "display_name": "When the guest's card was last declined on the page",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "authentication_page_decline",
          "description": "",
          "display_name": "The processor's words for the decline on the page — the chase stays pending",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "authentication_resent_at",
          "description": "",
          "display_name": "When the desk last re-sent the chase's page (one re-send per ten minutes)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "authentication_page_overpaid_at",
          "description": "",
          "display_name": "When the chase's page was paid after what it asked for had been taken another way — the page's payment has no ledger home",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "authentication_page_overpaid_reference",
          "description": "",
          "display_name": "The processor's intent the overpaid page took",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "authentication_page_refund_reference",
          "description": "",
          "display_name": "The processor's refund that gave the overpaid page's money back",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "authentication_page_overpaid_detail",
          "description": "",
          "display_name": "Why the page's payment had no home, and whether it was given back or is left for a person",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "page_fee_ceiling",
          "description": "The `application_fee_amount` the card rail set on the hosted page when it minted it, in the request's currency: the account's dearest card class online, because a Payment Link's fee is fixed before any card exists. When the page completes the rail reads the card it was paid with and refunds the difference to that card's own class to the hotel, recording the outcome on the payment (`record_page_fee`). Absent when the page settles to the platform's own account (no fee) or was minted before the ceiling.",
          "display_name": "The platform's fee the page's link fixed",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.mark_payout_reconciled",
          "description": "**Card-rail only.** Close a payout's reconciliation from its recorded lines: `line_count`, `money_line_count` (the lines that are a guest's money — not fees, the payout's own line, or a returned payout), `matched_count`, `unmatched_count`, `unmatched_amount` (the size of the money lines that matched no payment), `fees_total` (the lines' fees plus any fee lines), `reversed_total` (a failed payout's money sent again) and `reconciliation_difference` (the payout's amount less the net of every line bar the payout's own) are computed, never typed. Sets `reconciliation_status = completed` and emits `payout.reconciled` when the lines add up; `completed_with_difference` and `payout.reconciliation_differs` when they do not. A re-run that changes no count writes nothing."
        },
        {
          "api_name": "hospitality.action.record_payout_lines",
          "description": "**Card-rail only.** Record balance transactions a payout carried (Stripe's `txn_…`), each with the ledger payment it came from when the reconciliation resolved one. Idempotent by `psp_reference`: a line already recorded is left as it is, unless it was unmatched and now carries a payment. A payment must sit at the payout's house. Refused on a payout the processor has not itemised."
        }
      ],
      "api_name": "hospitality.payout",
      "description": "A payout from a hotel's processor balance to its bank (Stripe's `po_…`), mirrored from the processor's events on the property's merchant account. `status` is the processor's own and `paid` is provisional — a payout can fail after it reads as paid. Once the processor has itemised it, its balance transactions are recorded as `payout_line`s, each matched to the ledger payment it came from, and the payout carries the counts: lines, matched, the net of what did not match, and the fees taken.",
      "display_name": "Payout",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Payout id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "psp_reference",
          "description": "",
          "display_name": "The processor's id for the payout (Stripe `po_…`) — the replay key every event about it lands on. An identifier, not a credential.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "merchant_account_id",
          "description": "",
          "display_name": "The merchant account whose balance paid out (its object id) — the payout_for_merchant_account edge's target",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "property_id",
          "description": "",
          "display_name": "The house the merchant account settles for (its object id)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "The processor's payout status; `paid` is provisional",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reconciliation_status",
          "description": "",
          "display_name": "Whether the payout's lines are on the ledger: `in_progress` while the processor itemises it, `not_started` once it has and the ledger has not, `completed` once recorded and adding up to the payout, `completed_with_difference` once recorded but not adding up (`reconciliation_difference`), `not_applicable` for a manual payout",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "What the payout sends to the bank, as the processor states it (net of everything the lines carried)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "The payout's currency (ISO 4217, upper case)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "arrival_date",
          "description": "",
          "display_name": "The day the processor expects the money in the bank",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "initiated_at",
          "description": "",
          "display_name": "When the processor created the payout",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "automatic",
          "description": "",
          "display_name": "Whether the processor paid it out on its schedule. A manual payout cannot be itemised",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "failure_code",
          "description": "",
          "display_name": "The processor's reason the payout failed (`account_closed`, `no_account`, …)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "failure_message",
          "description": "",
          "display_name": "The processor's words for the failure",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "failed_at",
          "description": "",
          "display_name": "When the ledger heard the payout failed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "line_count",
          "description": "",
          "display_name": "Balance transactions recorded against the payout",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "matched_count",
          "description": "",
          "display_name": "Of those, the lines matched to a ledger payment",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "unmatched_amount",
          "description": "",
          "display_name": "The size of the money lines (charges, refunds, disputes, transfers) that matched no ledger payment — the sum of their nets' magnitudes, so £50 in and £50 out unmatched is £100, not nothing; computed from the lines, never typed",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "fees_total",
          "description": "",
          "display_name": "The processor's fees the lines carried: their `fee` fields plus any separate fee lines",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "reconciled_at",
          "description": "",
          "display_name": "When the ledger finished recording the lines",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "money_line_count",
          "description": "",
          "display_name": "Of the lines, those that are a guest's money and should match a payment (every category bar `fee`, `payout` and `payout_reversal`)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "unmatched_count",
          "description": "",
          "display_name": "Of the money lines, those that matched no ledger payment",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "reconciliation_difference",
          "description": "",
          "display_name": "The payout's amount less the net of every line bar the payout's own — zero when the lines add up to what the bank was sent; otherwise the payout is `completed_with_difference`",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "reversed_total",
          "description": "",
          "display_name": "The net of the `payout_reversal` lines: an earlier failed payout's money, returned to the balance and sent again in this one",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.payout_line",
      "description": "One balance transaction a payout carried (Stripe's `txn_…`): its reporting category, the processor object it came from, the gross, the fee and the net, as the processor states them. `matched` when the source resolves to a ledger payment — a charge to the payment it captured, a refund to its refund leg or original, a dispute to the payment disputed, a destination charge's `py_…` through its transfer to the platform charge. Append-only; a re-run reconciliation may match a line it could not before.",
      "display_name": "Payout line",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Payout line id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "psp_reference",
          "description": "",
          "display_name": "The processor's id for the balance transaction (Stripe `txn_…`) — the replay key",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "payout_id",
          "description": "",
          "display_name": "The payout that carried the line (its object id) — the payout_line_for_payout edge's target",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "property_id",
          "description": "",
          "display_name": "The house the payout is for (its object id)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "reporting_category",
          "description": "",
          "display_name": "The processor's reporting category for the line",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "line_type",
          "description": "",
          "display_name": "The processor's own transaction type (`payment`, `charge`, `payment_refund`, `stripe_fee`, …)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "source_reference",
          "description": "",
          "display_name": "The processor object the money came from (`ch_…`, `py_…`, `re_…`, `du_…`, `tr_…`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Gross, as the processor states it (negative for money out)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "fee",
          "description": "",
          "display_name": "The processor's fee on the line",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "net",
          "description": "",
          "display_name": "Gross less fee — what the line added to the payout",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "available_on",
          "description": "",
          "display_name": "The day the line's money became available to pay out",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "matched",
          "description": "",
          "display_name": "Whether the line resolved to a ledger payment",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "payment_id",
          "description": "",
          "display_name": "The ledger payment the line came from (its object id) — the payout_line_for_payment edge's target",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.capture_guest_id",
          "description": "Record a minimal government-ID document for a guest (statutory registration). Stores only typed facts + a verification result; the raw number / scan is never stored — pass `external_record_reference_id` pointing at an `identity`-domain external record for the vault-held artifact. `retain_until` is the storage-limitation clock."
        },
        {
          "api_name": "hospitality.action.redact_person_recognition",
          "description": "GDPR right-to-erasure surface for recognition data. Zeroes out `mobility_considerations` and free-text recognition fields; leaves projection and financial-audit fields intact per Article 17(3) retention-required-records carve-out."
        },
        {
          "api_name": "hospitality.action.refresh_person_projections",
          "description": "Recompute the denormalised projection fields on `person` (`lifetime_value`, `total_stays`, `direct_stays`, `last_stay_date`) and their `*_computed_at` companions from the underlying event history; `direct_repeat_rate` derives from the stored pair at read time. Operator-side rescue hatch; not the primary refresh path."
        },
        {
          "api_name": "hospitality.action.split_person_merge",
          "description": "Reversible unmerge of the targeted subordinate person: close the live `person_merged_into` link (`effective_to` stamp — the row stays as history) and restore `resolution_status = active`. Re-merging afterwards is a fresh `propose_person_match` → confirm; confirmed candidates are terminal audit rows and are never re-opened. Emits `person.merge_split`."
        },
        {
          "api_name": "hospitality.action.update_guest_profile",
          "description": "Edit the target guest's profile at the desk: names, email, phone, nationality, date of birth, postal address, tax identifier, and the room preferences (language, pillow, floor, quiet room, connecting rooms, dietary). Every parameter is optional and at least one must be passed; omitted fields keep their values. Empty text clears email, phone, nationality, tax identifier, language and pillow; an empty dietary list clears it; an address whose every field is blank clears the address; names cannot be cleared. An address that is not blank must carry line 1, city and country — the struct's own required fields, enforced here rather than left to arrive half-written. Preference edits merge into the existing struct, so the generated fields survive. A call whose values all match the current profile is an idempotent no-op. Recognition, ID documents, consent and merges keep their own verbs. Emits `person.profile_updated` with the changed field names."
        }
      ],
      "api_name": "hospitality.person",
      "description": "An individual person known to the hospitality system — guest, local, member, or any combination across a stay's lifecycle. Persons are tenant-wide so loyalty, recognition, and history follow them across the portfolio. The 'guest' label is a role carried on `reservation_for_guest` and on a stay; the same person may also be a local (visiting a venue without a reservation), a member (enrolled in a membership-club program), or all three.",
      "display_name": "Person",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "given_name",
          "description": "",
          "display_name": "Given name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "family_name",
          "description": "",
          "display_name": "Family name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "email",
          "description": "",
          "display_name": "Email",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "phone",
          "description": "",
          "display_name": "Phone",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "nationality",
          "description": "",
          "display_name": "Nationality (ISO 3166-1 alpha-2)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "date_of_birth",
          "description": "",
          "display_name": "Date of birth",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "address",
          "description": "The guest's own postal address. Required on any tax invoice above the property's simplified-invoice threshold — reg 14(1)(e) of the UK VAT Regulations 1995 wants \"the name and address of the person to whom the goods or services are supplied\", and §14(4) Nr. 1 UStG the same. Snapshotted onto the document at issue.",
          "display_name": "Postal address",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "tax_identifier",
          "description": "A personal tax number where a jurisdiction prints one on a guest's invoice. Rare in the UK; ordinary in parts of southern Europe and Latin America.",
          "display_name": "Tax identifier",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "preferences",
          "description": "",
          "display_name": "Preferences",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "dietary_observance",
          "description": "",
          "display_name": "Faith-driven dietary requirement (reveals religious observance)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "relationship_context",
          "description": "",
          "display_name": "Relationship context (Tier 1.4 / 2 §D.2)",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "lifetime_value",
          "description": "",
          "display_name": "Lifetime value (projection over folio history)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "lifetime_value_computed_at",
          "description": "",
          "display_name": "Lifetime value refresh timestamp",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "direct_stays",
          "description": "",
          "display_name": "Direct-channel checked-out stays (projection)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "total_stays",
          "description": "",
          "display_name": "Total checked-out stays (projection)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "total_stays_computed_at",
          "description": "",
          "display_name": "Total stays refresh timestamp",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "last_stay_date",
          "description": "",
          "display_name": "Most recent stay departure (projection)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "last_stay_date_computed_at",
          "description": "",
          "display_name": "Last stay date refresh timestamp",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "vip_recognition",
          "description": "",
          "display_name": "VIP recognition assignment (proposal 0025)",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "loyalty_id",
          "description": "",
          "display_name": "Loyalty ID",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "consent",
          "description": "",
          "display_name": "Marketing consent",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "resolution_status",
          "description": "",
          "display_name": "Identity-resolution status (proposal 0027)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for connector-minted persons",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.confirm_person_merge",
          "description": "Accept a pending candidate: create the `person_merged_into` link from the subordinate to the caller-chosen survivor (proposal 0027 §8.1), demote the subordinate to `resolution_status = merged_subordinate`, and reconcile the survivor's consent to the most-restrictive across the pair — a withdrawal on either row wins; a merge can never grant consent the subordinate had withdrawn (§6). Rejects self-merges, chains (either party already in a live merge), and non-active survivors. May be routed through a 0020 approval gate by operator policy (§8.2); the action itself is gate-agnostic. Emits `person.merge_confirmed`."
        },
        {
          "api_name": "hospitality.action.reject_person_match",
          "description": "Decline a pending candidate: a human looked and said \"not the same\". Terminal and permanent — the pair is suppressed from re-proposal (only a `manual` re-propose overrides, proposal 0027 §8.3). The candidate row stays as the audit record of the decision. Emits `person.match_rejected`."
        }
      ],
      "api_name": "hospitality.person_merge_candidate",
      "description": "A proposed match between two person rows suspected to be the same human, awaiting a merge decision: who matched, how (deterministic key / probabilistic resolver / manual assertion), how strongly, and on which attributes. Confirming creates the `person_merged_into` link and demotes the subordinate to `resolution_status = merged_subordinate`; rejecting permanently suppresses re-proposal of the pair. Candidates are audit rows — terminal statuses are never deleted.",
      "display_name": "Person merge candidate",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Candidate id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "person_a",
          "description": "",
          "display_name": "First person of the proposed pair",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "person_b",
          "description": "",
          "display_name": "Second person of the proposed pair",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "match_score",
          "description": "",
          "display_name": "Match confidence in [0, 1] (1.0 for deterministic / manual)",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "match_method",
          "description": "",
          "display_name": "How the candidate was surfaced",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "matched_attributes",
          "description": "",
          "display_name": "Attribute names the match keyed on (names only, never values)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Candidate lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "surfaced_at",
          "description": "",
          "display_name": "When the candidate was proposed",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.platform_ledger_line",
      "description": "One balance transaction on the platform's own processor balance (Stripe's `txn_…`) that belongs to a hotel's merchant account: the platform charge a destination payment was made with (and Stripe's processing fee on it), the transfer that carried the hotel's share, the application fee the platform earned, a refund and the reversal that recovered it, a chargeback's withdrawal and what was passed back. Filed at the house the account is for, `matched` when it resolves to a ledger payment or case. Append-only; a line read again may gain a match it could not make before. The platform's lines no hotel owns (its own payouts, Stripe's fees on the platform and the tax on them, a reserve, a charge for an account no merchant account holds) are filed at the tenant scope of the operator's own tenant, with no merchant account, never matched, their day the processor's UTC day.",
      "display_name": "Platform ledger line",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Platform ledger line id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "psp_reference",
          "description": "",
          "display_name": "The processor's id for the balance transaction on the platform's balance (Stripe `txn_…`) — the replay key",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "merchant_account_id",
          "description": "",
          "display_name": "The hotel's merchant account the line was for (its object id) — the platform_ledger_line_for_merchant_account edge's target; absent on the platform's own line, which no hotel owns",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "property_id",
          "description": "",
          "display_name": "The house the account is for (its object id); absent on the platform's own line",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "reporting_category",
          "description": "",
          "display_name": "The processor's reporting category for the line, verbatim (`platform_earning` is an application fee; `other` when the processor named one this ledger does not know)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "line_type",
          "description": "",
          "display_name": "The processor's own transaction type (`charge`, `transfer`, `application_fee`, `transfer_refund`, `adjustment`, …)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "source_reference",
          "description": "",
          "display_name": "The processor object the money moved for (`ch_…`, `tr_…`, `trr_…`, `fee_…`, `fr_…`, `re_…`, `du_…`, `py_…`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Gross on the platform's balance, as the processor states it (negative for money out — a transfer to the hotel, a chargeback)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "fee",
          "description": "",
          "display_name": "The processor's fee on the line, charged to the platform",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "net",
          "description": "",
          "display_name": "Gross less fee — what the line did to the platform's balance",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "available_on",
          "description": "",
          "display_name": "The day the line's money became available on the platform's balance",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "occurred_at",
          "description": "",
          "display_name": "When the processor created the balance transaction",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "occurred_on",
          "description": "",
          "display_name": "The house's calendar day the line counts in — the processor's creation instant in the house's own timezone, the close's rule for a card payment's day; computed when the line is recorded",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "matched",
          "description": "",
          "display_name": "Whether the line resolved to a ledger payment or chargeback",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "payment_id",
          "description": "",
          "display_name": "The ledger payment the line came from (its object id) — the platform_ledger_line_for_payment edge's target",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dispute_id",
          "description": "",
          "display_name": "The chargeback the line moved money for (its object id) — the platform_ledger_line_for_dispute edge's target",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "dispute_leg",
          "description": "",
          "display_name": "What the line did for its chargeback: the network's withdrawal or reinstatement, the amount or fee recovered from the hotel, money returned to the hotel after a late win",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "processor_account_reference",
          "description": "",
          "display_name": "On the platform's own line: the connected account (`acct_…`) the processor named for it when no merchant account on the ledger holds that account — what the operator looks up to place it",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.portfolio",
      "description": "A grouping of properties under a brand or tenant — typically a region, operating company, or franchisee. Used by chains for reporting and permission rollups.",
      "display_name": "Portfolio",
      "domain": "organization",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Portfolio code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Portfolio name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "region",
          "description": "",
          "display_name": "Region",
          "kind": "enum",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.pre_shift_lineup_event",
      "description": "An instance of a pre-shift lineup (or its absence). Logged per venue per shift so the operator can validate the cadence and so the absence of expected lineups becomes queryable as a drift signal. The `cancelled` flag distinguishes 'lineup scheduled but didn't happen' from 'no lineup attempted'; absence of rows on a venue with `service_register.lineup_format ≠ none` is the structural drift signal. Per proposal §6, the 60-90-day folklore threshold is not hard-coded — operators query their own drift.",
      "display_name": "Pre-shift lineup event",
      "domain": "service_philosophy",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Lineup event id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the lineup started",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "duration_seconds",
          "description": "",
          "display_name": "Duration in seconds",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "leader",
          "description": "",
          "display_name": "Leader (staff identifier)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "attendance_count",
          "description": "",
          "display_name": "Attendance count",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "attendance_principals",
          "description": "",
          "display_name": "Attendees (staff identifiers)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "culture_moment_present",
          "description": "",
          "display_name": "Culture moment shared",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "culture_moment_text",
          "description": "",
          "display_name": "Culture moment text",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "standards_reinforcement_topic",
          "description": "",
          "display_name": "Standards reinforcement topic",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "operational_briefing_present",
          "description": "",
          "display_name": "Operational briefing covered",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "operational_briefing_summary",
          "description": "",
          "display_name": "Operational briefing summary",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cancelled",
          "description": "",
          "display_name": "Lineup cancelled (scheduled but didn't happen)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "lineup_format",
          "description": "",
          "display_name": "Lineup format",
          "kind": "enum",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.prep_station",
      "description": "A fulfilment station fired lines route to — grill, saute, bar, expo, pastry. fire_course cuts one kitchen ticket per station per course; a menu item's default station is the menu_item_prepared_at_station link.",
      "display_name": "Prep station",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Station id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Station name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Station kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Active",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.privacy_budget_account",
      "description": "Tracks privacy spend for a participant, program, definition, or composition group. Epsilon / delta totals and the running spent / reserved totals are the durable evidence of DP accounting; replays cannot double-spend (proposal 0021 §4.5 / §9).",
      "display_name": "Privacy budget account",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Account id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "account_scope",
          "description": "",
          "display_name": "tenant_program / property_program / definition / composition_group / output_recipient",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "program_api_name",
          "description": "",
          "display_name": "Program api name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "definition_api_name",
          "description": "",
          "display_name": "Definition api name",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "composition_group",
          "description": "",
          "display_name": "Composition group",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "period_kind",
          "description": "",
          "display_name": "Run / day / month / quarter / year / lifetime",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "period_start",
          "description": "",
          "display_name": "Period start",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "period_end",
          "description": "",
          "display_name": "Required unless lifetime",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "epsilon_total",
          "description": "",
          "display_name": "Required for DP accounts",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "epsilon_reserved",
          "description": "",
          "display_name": "Defaults 0",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "epsilon_spent",
          "description": "",
          "display_name": "Defaults 0",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "delta_total",
          "description": "",
          "display_name": "Required when delta applies",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "composition_strategy",
          "description": "",
          "display_name": "Basic / advanced / RDP / zCDP / operator_supplied_reviewed",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "mechanism_family",
          "description": "",
          "display_name": "Central DP / local DP / DP-SGD / DP clean room / threshold-only",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "alert_thresholds",
          "description": "",
          "display_name": "Optional thresholds for operator warnings",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "last_spend_at",
          "description": "",
          "display_name": "Audit convenience",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.privacy_budget_event",
      "description": "Immutable ledger row recording one reservation / commit / release / exhaustion / adjustment against a `privacy_budget_account`. UNIQUE on `(account_id, correlation_id, event_kind)` (migration 0015) so duplicate submissions are no-ops — the Brandur idempotency pattern transposed onto DP accounting.",
      "display_name": "Privacy budget event",
      "domain": "federated_intelligence",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Event id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "account_id",
          "description": "",
          "display_name": "Privacy budget account this event acts on",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "correlation_id",
          "description": "",
          "display_name": "Originating run / batch correlation id — keys the commit-once idempotency",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "event_kind",
          "description": "",
          "display_name": "reserve / commit / release / exhaust / adjust",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "epsilon_delta",
          "description": "",
          "display_name": "Signed change in ε; required for basic / advanced composition accounts. Positive on reserve / commit (budget consumed); positive on release (budget returned to available). The account projection reads delta and the kind together to know whether to increment epsilon_reserved or epsilon_spent.",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "delta_delta",
          "description": "",
          "display_name": "Signed change in δ; populated when the account carries an explicit (ε, δ) shape.",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "rho_delta",
          "description": "",
          "display_name": "Signed change in ρ for zCDP-accounted accounts. The seam to rho-additive composition (Bun–Steinke 2016); v1 leaves this field unwritten for basic-composition accounts.",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Optional free-text reason (audit only; bounded)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "applied_at",
          "description": "",
          "display_name": "Wall-clock timestamp of the ledger write",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.book_function_event",
          "description": "Book one use of a function space: a window on a date, a setup style, and the expected attendance, `tentative` unless asked for `definite`. Refused when the space (or its combined room, or any of its sections) already has a live event overlapping the setup-to-teardown window, when the space is not active, or when the expected count exceeds the room's capacity in that style. A `group_block_id` makes it the group's event; charges then post to the block's master folio by default."
        },
        {
          "api_name": "hospitality.action.contribute_fraud_signal",
          "description": "**Card-rail only.** Offer a confirmed fraud outcome to Watchtower, the card rail's federated fraud network (proposal 0029), as salted one-way fingerprints. Confirmed means one of two things: a dispute on `payment_ref` that the house lost (`chargeback_lost_fraud`), or the house's own block (`confirmed_fraud`, `fraud_risk_assessment_ref`): the assessment applied as `decline_block`, or the hold it gated whose review the house declined (`resolve_payment_review`) — on its own evidence, never one Watchtower refused itself or one scored with the network's answer, or the network would confirm itself. Refused without the house's `fraud_network` consent (its consent policy's default scope, not revoked) and refused for anything that is not a 64-hex digest — no card number, email or phone is ever accepted. One contribution per `source_reference`; a repeat is a no-op. The card rail publishes the digests to the network after this commits. Emits `fraud_network.contribution_recorded`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.create_function_space",
          "description": "Add a bookable function room to the property's diary. `capacities` is a JSON array of `{setup_style, capacity}` rows; a `combined_space_id` makes the new room a section of a divisible one. The code must be unique at the property."
        },
        {
          "api_name": "hospitality.action.create_group_block",
          "description": "Open an operational room block at this property. The block starts `tentative` — it holds no inventory until it is made `definite`. Optionally attaches to the group_request it came from; blocks with no request behind them are legitimate."
        },
        {
          "api_name": "hospitality.action.define_feedback_label",
          "description": "Define one of the house's guest-feedback labels — hubOS's quality labels — under its `code`, with the `name` the desk and the floor read. The same code again re-states it in place (a new name, or `status: retired` to stop it going on feedback, or `status: active` to revive it); an identical statement writes nothing. A retired label stays on the feedback it was put on. Emits `feedback_label.defined`."
        },
        {
          "api_name": "hospitality.action.define_round_template",
          "description": "Define a house's planned walk — hubOS's revision structure: its `name`, `family` (corrective, preventive, technical_legal, inspection_legal), cadence (`fixed` to the calendar or `floating` from the last completion, every `interval_days` — the maintenance plan's vocabulary) and the `checkpoints` in walking order. Each checkpoint has a unique `position`, a `kind` — `room` or `asset` with `ref_id` naming one of this house's rooms or (unretired) assets, or `general` with a `label` and no `ref_id` — and its `items`, each a unique `code`, a `label` and a `kind` (`check`; `reading` with an optional `unit` and optional limits `min` / `max`, inclusive, which judge a reading the walker leaves unjudged; `text`; `photo`, answered by a photo on the checkpoint's result; `section`, a heading never answered — a checkpoint needs at least one item that is not a heading). Replay-safe by its business key, house × name: the same name again revises the active template's family, cadence, description and checkpoints in place (an identical repeat writes nothing; a round already under way keeps the checkpoints it started with). The template carries its `revision` — 1 when defined, one more each time a re-definition moves its checkpoints (words and schedule leave it) — which a start may name. `next_due_on` is the date the next round is due — required the first time; on a revision, absent keeps the schedule. After `retire_round_template` the name mints a new template. Emits `round_template.defined`."
        },
        {
          "api_name": "hospitality.action.define_stock_store",
          "description": "Define one of the house's stock stores under its code — the stores room, a floor cupboard, the minibar cart, a room's minibar (room_minibar, which names its room_id; one per room), the laundry, the workshop. Defining a code the house already holds re-states the store in place, so the call is replay-safe; an identical re-statement changes nothing. is_default makes it the house's store of its kind that work draws from when it names none (a minibar task in a room with no minibar of its own, a laundry task, a fault's material line) — the previous default of the kind stops being one. allow_negative lets its stock go below zero (an uncounted store); it cannot be switched off while a level is negative. The store is linked to its house (stock_store_at_property); re-stating a store defined before that edge existed writes it. A store is kept by the role whose store it is (2026-10-17): an engineer's session keeps the workshop only, a housekeeping supervisor's any store but a workshop — held to the kind the code had and the kind it is given, so neither re-kinds, defaults or loosens the other's store (403); the desk and agents are held to none."
        },
        {
          "api_name": "hospitality.action.define_worker_group",
          "description": "Define a named group of staff at the target property that work can be assigned to — the third-floor runners, the public-areas team, the night maintenance crew (hubOS's workers groups). `department` says whose work it takes: a housekeeping task goes to a housekeeping, public_areas or other group, a work order to a maintenance or other one. `floors`, when given, are the floors it covers — runners by floor are a group of runners naming its floors, and work in a room or area on another floor is refused to it, as is work in a room or area that states no floor. Upsert by name: the same name again restates the group in place (an identical restatement writes nothing; absent `floors` clears them), and a retired group defined again comes back active. A restatement releases the group's open work it no longer covers — on a floor it no longer names, or of a kind its department no longer takes — back to the open board, unassigned, as `retire_worker_group` does. Two first definitions of one name queue on a per-house lock, so the second restates the first. Members are `set_worker_group_members`'. Emits `worker_group.defined`, naming the work it released."
        },
        {
          "api_name": "hospitality.action.detect_fraud_risk",
          "description": "Emit a typed fraud-risk assessment against a reservation (the dominant fire-target), payment, charge, person, loyalty redemption, or a folio with no booking behind it (a group master or house account taking a card hold; the folio must sit at the target property). Targets the property (container); the scoring subject is identified by subject_kind + subject_id params. Inserts a `fraud_risk_assessment` row in `suggested` state with an immutable `risk_context_snapshot` captured at issue time. Validators enforce: subject_kind/subject_id coherence, band/score coherence, recommended_action/band coherence, score and confidence bounds, reason-code contribution bounds, PCI no-PAN in snapshot. Decision lifecycle flows through apply / reject / escalate / expire."
        },
        {
          "api_name": "hospitality.action.detect_recurring_failure_pattern",
          "description": "Emit a typed service_recovery_pattern in suggested state. The action targets the property container; subject_kind and subject_id identify the clustered operational subject. A run_id may be supplied or the handler can open a manual run."
        },
        {
          "api_name": "hospitality.action.enrol_property_in_program",
          "description": "Link a loyalty_program to the target property via the M:N `loyalty_program_at_property` link with effective dates. A property can run multiple programs simultaneously (a soft-brand collection property running both the parent chain's program and the soft-brand's recognition layer; an SLH-Hilton partnership property running SLH INVITED + Hilton Honors). The temporal link tracks the participation window so programs can be left and rejoined without losing history."
        },
        {
          "api_name": "hospitality.action.expire_fraud_assessments",
          "description": "Sweep assessments on the target property whose `expires_at` is at or before `swept_at` (default now) and whose `decision_status` is still `suggested`, transitioning them to `expired`. Idempotent: re-run after an empty sweep is a no-op. Each transition emits a single event."
        },
        {
          "api_name": "hospitality.action.expire_message_triages",
          "description": "Sweep triages on the target property whose `expires_at` is at or before `swept_at` (default now) and whose `decision_status` is still `suggested`, transitioning them to `expired`. Idempotent: re-run after an empty sweep is a no-op. Each transition emits a single event."
        },
        {
          "api_name": "hospitality.action.expire_pattern_assessments",
          "description": "Sweep suggested service_recovery_pattern rows on the target property whose expires_at is in the past."
        },
        {
          "api_name": "hospitality.action.expire_rate_recommendations",
          "description": "Sweep recommendations on the target property whose `expires_at` is in the past (or whose `expires_on` business day the house has closed) and whose `decision_status` is still `suggested`, transitioning them to `expired`. Idempotent: a re-run after an empty sweep is a no-op. Each transition emits a single event so the audit trail captures the sweep."
        },
        {
          "api_name": "hospitality.action.expire_restriction_recommendations",
          "description": "Sweep restriction recommendations on the target property whose `expires_at` is in the past (or whose `expires_on` business day the house has closed) and whose `decision_status` is still `suggested`, transitioning them to `expired`. Idempotent: a re-run after an empty sweep is a no-op. The sibling of `expire_rate_recommendations`, one sweep per recommendation kind."
        },
        {
          "api_name": "hospitality.action.expire_sell_limit_recommendations",
          "description": "Sweep sell-limit recommendations on the target property whose `expires_at` is in the past (or whose `expires_on` business day the house has closed) and whose `decision_status` is still `suggested`, transitioning them to `expired`. Idempotent: a re-run after an empty sweep is a no-op. One sweep per recommendation kind, beside `expire_rate_recommendations` and `expire_restriction_recommendations`."
        },
        {
          "api_name": "hospitality.action.generate_housekeeping_tasks",
          "description": "Derive the day's housekeeping board from system truth: each occupied room due out on the business date gets a departure clean, other occupied rooms get stayover cleans (and turndown when include_turndown is set), vacant dirty / pickup rooms get vacant pickups, and rooms under an active out-of-order block covering the date are skipped (release decides their state). Rooms whose reservation is waiting in the room queue generate at urgent priority. Idempotent by construction: every derived task carries a generation_key, so re-running lands on the existing rows, and generated pending / blocked tasks the current state no longer calls for retire as skipped. Manually minted tasks are never touched. Each new task carries `credits` and `standard_minutes` from the property's most specific active housekeeping credit rule (task type and room type over task type alone; none when the house prices none), and a re-run re-prices generated tasks still pending to the rules as they stand — work under way keeps its price. Every active public area of the house gets one `public_area_clean` for the date (2026-10-14), keyed `hk:area:{area}:{date}` and converging the same way: a retired area's open generated clean is skipped, an active one's skipped clean revives. Run it from a cron, an agent, or the desk — twice is safe."
        },
        {
          "api_name": "hospitality.action.list_retail_product_at_property",
          "description": "List a retail product as available at the target property (pillow-side amenity, in-room minibar, lobby retail), with effective dates. Same `is_dtc_only` validator as the venue-level listing."
        },
        {
          "api_name": "hospitality.action.materialise_channel_mix_snapshot",
          "description": "Compute and persist a channel-mix snapshot for the target property on a given business date + cadence. The snapshot is immutable post-write; re-runs require explicit deletion of the prior row."
        },
        {
          "api_name": "hospitality.action.message_guest",
          "description": "Write to a guest the house has not heard from, or write first in a conversation they already have: by SMS to the profile's mobile number or by email to its address. With no `channel`, the kernel takes the first the guest can be reached on — a text when the profile holds a mobile number the guest has not stopped, else an email. The message lands in the guest's conversation at the house (opened when they have none) and is queued for the kernel's delivery adapter, which sends it after commit and records what happened. Refused when the profile holds no contact for the channel, when the guest asked for erasure or texted STOP in any conversation at the house, and — for a marketing message — when the profile holds no live marketing consent for the channel or the guest opted out of marketing there. A service message (the default) needs no marketing consent."
        },
        {
          "api_name": "hospitality.action.open_business_date",
          "description": "Open the property's accounting day: mints the BusinessDate row (status open) that charges, postings, and restrictions attach to. Exactly one business date may be open per property; the previous one must close through run_night_audit first. Idempotence: re-opening the same date is refused with a clear error rather than minting a duplicate day. Normally only needed once — run_night_audit rolls the next day open as it closes the last. The property carries the open day (open_business_date, business_date_opened_at): the hotel date every posting made from now is dated to, so a charge at 01:30 before the close lands on the day still open."
        },
        {
          "api_name": "hospitality.action.open_channel_connection",
          "description": "Register a conduit between a property and a distribution partner. Created `pending_mapping`: a conduit with no `channel_mapping` rows can publish nothing, and publishing or accepting an unmapped code is a named failure in both the SiteMinder and Mews certification suites — so the birth state says so rather than letting an empty conduit look healthy. `channel_ids` names the demand channels the conduit carries; a channel manager fronts many, and each keeps its own commission period. `secret_ref` names a credential in the external secret store — never pass the credential itself. `rate_model` and `price_basis` are fixed per conduit, not per property: SiteMinder configures the pricing model at integration level and will not let properties differ. Emits `channel_connection.opened`."
        },
        {
          "api_name": "hospitality.action.open_merchant_account",
          "description": "**Card-rail only.** Record the processor account a property's card payments will settle to, once the processor has created it. Opens in `onboarding`: the hotel still has to complete the processor's own form, and only the processor's verdict (`record_merchant_account_state`) makes it `active`. Refused while the property has an account that is not closed. Emits `merchant_account.opened.recorded`."
        },
        {
          "api_name": "hospitality.action.open_pattern_detection_run",
          "description": "Open a property-scoped recovery_pattern_detection_run for a scan window and signal set. The run remains in_progress until close_pattern_detection_run marks it completed or failed."
        },
        {
          "api_name": "hospitality.action.pay_commission_batch",
          "description": "Pay every commission row that is due at this property, optionally limited to one agent or to stays up to a date. Held rows are passed over, and rows awaiting a channel's own invoice are never collected — an agency-model OTA bills us, so paying those here would settle the same commission twice. An agent whose total falls below their agreed minimum is skipped rather than sent a cheque that costs more than it carries."
        },
        {
          "api_name": "hospitality.action.post_housekeeping_briefing",
          "description": "Post the head housekeeper's instructions for a day to the floor (hubOS's 'indications'). The author is the staff member behind the session; a caller that is not one (an agent, the desk without a staff record) names the author with `author_id` — a supervisor (housekeeping or front-office supervisor, manager on duty, general manager, owner) at the house. `business_date` defaults to the house's today. Replay-safe per property × date × author: the same text again writes nothing; different text is a second briefing, never an edit — readers list a day's briefings newest first by `posted_at`. Emits `housekeeping.briefing.posted`."
        },
        {
          "api_name": "hospitality.action.publish_federated_program",
          "description": "Create or activate a federated intelligence program. Cross-tenant programs require `default_min_tenants >= 3`, `default_min_properties >= 3`, and `default_max_contributor_share`; high-risk programs require a DPIA reference. Validators (commit 5): `program_cross_tenant_minimums`, `program_high_risk_requires_dpia_review`, `program_dpa_required_for_cross_tenant`, `program_none_threshold_only_internal_portfolio_only`."
        },
        {
          "api_name": "hospitality.action.publish_workflow_definition",
          "description": "Create a new `workflow_definition` (status `draft` or `active` per `publish_status`) and compute its canonical `definition_hash`. Once active the `step_definitions`, schemas, risk tiers, and policies are immutable — operators publish a new version to change them. Validators (commit 9): step-keys-unique, DAG-acyclic, registered-action/function-names, target-selector-required, mutating-steps-have-idempotency, retry-bounded, subworkflow ref/acyclic/depth, high-risk-gate-required, compensation-declared-for-side-effects."
        },
        {
          "api_name": "hospitality.action.recognise_person",
          "description": "Move a person up the four-rung local_status ladder (not_local → observed_local → known_regular → named_recognised). The status overwrites the existing `relationship_context.local_status`; the companion `local_status_computed_at` records the transition time. When `venue_id` is provided, a temporal `person_frequents_venue` link is created for the cross-property recognition handoff."
        },
        {
          "api_name": "hospitality.action.recommend_restriction",
          "description": "Emit a typed sell-restriction recommendation against the target property: a closure (`closed`), closed-to-arrival / departure, or a length-of-stay bound over `start_date..=end_date`, scoped to a room type, a rate plan, or the pair (at least one). At least one constraint must be set. The reading behind it — rooms remaining, the protection level, the demand and sell-up believed — and the forecast snapshot are frozen at issue as a `restriction_recommendation` in `suggested` state; a still- suggested prior against the same scope and range is superseded. Decision lifecycle flows through `apply_restriction_recommendation` / `reject_restriction_recommendation` / `expire_restriction_recommendations`. Joins an in-progress `rate_recommendation_run` when `run_id` is given and opens one otherwise; a run carries rate and restriction recommendations side by side. With `lift` it recommends the re-open instead: no constraint, and its apply lifts the rows standing in exactly this scope over the range."
        },
        {
          "api_name": "hospitality.action.recommend_sell_limit",
          "description": "Emit a typed overbooking recommendation against the target property: for one room type over `start_date..=end_date`, the oversell headroom it may sell past its sellable rooms (`oversell_limit`, at least 0), or `reset` — the explicit return to the operator's declared limit. Exactly one of the two: a recommendation states the limit, absence never means reset. The reading behind it — the rooms on the books, the share believed to arrive, the walk cost and the marginal rate the limit balances, the walks expected at it — and the forecast snapshot are frozen at issue as a `sell_limit_recommendation` in `suggested` state; a still-suggested prior for the same room type and range is superseded. Decision lifecycle flows through `apply_sell_limit_recommendation` / `reject_sell_limit_recommendation` / `expire_sell_limit_recommendations`. Joins an in-progress `rate_recommendation_run` when `run_id` is given and opens one otherwise."
        },
        {
          "api_name": "hospitality.action.record_brand_operator_cost",
          "description": "Post one mandatory brand/operator charge (USALI 12 Schedule 16) — only mandatory fees; optional services and pass-throughs are excluded by the schedule's definition. Fee computation stays in the agreement's system of record. `external_ref` is the idempotency anchor for invoice imports. Emits `brand_operator.cost.recorded` for the `daily_brand_operator` projection."
        },
        {
          "api_name": "hospitality.action.record_fraud_network_signal",
          "description": "**Card-rail only.** Freeze what Watchtower, the card rail's federated fraud network (proposal 0029), answered for a risk read at this house: whether a confirmed outcome carries one of the queried identifier kinds, which kinds matched, and the bucketed count, contributor breadth and recency — never a house, never a number. `band_floor` is the lowest band the hold's assessment may carry and `floor_basis` why; the two must agree (a card blocks only on `card_own_house` or `card_corroborated`, any other match is `high`, no match is `low`). `desk_override` names in `override_of` the contested `block` answer for the same subject that it takes to review instead. `fingerprints` are the digests asked about, one per kind queried, kept (`internal.restricted`) so the platform's review of a contest can find the contributions behind the answer. Refused without the house's `fraud_network` consent (a house that does not contribute does not read), and refused when the buckets contradict `match_present`. Expires 24 hours after issue. Emits `fraud_network.signal_recorded`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_guest_feedback",
          "description": "Record what a guest told the house — a complaint, a compliment or a suggestion — as an `open` case at the target property. From a triaged message (`from_triage_id`, or the message itself as `message_id`): the feedback links the message, takes its guest, channel and time, and a `complaint.*` intent gives the kind and category (`complaint.noise` → complaint / noise). One feedback per message: recording the same message again with the same kind, category and text is a no-op, and with different ones is refused. From a stay (`stay_id`): the stay's guest is linked unless `person_id` names another. A complaint needs a category. `text` is the guest's words (or the desk's summary of them) and is marked as the guest's; the message's own excerpt is never copied. `delegate_to` hands the case to a role at once. Emits `guest_feedback.recorded`."
        },
        {
          "api_name": "hospitality.action.record_guest_message",
          "description": "Record an inbound or outbound `guest_message` into Aerandil. Opens a `guest_conversation` lazily for the person at the property when none exists; appends the message; refreshes the conversation's denormalised counters. PCI-DSS hard validator gates `redacted_excerpt` against Luhn-matching sequences. WhatsApp 24-hour service-window validator gates outbound free-form messages without a `template_id`. Rejected if the conversation is in a closed state or if GDPR erasure has been requested in-thread. A reply the kernel composes — outbound into a named `conversation_id` with no `channel` — goes back on the channel and thread of the guest's most recent inbound message, to the conversation's person, and is refused when the guest opted out of that channel or asked for erasure; an outbound message given no `content_hash` is hashed from its excerpt (the kernel wrote it, so the excerpt is the whole message)."
        },
        {
          "api_name": "hospitality.action.record_labour_cost",
          "description": "Post one labour-cost amount from the payroll system of record — Aerandil never computes pay. Multi-day pay-period lines post to `business_date` un-prorated in v1; `period_start`/`period_end` preserve the source window. `external_ref` is the idempotency anchor for imports. Emits `labour.cost.recorded` for the `daily_labour` projection."
        },
        {
          "api_name": "hospitality.action.record_lost_found_item",
          "description": "Record something found at the target property: what it is (never whose), where — `room_id` for a room of this property, or `found_location` for anywhere else — and when (`found_at`, defaulting to now). The staff member behind the principal (the handset's attendant or runner) is linked as the finder. The item is `found`, or `stored` at once when `storage_location` is given (the desk putting a handed-in phone straight in the safe). Its tag is the `id`: `LF-` and the last eight hex digits of the item's object id, upper-case. A caller may mint that object id itself (`item_id`, a v4 or v7 UUID) — a handset recording offline then knows the item, and the tag for the bag, before the record lands, and can hang the photo off it once it has; the same find under the same `item_id` again is a no-op, a different one is refused as a duplicate. Emits `lost_found_item.recorded`."
        },
        {
          "api_name": "hospitality.action.record_recognition_event",
          "description": "Log a recognition event against the target property. Always-on across all loyalty archetypes — the program_type does not gate recognition. The program reference is optional because recognition-only patterns without a formal program record events without a program ref. The pair with record_service_recovery lets the operator's recognition-vs- recovery balance surface as a structural read about the property's hospitality character."
        },
        {
          "api_name": "hospitality.action.record_utility_cost",
          "description": "Post one utility invoice line (USALI 12 Schedule 9 — Energy, Water, and Waste), with the metered consumption when the invoice carries it. `category` is stamped from `kind`; the quantity/unit pair stays in its source unit. `external_ref` is the idempotency anchor for invoice imports. Emits `utility.cost.recorded` for the `daily_eww` projection."
        },
        {
          "api_name": "hospitality.action.register_agent",
          "description": "Create or supersede an `agent_registry_entry` version. Registry entries declare the maximum scopes an agent claims to need; only `review_status = approved` entries are installable (modulo the `registry_source = manual` operator override). Validators (commit 11): `agent_registry_key_version_unique`, `mcp_remote_requires_https`."
        },
        {
          "api_name": "hospitality.action.register_asset",
          "description": "Register a piece of equipment at the house under its code — the house's tag for it, unique within the house. Registering a code the house already holds re-states that asset in place (details replaced, location re-pointed), so the call is replay-safe and is also how an asset is corrected or moved; an identical re-statement changes nothing. A retired code refuses. The asset stands in a room (room_id) or at a venue (venue_id) — one or the other, both of this house — or nowhere in particular. New assets start active."
        },
        {
          "api_name": "hospitality.action.register_public_area",
          "description": "Register a place at the target property that is not a guest room — a lobby, a corridor, a lift, the pool, the staff canteen — under the house's `code` for it, with its `name`, its `kind` (what a checklist template names) and the `floor` it is on. Upsert by code: the same code again restates the area in place (an identical restatement writes nothing; an absent `floor` clears it), and a retired area registered again comes back active. Two first registrations of one code queue on a per-house lock, so the second restates the first. An active area gets one `public_area_clean` per business date from `generate_housekeeping_tasks`. The reader code is `set_public_area_reader_code`'s. Emits `public_area.registered`."
        },
        {
          "api_name": "hospitality.action.run_night_audit",
          "description": "Close the property's open business date — a real audit, not a bookkeeping stamp, and never a blocking batch: the whole run is one transaction (a failure rolls everything back and the day stays open; a re-run replays idempotently), and nothing else stops while it runs. In order: (1) sweeps the day's unresolved arrivals to no_show through the mark_reservation_no_show path, releasing their forward room-nights — and on partially-arrived multi-room reservations cancels the individual legs that never showed, releasing theirs (disable with sweep_no_shows = false); (1a) assesses the no-show fee from the reservation's frozen cancellation policy and posts it as a rooms-bucket no_show_fee charge, recording the amount and an `assessed` status on the reservation — OPERA's Post-Revenue-To-No-Shows, and like OPERA's it is **opt-in**: enable with post_no_show_fees = true. The fee carries the tax the house's rules for a `no_show_fee` produce, and none where it authored none (the UK taxes it, Austria does not); a booking's tax exemption reaches it. Assessment only; collecting the fee off-session against a stored card is a separate step, because the audit runs inside the ontology and the card rail does not; (1c) releases group-block rooms nobody picked up, per each definite block's cutoff mode — after the no-show sweep, so a room a no-show hands back to its block is cut off the same night rather than sitting held for a day (disable with release_block_cutoffs = false); (1d) accrues commission on stays that departed today — one row per payee, with the contract that produced it frozen on the row (disable with accrue_commission = false); (1e) expires waitlist entries two business days past their requested departure — OPERA's End-of-Day auto-delete, as a terminal status rather than a deletion, so the turnaway stays countable (disable with expire_waitlist = false); (1g) strips the booking engine's visitor key from the offers, turnaways and reservations whose arrival night is the property's visitor_key_grace_days past (30 unset), keeping the records — the key only joins records of one night (disable with strip_visitor_keys = false); (1h) expires payment requests still pending whose expires_at falls on or before the audited date — the pay link lapses at the close (always on; absent from the summary when none expired); (2) posts the night's room charge to every in-house stay's window-1 folio from its reservation_night rate — replay-safe via the charge external_ref business key, complimentary nights skipped, tax_application lines derived from the property's active rooms tax_rule rows (inclusive rules carve out of the rate, exclusive rules join the folio debit; no rules = zero tax) — disable with post_room_and_tax = false; (2c) balances package allowances: components due on the closing date with no consumption row settle as profit — an append-only consumption row plus the package_discount_adjustment charge, replay-safe (disable with balance_packages = false); (3) counts exceptions instead of halting on them — due-outs still in house, open cashier sessions, unresolvable folios, malformed tax or package config — the OPERA halt conditions turned into a queue (the date roll never waits on a drawer); (4) mints an immutable NightAuditRun carrying posted_room_revenue, posted_room_tax, package_adjustments_posted and exception_count (re-runs create new rows, never edit); (5) closes the BusinessDate and rolls the next day open (disable with open_next = false), moving the property's open_business_date to it — or clearing it when nothing is opened — so postings from the close on are dated to the new hotel date."
        },
        {
          "api_name": "hospitality.action.set_board_sort_pattern",
          "description": "Set the order of one of the property's boards — hubOS's server-defined sort pattern: the attendant's housekeeping_sheet, the supervisor's housekeeping_rack, the engineer's work_orders board, or the pacing report. `keys` is ordered, first key first, each a key the board's rows carry (priority, room_number, floor, task_type, status, credits, arrival_time, departure_time on the housekeeping boards; priority — the order's severity — room_number, floor, status, due_at on work_orders; name, credits, pending_credits, target_percent on pacing) with a direction, and for a ranked key (priority, task_type, status) optionally the house's order of its values, least first (departures before stayovers). One pattern per board per house: setting a board again replaces its pattern in place and revives it if it was retired; the same order again writes nothing. A staff member's own preference (set_staff_sort_preference) wins over it; `board_sort` reads the order a caller sees. Emits `board_sort_pattern.set.recorded`."
        },
        {
          "api_name": "hospitality.action.set_cancellation_policy",
          "description": "Author the property's cancellation terms — the rule booking resolves into a reservation's frozen cancellation schedule, and the night audit's no-show sweep charges from. Upsert by `code`: a repeated code rewrites that policy in place. Unlike the deposit engine there is no dated succession, because a reservation freezes the resolved money at booking and an edit here can no longer reach a booked stay — which is also Apaleo's contract (\"changing a cancellation policy will not affect existing reservations\"). `tiers` is the ladder, sorted descending by `trigger_offset_hours_before_arrival`; exactly one of `penalty_amount` / `penalty_rate` / `penalty_nights` is set per tier, matching its `penalty_kind`. A non-refundable rate ships `non_refundable = true` with empty tiers. Attach to a rate plan via `rate_plan_requires_cancellation_policy` to outrank the property default."
        },
        {
          "api_name": "hospitality.action.set_credit_limit_overage",
          "description": "Set what the night audit does once an in-house stay's bill has reached what its pending card holds hold — OPERA's credit limit, the authorised total. `off` (the default): nothing beyond the house's top-up rule (`set_hold_top_up`). `hold`: the excess is held as a second hold, as a top-up, even where the top-up rule is off or the excess below its threshold (never past its `max_per_stay`). `charge` — OPERA's CREDIT_LIMIT_OVERAGE_PAYMENTS, which \"charges the total balance of a folio when the balance is equal or higher than the credit limit\": the stay's holds are taken for what the bill owes, and one fresh hold placed for what is left of it plus the rule's allowance, instead of stacking another hold on the guest's card. A refusal (the card, the consent, the processor) is recorded on the stay for the desk."
        },
        {
          "api_name": "hospitality.action.set_deposit_policy",
          "description": "Author one deposit policy at the property — the rule reservation creation derives `deposit_required_amount` / `deposit_due_at` from, and the night audit's deposit sweep enforces. Same succession contract as `set_tax_rule`: policies sharing a `code` form a non-overlapping timeline — a later effective_from end-dates the open-ended predecessor, a repeated (code, effective_from) replaces in place, a call dated before an existing later policy refuses. `basis` decides the numeric shape: the percent bases require `rate` (in [0, 1]) and forbid the others; `flat` requires `flat_amount`; `nights` requires `nights`. `due_basis` + `due_days` place the due instant (days after booking, or days before arrival); `auto_cancel_on_lapse` decides whether End of Day cancels an unpaid lapsed booking or just records the lapse. Attach to a rate plan via `rate_plan_requires_deposit_policy` to outrank the property default."
        },
        {
          "api_name": "hospitality.action.set_fnb_open_checks_policy",
          "description": "Set what the night audit does with the house's outlets' guest checks still open on the business date it closes (any check whose `business_date` is that day or earlier). `carry` — the default when unset: the day closes, the checks stay open on their own business date (a check may span dates, as Simphony's do; its lines priced and its revenue reported on the day it opened, its settlement stamped when it happens), the audit emits `fnb.checks_carried` listing them, and the till reads them as `carried`. `refuse`: the audit refuses, naming each outlet and check number, until they are settled — Simphony's opt-in privilege to prevent Start of Day with open checks."
        },
        {
          "api_name": "hospitality.action.set_guest_replies",
          "description": "Switch the property's guest replies on or off — the six `event_named` automations that answer a guest who is waiting: their room is ready (`room_queue.room_ready`), the housekeeping they asked for is done (`housekeeping.task.completed`), the fault they reported is fixed (`work_order.completed`). Three fire `record_guest_message` into the guest's own conversation — the asking message's, or, for a request the desk keyed for their stay, the one they have open at the house — on the channel they last wrote on, and only when there is one. Three write first, through `message_guest`, to a guest who has no conversation open at the house — whose request the desk keyed, or who is queued for a room: by text, else email, to the contact their profile holds; an event answers through one of the two kinds, never both. The first call installs them at the property (in draft when switched off), and a later call installs any the house lacks; `enabled` activates them, `false` pauses them. Off by default: a house that never calls this has none, and installed definitions are ordinary automations the tenant can read, tune while paused (`update_automation`) and archive. Delivery is the messaging adapter's: the kernel records the outbound message, and its creation is what a channel adapter subscribes to. Emits `guest_replies.set.recorded`."
        },
        {
          "api_name": "hospitality.action.set_hold_top_up",
          "description": "Set whether the night audit tops up an in-house stay's card hold as its bill grows. On, after each audit the card rail works out, for every in-house stay with a pending hold, OPERA's top-up — the bill's balance, less what the stay's pending holds hold, plus the allowance of the incidental-hold rule the stay's booking matches (for the nights still to come) — and when that excess is more than `threshold` places it as a second hold (`hold_purpose = top_up`) on the card the stay's hold was taken on, off-session (the card must carry the guest's consent to merchant-initiated charges), or raises the hold itself where the card and the account allow incremental authorisation. At most `max_per_stay` top-ups a stay (1 to 10, default 3): each is a hold on the guest's card until check-out. A refusal is recorded on the stay for the desk. Off (`enabled: false`, the default) the audit tops up nothing; `set_credit_limit_overage` still acts once a bill reaches what is held. With `interval_minutes` (60 to 1440) the card rail also plans the house's in-house stays that often between audits — OPERA's scheduler, \"every 60 minutes or more\" — skipping a stay whose last top-up or refusal is inside the interval, and one whose refusal still stands (the card is not asked again before the audit or the desk); 0 or unset (the default), only the audit and the desk's Top up now."
        },
        {
          "api_name": "hospitality.action.set_housekeeping_credit_rule",
          "description": "Author what one kind of housekeeping task is worth at the property (OPERA's housekeeping credits, the unit of hubOS's pacing report): `credits` — the workload unit attendants' sheets are balanced in and shift targets are counted in — and optionally `standard_minutes`, what the work should take. Keyed by (`task_type`, `room_type_id`): a call repeating a key at the property replaces that rule in place and revives it if it was retired (the authoring event keeps the history), and an identical restatement writes nothing; omitting `room_type_id` authors the rule for every room type without one of its own. From then on `create_housekeeping_task` and `generate_housekeeping_tasks` stamp the most specific active rule's figures onto each new task; tasks already minted keep theirs, except a generated task still pending, which the next generation run re-prices. Emits `housekeeping_credit_rule.set.recorded`."
        },
        {
          "api_name": "hospitality.action.set_incidental_hold_policy",
          "description": "Author one incidental-hold rule at the property — the card hold `suggest_incidental_hold` derives for a check-in, so the desk pre-fills it instead of typing it. Keyed by `code`: a call repeating an existing code at the property replaces that rule in place (the authoring event keeps the history); a new code mints a rule. `basis` decides how `allowance` is used — `per_night` (allowance × nights), `room_and_per_night` (room total + allowance × nights), `room_plus_flat` (room total + allowance), `flat` (allowance) — OPERA's authorization rules 7, 2, 9 and 5 — and `room` (room total), `room_and_per_person_night` (room total + allowance × persons × nights), `per_person_night` (allowance × persons × nights) and `room_plus_percent` (room total + `percent` of it) — rules 1, 3, 8 and 4. The room total is OPERA's nights × Daily Rate: the booked rate plus the house's room taxes charged on top of it. `guarantee` (OPERA's reservation type), `room_type_ref`, `rate_plan_ref` and `source` narrow the rule; a reservation is served by the active rule every one of whose narrowings it matches, the most specific first in OPERA's precedence — room type, then rate plan, then guarantee, then source — and a rule narrowed by none is the house default. `active: false` retires a rule without deleting it."
        },
        {
          "api_name": "hospitality.action.set_no_show_posting_rule",
          "description": "Author the property's no-show posting rule for one reservation guarantee — what the night audit posts when it marks such a booking a no-show (OPERA's Post Revenue To No Shows, opt-in): `all_nights` posts every booked night's rate, `first_night` the first night of every room, `deposit_only` forfeits the deposit the house holds (nothing when it holds none), `none` posts nothing. Postings land on the booking's own account through `post_charge` / `forfeit_deposit`, attributed to `outlet_id` (required unless the basis is `none`). Keyed by `guarantee`: a call repeating a guarantee at the property replaces that rule in place (the authoring event keeps the history); omitting it authors the house default for every guarantee without a rule of its own. `active: false` retires a rule without deleting it."
        },
        {
          "api_name": "hospitality.action.set_payment_request_messaging",
          "description": "Turn pay links to the guest on or off at a house. On, every payment request's pay link — the desk's request or the booking automation's — is sent to the booking's guest as soon as the card rail has minted it: by email, or by SMS when the profile holds no email, refused (and noted on the request) when the guest asked for erasure or texted STOP. Off, the desk sends the link itself. A request whose link was minted while it was off is not sent later. `reuse_terms` sets the words the pay page shows the guest about keeping their card for later charges (an empty string puts the card rail's default back; omitted, they are unchanged)."
        },
        {
          "api_name": "hospitality.action.set_post_stay_charging",
          "description": "Set how many calendar days after departure a charge found later — the minibar, damage, laundry — may still be posted to the departed stay's bill and taken from its card (`days`, 0 to 14; 0 turns it off), counting the departure's own date in the house's time zone as the first. On, check-out keeps the stay's pending card hold that lasts longest for the window — until the window's end or the hold's own deadline, whichever comes first — and releases the others, and the window closes at the house's midnight after its last day: 1 keeps the bill chargeable until the end of the departure's date, as OPERA's Post Stay Charging does. Off (the default), check-out releases every hold the stay still has. Applies to departures from now on; a window already open keeps its end."
        },
        {
          "api_name": "hospitality.action.set_request_urgency_window",
          "description": "Set the house's own \"soon\": how many minutes before its due time a request reads as `soon` on the housekeeping_requests read and the handset's card (`property.request_soon_minutes`, 1 to 720; unset, the kernel's 30). The same value again is a no-op."
        },
        {
          "api_name": "hospitality.action.set_sca_chase",
          "description": "Set the house's rule for a guest whose bank asks them to confirm a charge or a hold the card rail tried without them (Stripe's `authentication_required`): `enabled` sends the guest a page to confirm it on, `hours` is how long they have (1 to 168, default 24) and `reminders` how many reminders go before the deadline, spread evenly across it (0 to 3, default 1 — one at half-time). Past the deadline the house is told (`payment.authentication_overdue`). Off (the default), the pending state and its deadline are still recorded, and the desk sends the page itself. Applies to authentications asked for from now on; one already pending keeps its deadline."
        },
        {
          "api_name": "hospitality.action.set_tax_rule",
          "description": "Author one tax rule at the property — the config the night audit derives `tax_application` lines from. Rules sharing a `code` form a non-overlapping succession timeline: a call whose effective_from lands after the current open-ended rule end-dates that predecessor at effective_from − 1 day and mints the successor (the Mews validity-interval model — a rate change is never an in-place edit of history); a call repeating an existing rule's (code, effective_from) replaces that row in place (converging replay / mis-key correction); a call dated before an existing later rule for the same code refuses — successions are authored forward. `basis` decides the shape: `percent` requires `rate` (in [0, 1]) and forbids `flat_amount`; `flat_per_night` and `per_person_per_night` require `flat_amount` and forbid `rate`. `inclusive` decides whether the tax lives inside the posted amount (VAT carve-out) or on top of it (US-style add-on). The rule targets exactly one of `bucket`, `system_charge_kind` or `tax_class` (the VAT class an F&B item class names — a percent rule; the till rings every rule in force for a line's class): a forfeited deposit, cancellation fee or no-show charge is taxed only by a percent, inclusive rule targeting its kind, so a house in a jurisdiction that taxes retained payments (the UK) authors them and one that does not (Austria) authors none. The succession timeline is per (code, target), so one `VAT` code can cover rooms and the retained payments alike. `reduced_after_nights` + `reduced_value_share` on a percent rooms rule is the UK 28-day rule. The emitted authoring event is the permanent who-changed-what-tax-when trail."
        },
        {
          "api_name": "hospitality.action.set_waitlist_offer_messaging",
          "description": "Turn the waitlist's guest half on or off at a house. On, every waitlist offer (the desk's or the matcher's) is texted to the guest, or emailed when the profile holds no mobile number, with a link to claim it; off, the desk tells the guest itself. The claim link is `claim_url_base` with the offer's token appended (the kernel's own page is `<api>/v1/waitlist-claims/`); without one the offer asks the guest to reply or call. The base is an https URL with no query or fragment (plain http only to a loopback dev host); a missing trailing slash is added."
        },
        {
          "api_name": "hospitality.action.submit_owner_statement",
          "description": "Submit the target property's monthly owner statement — the USALI summary figures, the fee computation and the variance snapshot — for owner approval. Requires an active `management_agreement` covering the property (the statement links to it); a changes_requested statement is answered by resubmitting with `revision` + 1, never by editing. `external_ref` is the idempotency anchor."
        },
        {
          "api_name": "hospitality.action.submit_property_budget",
          "description": "Submit the target property's annual operating budget for owner approval (contract band: Oct 15 – Dec 1 submissions). Creates the `property_budget` in `submitted`; the owner's approval gate transitions it (approved / objected). `external_ref` is the idempotency anchor."
        },
        {
          "api_name": "hospitality.action.update_service_philosophy",
          "description": "Upsert the property's service philosophy. Creates the philosophy row on first call, modifies on subsequent calls. Every axis is required so the philosophy is always fully specified — partial updates would let operators drift into half-set axes without intending to. The 1:1 `property_has_philosophy` link is established on first call and not re-linked on subsequent updates."
        }
      ],
      "api_name": "hospitality.property",
      "description": "A single operated site — a hotel or resort, or a non-lodging business run on the same substrate (a standalone day spa). The property is the canonical home of most operational data and the anchor for local timezone, currency, and brand standards.",
      "display_name": "Property",
      "domain": "organization",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Property code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Property name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "legal_name",
          "description": "",
          "display_name": "Legal name",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Property kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "positioning",
          "description": "",
          "display_name": "Market positioning",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "timezone",
          "description": "",
          "display_name": "IANA timezone",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "default_currency",
          "description": "",
          "display_name": "Default currency (ISO 4217)",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "address",
          "description": "",
          "display_name": "Address",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "location",
          "description": "",
          "display_name": "Geo location",
          "kind": "geo_point",
          "required": false
        },
        {
          "api_name": "room_count",
          "description": "",
          "display_name": "Total rooms",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "opened_on",
          "description": "",
          "display_name": "Opened on",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "standard_week_hours",
          "description": "",
          "display_name": "Standard work week hours",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "star_rating",
          "description": "",
          "display_name": "Star rating",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "open_business_date",
          "description": "The business date this property has open — the hotel date a posting made now is dated to. Written by open_business_date and the night audit's close; unset while no day is open.",
          "display_name": "Open business date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "business_date_opened_at",
          "description": "When the open business date was opened. A posting stamped before this instant belongs to an earlier day and is dated by the property-local calendar instead.",
          "display_name": "Open business date opened at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "visitor_key_grace_days",
          "description": "How many days past a stay's arrival night the night audit keeps the booking engine's visitor key on its offers, turnaways and reservation before stripping it; the records themselves stay. Unset: 30. At most 365.",
          "display_name": "Days past the arrival night a visitor key is kept",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "waitlist_offer_hold_minutes",
          "description": "How many minutes a waitlist offer — the desk's or the matcher's — keeps its room-nights before they are given back and offered to the next entry. Unset: 30. Between 1 and 1440 (OPERA's room-hold range).",
          "display_name": "Minutes a waitlist offer keeps its room-nights",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "request_soon_minutes",
          "description": "How many minutes before its due time a housekeeping request reads as `soon` on the housekeeping_requests read (and on the handset's card). Unset: 30. Between 1 and 720. Set through set_request_urgency_window.",
          "display_name": "Minutes ahead within which a request reads as soon",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "waitlist_offer_messaging",
          "description": "When true, every waitlist offer — the desk's or the matcher's — is sent to the guest by SMS (or email when the profile holds no phone), with a link to claim it. Unset: off.",
          "display_name": "Send waitlist offers to the guest",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "waitlist_claim_url_base",
          "description": "The start of the link a waitlist offer carries; the offer's claim token is appended as a path segment. Point it at the kernel's own claim page (`https://<kernel>/v1/waitlist-claims/`) or at a booking site that calls the claim route. An https URL ending in `/`, with no query or fragment (plain http only to a loopback dev host). Unset: the offer goes out without a link and asks the guest to call.",
          "display_name": "Where a waitlist claim link points",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "payment_request_messaging",
          "description": "When true, a payment request's pay link is sent to the booking's guest — by email, or by SMS when the profile holds no email — as soon as the card rail has minted it. Unset: off, and the desk sends the link itself.",
          "display_name": "Send pay links to the guest",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "payment_request_reuse_terms",
          "description": "What the pay page tells the guest about keeping their card (Stripe's submit-button text, at most 1,200 characters): that the house may charge it without them present, for what, when and how often, and how each amount is set — the terms Stripe requires for a merchant-initiated charge. Set with `set_payment_request_messaging`. Unset: the card rail's default wording.",
          "display_name": "Pay page card terms",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "billing_identity",
          "description": "Who this property is, legally, on a document it issues. Absent until an operator fills it in, and `issue_invoice` refuses without it — a tax invoice with no supplier registration number is not one. Snapshotted onto every invoice at issue, so editing this never rewrites a document already handed over.",
          "display_name": "Billing identity",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "post_stay_charging_days",
          "description": "How many business days after departure a charge found later (the minibar, damage, laundry) may still be posted to the departed stay's bill and taken from its card. On (1–14), check-out keeps the stay's largest card hold and releases the rest, and the window closes at the house's midnight after its last day — 1 is OPERA's \"until the end of the business date\". Off (0 or unset), check-out releases every hold. Set with `set_post_stay_charging`.",
          "display_name": "Post-stay charging window, in days (0 = off)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "hold_top_up",
          "description": "",
          "display_name": "Whether the night audit tops up an in-house stay's card hold as its bill grows (set_hold_top_up; off unset)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "hold_top_up_threshold",
          "description": "",
          "display_name": "The smallest excess worth a top-up — the bill less what is held plus the rule's allowance",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hold_top_up_max_per_stay",
          "description": "",
          "display_name": "The most top-ups the card rail places for one stay",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "hold_top_up_interval_minutes",
          "description": "",
          "display_name": "How often, in minutes, the card rail plans the house's in-house stays' top-ups between night audits (60 to 1440; 0 or unset: at the audit and on the desk's press only)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "credit_limit_overage",
          "description": "",
          "display_name": "What the night audit does once a stay's bill reaches what its holds are for: off, hold the excess, or charge (set_credit_limit_overage; off unset)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "fnb_open_checks_at_roll",
          "description": "",
          "display_name": "What the night audit does with the outlets' checks still open on the day it closes: carry them on their own business date, or refuse the close (set_fnb_open_checks_policy; carry unset)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "sca_chase",
          "description": "When true, a charge or hold the card rail tried with the guest absent that the issuer will only complete with the cardholder sends the guest a page to confirm it on (email first, else SMS, under the service-message consent gate), reminds them before the deadline, and tells the house when the deadline passes. Unset: off — the state and its deadline are still recorded and the desk sends the page itself. Set with `set_sca_chase`.",
          "display_name": "Chase a guest whose bank asks them to confirm",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "sca_chase_hours",
          "description": "The house's deadline for a pending authentication, in hours from the moment the issuer asked (1–168). Unset: 24.",
          "display_name": "Hours the guest has to confirm",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "sca_chase_reminders",
          "description": "How many reminders go to the guest before the deadline, spread evenly across it (0–3; 1 is one at half-time). Unset: 1.",
          "display_name": "Reminders before the deadline",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.property_budget",
      "description": "One fiscal year's operating budget for one property, submitted by the operator in the HMA's window (contract band: Oct 15 – Dec 1) and approved or objected to by the owner through an approval gate (implements `approvable`; `objected` is the PHR HMA's own vocabulary). Load-bearing beyond narrative: the owner statement's variance columns and the performance test's NOI prong are computed against it.",
      "display_name": "Property budget",
      "domain": "management",
      "interfaces": [
        "hospitality.interface.approvable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Budget id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "fiscal_year",
          "description": "",
          "display_name": "Fiscal year",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "budgeted_revenue",
          "description": "",
          "display_name": "Budgeted total operating revenue",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "budgeted_gop",
          "description": "",
          "display_name": "Budgeted gross operating profit",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "budgeted_noi",
          "description": "",
          "display_name": "Budgeted net operating income (the performance test's denominator)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "submitted_at",
          "description": "",
          "display_name": "When the operator submitted",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the owner's gate decided",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "External ref",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.confirm_proposed_action",
          "description": "Records a confirmed `proposed_action` (proposal 0030 §5.2). Internal: invoked only by the dedicated confirm endpoint, which first enforces confirmer_scope and fires the chained `target_action` through the scope-checked engine (Return-of-Control — the kernel, not the agent, executes the write). This step then transitions the proposal to `applied`, recording `applied_params` (the frozen snapshot or the confirmer's edit — the edit-rate trust signal, §7), `applied_action_correlation_id`, and `applied_target_ref`. Rejected if the proposal is not `proposed` or has expired. Emits `proposed_action.applied`."
        },
        {
          "api_name": "hospitality.action.reject_proposed_action",
          "description": "The confirmer declines a `proposed_action`, transitioning it to `rejected`. Emits `proposed_action.rejected`."
        }
      ],
      "api_name": "hospitality.proposed_action",
      "description": "A frozen, auditable record of an action an agent intends to fire, surfaced for human/guest confirmation. Carries the target action name, the frozen proposed parameters, the rationale, and the context the agent saw. On `confirm_proposed_action` the kernel — not the agent — fires the chained typed action through the normal scope-checked, idempotency-keyed action surface; the agent never touches state directly (single-write-surface honesty, proposal 0030 §1). The `decision_*` / `applied_*` fields mutate exactly once on the transition out of `proposed`. Supersession is on the `proposed_action_superseded_by` self-link.",
      "display_name": "Proposed action",
      "domain": "proposed_action",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Proposal id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "proposed_by",
          "description": "",
          "display_name": "Principal::stable_id() of the proposing agent — the audit anchor",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "target_action",
          "description": "",
          "display_name": "The registered action this proposal intends to fire (ApiName form); validated against the registry",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "target_params_snapshot",
          "description": "",
          "display_name": "Frozen proposed parameters for `target_action` — the audit-grade record of what the agent wanted to do",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "subject_kind",
          "description": "",
          "display_name": "The object class the action targets",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "subject_ref",
          "description": "",
          "display_name": "Id of the subject object (resolved against `subject_kind`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "Human-readable 'why', surfaced at confirmation time",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "context_snapshot",
          "description": "",
          "display_name": "Frozen context the agent saw (the guest request, the disruption); bounded excerpts only — full content stays in the system of record",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "confidence",
          "description": "",
          "display_name": "Agent confidence in the proposal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "requires_confirmation_from",
          "description": "",
          "display_name": "Who must confirm before the kernel fires the action",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "When an unconfirmed proposal sweeps to `expired` (default 24h)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decision_status",
          "description": "",
          "display_name": "Frozen-artefact lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "Set on exit from `proposed`",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "decided_by",
          "description": "",
          "display_name": "Principal::stable_id() of the confirming principal (guest surface, staff member, approval gate, or the expiry sweep)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "applied_params",
          "description": "",
          "display_name": "The params actually fired on `applied` — may differ from `target_params_snapshot` when the confirmer edited them (the edit-rate trust signal, §7)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "applied_action_correlation_id",
          "description": "",
          "display_name": "The chained typed action's correlation id — the audit-trail join",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "applied_target_ref",
          "description": "",
          "display_name": "Id of the object the chained action created/modified",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Set on `rejected`",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.create_public_area_task",
          "description": "Mint an ad-hoc housekeeping task for a public area — the clean a scan of its code opens on the handset, an extra round of the lobby toilets, or the supervisor's inspection of the area — through `create_housekeeping_task`'s own path, linked `housekeeping_task_for_area` instead of to a room. `task_type` is `public_area_clean` (the default) or `public_area_inspection`. Carries no generation_key, so the generator never touches it; `credits` and `standard_minutes` come from the house's credit rule for the task type unless given; `assignee_id` assigns it in the same write (a housekeeper or runner session names only itself). Refuses a retired area. The area's checklist resolves by its kind (`checklist_for_task`), and an inspection's verdict at completion rates the area's latest completed clean that day, as a room's does. Stamps `origin` manual and the task's own `area_id`; emits `housekeeping.task.created` with `area_id`. `requested_by`, `stay_id` and `due_at` (2026-10-15) are create_housekeeping_task's — a floor session states only floor, and a guest's request is the desk's or triage's."
        },
        {
          "api_name": "hospitality.action.create_public_area_work_order",
          "description": "Raise a maintenance work order against a public area — the lobby's blown light, the lift that sticks, the pool's broken tile — through `create_work_order`'s own path, linked `work_order_for_area` instead of to a room (an order is for one place). Takes create_work_order's words: `title`, `category`, `severity` (each filled from `fault_type_id` when not stated; the category from the `element` when neither names one), `description`, `staff_member_id`, `asset_id`, `stay_id`, `from_triage_id`. An area is not inventory, so there is no out-of-order window. Refuses a retired area. An order already open at the area under the same title or fault type is that order: nothing is minted, the response carries a `work-order-open-twin` warning naming it and `work_order.reported_again` records the report — unless `allow_duplicate`; a `staff_member_id` the call names takes the twin when nobody holds it (2026-10-17), and a holder it already has keeps it. Postpone, resume, the thread, assignment and completion serve it unchanged. Emits `work_order.created` with `area_id`."
        },
        {
          "api_name": "hospitality.action.retire_public_area",
          "description": "Retire a public area — closed for the season, walled off in a refit. It gets no more daily cleans, and its reader code is cleared, so a scan opens nothing and the code is free for another place. Its open generated tasks (pending or blocked) are skipped with the reason recorded, as the generator retires work the state no longer calls for; work under way, finished work and hand-minted tasks are left alone. Retiring a retired area is a no-op; `register_public_area` with its code revives it. Emits `public_area.retired`."
        },
        {
          "api_name": "hospitality.action.set_public_area_reader_code",
          "description": "Set the code the area's QR / NFC reader carries — what the handset scans to open the area's card and its checklist (hubOS's common-areas QR) — or clear it when `reader_code` is absent or blank. Unique within the property across rooms and areas alike, one scan, one place: a code a room or another area of the house already carries is refused, and two first writes of one code — to an area or a room — queue on the house's reader-code lock, so the second reads the first. A retired area takes no code. Setting the code the area already carries is a no-op. The object search finds the area by it (an `eq` filter on `reader_code`, narrowed to the house). Emits `public_area.reader_code_set`."
        }
      ],
      "api_name": "hospitality.public_area",
      "description": "A place in the house that is not a guest room — a lobby, a corridor, a lift, the pool, the gym, the staff canteen — whose cleaning and inspection are housekeeping tasks linked `housekeeping_task_for_area` (hubOS's common zones). `code` is the house's business key for it (`register_public_area` upserts by it); `kind` is what a checklist template names (`applies_to_area_kind`), so every corridor shares one list; `reader_code` is what its QR / NFC reader carries, unique in the house across rooms and areas (`set_public_area_reader_code`) — the handset scans it and asks the object search for an `eq` on `reader_code`. An `active` area gets one `public_area_clean` per business date from `generate_housekeeping_tasks`; a `retired` one gets nothing and carries no reader code.",
      "display_name": "Public area",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "The house's code for the area (\"LOBBY\", \"C3-EAST\")",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "What the floor calls it (\"Main lobby\", \"Third-floor corridor, east\")",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "What kind of place it is — what a checklist template names",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "floor",
          "description": "",
          "display_name": "The floor it is on, in the rooms' own words (a room's `floor`)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "reader_code",
          "description": "",
          "display_name": "The code the area's QR / NFC reader carries (unique within the property, across rooms and areas)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Active (cleaned daily, scannable) or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the area was retired (present while retired)",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.expire_purpose_grant",
          "description": "Transition an overdue `requested`/`active` grant to `expired` — invoked by the §9.1 expiry sweep when `expires_at` elapses (the grant implements the `expirable` interface); also callable by an operator forcing the question. Idempotent on a terminal grant. Emits `purpose_grant.expired`."
        },
        {
          "api_name": "hospitality.action.revoke_purpose_grant",
          "description": "Revoke a `requested` or `active` grant — the operator kill-switch. Terminal; the row is kept for audit. Idempotent on an already-revoked grant. Emits `purpose_grant.revoked`."
        }
      ],
      "api_name": "hospitality.purpose_grant",
      "description": "One principal's hold on an `access_purpose`: requested with a recorded rationale, approved through an ordinary 0020 approval gate (the grant implements the `approvable` interface; the gate decision transitions it), alive until `expires_at` (the §9.1 sweep expires it — the grant implements `expirable`) or an operator revocation. Terminal grants are kept: the row is the audit answer to `who could touch this marking, under what purpose, on whose approval, and why`.",
      "display_name": "Purpose grant",
      "domain": "pbac",
      "interfaces": [
        "hospitality.interface.approvable",
        "hospitality.interface.expirable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Grant id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "purpose_ref",
          "description": "",
          "display_name": "Business id of the `access_purpose` this grant confers (also linked via `purpose_grant_for_purpose`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "principal_id",
          "description": "",
          "display_name": "The holding principal's stable id (`user:<uuid>` / `service:<name>` / `agent:<vendor>/<model>@<version>`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "The requester's recorded rationale — why this principal needs this purpose now. The PBAC audit artefact; bounded prose, not a ticket link (put references in external_reference)",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "external_reference",
          "description": "",
          "display_name": "Opaque ref to the driving case (ticket, investigation, DSAR)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "requested_at",
          "description": "",
          "display_name": "When the grant was requested",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "When an `active` grant lapses (the expiry sweep transitions it); bounded by the purpose's `max_grant_hours`",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the approval gate decided (or an operator revoked)",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.cancel_rail_instruction",
          "description": "Take back a card-rail instruction that is still `queued` — a capture, release, refund or departed stay's collection the card rail wrote down while the processor was unreachable — before the rail sends it: the desk is settling it another way, or asked for the wrong thing. Only a `queued` instruction can be cancelled; one the rail is `sending` is being asked of the processor now and finishes on its own, and a final one (done, refused, abandoned, cancelled) stays as it is — cancelling a cancelled one again changes nothing. Nothing is asked of the processor: a queued instruction's earlier attempt that may have reached it is the desk's to check at the dashboard (the instruction's words say so). The payment or stay is free for another card verb at once. Emits `rail_instruction.cancelled`."
        },
        {
          "api_name": "hospitality.action.record_rail_instruction_outcome",
          "description": "**Card-rail only.** Record where a queued rail instruction stands: `sending` (the drain claimed it and is asking the processor — the attempt is counted; a claim left by a drain that stopped mid-send may be claimed again, under the same keys), `queued` again (the processor was still unreachable: `not_before` is the backoff, `processor_words` why), `done` (the processor did it and the ledger recorded it through the online path's verb; `outcome_detail` says what, `result_payment` the row it made), `refused` (the processor or the ledger said no — `processor_words`; never retried) or `abandoned` (unreachable past the retry cap or the key's window, or the processor's answer could not be told — the GM settles it by hand). `queued` with a new `attempt_id` is the processor having replayed a saved failure under the old key when nothing moved: the attempt is spent and the next is a new request. Done, refused, abandoned and cancelled are final; recording the same final state again changes nothing; `cancelled` is the desk's (`cancel_rail_instruction`). Emits `rail_instruction.done`, `.refused` or `.abandoned`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        }
      ],
      "api_name": "hospitality.rail_instruction",
      "description": "One thing the card rail is to do at the processor once it can be reached again — OPERA's offline settlement queue. Written by the rail (`queue_rail_instruction`) when the processor was unreachable (a transport failure, a 5xx, a 429) during a deferrable verb: a hold captured at check-out (`capture`), a hold released (`void`), a refund (`refund`), a departed stay's late charges (`charge_off_session`), the desk's Top up now for an in-house stay (`top_up`, continuing the stay's open top-up attempt). Never a check-in's authorisation — that hold is placed because the desk needs the answer now. `attempt_id` is written before the processor is asked again and every processor idempotency key for the instruction derives from it, so a retry is the same request and a capture that reached the processor once is never taken twice. The rail's drain claims it (`sending`), asks the processor, and records the outcome through the same ledger verbs the online path uses: `done`; `refused` with the processor's words, never retried; or `queued` again with an exponential backoff (`not_before`) until the cap or the key's window, then `abandoned` for the GM. A processor that replayed a saved failure under the key has spent that attempt: what it did is read back, and when nothing moved the instruction is queued again under a new `attempt_id`. The desk may take a `queued` one back (`cancel_rail_instruction` → `cancelled`), never one the drain is sending. The night audit drains whatever is queued, and its close lists what is still open.",
      "display_name": "Rail instruction",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Rail instruction id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "capture, void, refund, charge_off_session or top_up",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "state",
          "description": "",
          "display_name": "queued, sending, done, refused, abandoned or cancelled",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "payment_id",
          "description": "",
          "display_name": "The payment a capture, release or refund acts on (its object id) — the rail_instruction_for_payment edge's target",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "stay_id",
          "description": "",
          "display_name": "The stay a collection or a top-up is for (its object id) — the rail_instruction_for_stay edge's target",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "property_id",
          "description": "",
          "display_name": "The house the instruction is for (its object id)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "What it moves: the capture to take, the hold to release, the refund to give, the collection asked for",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "attempt_id",
          "description": "",
          "display_name": "The persisted attempt every processor idempotency key for the instruction derives from — a refund's is the key its online attempt was sent under, a collection's the stay's open collection attempt",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "void_reason",
          "description": "",
          "display_name": "Why a queued release releases the hold (the void's reason)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "refund_reason",
          "description": "",
          "display_name": "Why a queued refund gives money back",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "reverse_transfer",
          "description": "",
          "display_name": "Whether a queued refund pulls the hotel's transfer back — the online attempt's form, kept so the drained retry under the same key is the same request (F4, 2026-10-19)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "queued_by",
          "description": "",
          "display_name": "Who asked for it: the principal the queued request carried (its JSON), under whose name the drain records the ledger's leg — the online attempt's, not the rail's (F4, 2026-10-19)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "attempts",
          "description": "",
          "display_name": "How many times the drain has asked the processor",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "not_before",
          "description": "",
          "display_name": "When the drain may ask again — the backoff after an unreachable rail",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "queued_at",
          "description": "",
          "display_name": "When the rail wrote the instruction down",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_attempt_at",
          "description": "",
          "display_name": "When the drain last asked the processor",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "finished_at",
          "description": "",
          "display_name": "When it was done, refused, abandoned or cancelled",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "queued_because",
          "description": "",
          "display_name": "What kind of failure the rail met when the instruction was written (`server_error (503)`, `timeout`, …) — a kind, never the request path, which names the processor's handle",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "processor_words",
          "description": "",
          "display_name": "The processor's last words: its refusal, or the kind of failure that kept it from being reached — processor handles left out",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "outcome_detail",
          "description": "",
          "display_name": "What the instruction came to, in the rail's words",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "result_payment_id",
          "description": "",
          "display_name": "The payment row the outcome recorded, when it made one (a refund's leg)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.rate_per_night",
      "description": "A per-date price for a `(rate_plan, room_type, occupancy)` tuple. Maps directly to OTA `OTA_HotelRateAmountNotifRQ`. Rates are intentionally explicit — no derivation is implied; derived rates live on `RatePlan` itself or on a future `DerivedRate` object.",
      "display_name": "Rate per night",
      "domain": "distribution",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Rate id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "occupancy",
          "description": "",
          "display_name": "Occupancy (adult count for this rate)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Amount per night",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "inclusive_of_tax",
          "description": "",
          "display_name": "Inclusive of tax",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "bar_anchor_amount",
          "description": "",
          "display_name": "BAR anchor (bar_grid rate plans)",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.recommend_rate",
          "description": "Emit a typed rate recommendation. The agent payload (recommended_amount, current_amount, confidence, factors, forecast_snapshot, rationale) lands as a `rate_recommendation` row in `suggested` state with an immutable forecast snapshot captured at issue time. Decision lifecycle then flows through `apply_rate_recommendation` / `reject_rate_recommendation` / `expire_rate_recommendations`."
        },
        {
          "api_name": "hospitality.action.update_pricing_strategy",
          "description": "Update a rate plan's pricing strategy and coherent derivation / yieldable fields together. The rate-plan validator rejects incoherent transitions."
        }
      ],
      "api_name": "hospitality.rate_plan",
      "description": "A pricing plan attached to a property or brand. Reservations reference a rate plan to determine pricing, inclusions, and cancellation policy.",
      "display_name": "Rate plan",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "commissionable",
          "description": "",
          "display_name": "Whether stays on this plan earn agent commission (default true)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Rate plan code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Rate plan name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "inclusion",
          "description": "",
          "display_name": "Inclusion",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cancellation_policy",
          "description": "",
          "display_name": "Cancellation policy",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "day_use",
          "description": "",
          "display_name": "Day-use plan (sells zero-night day slots; default false)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "min_stay_nights",
          "description": "",
          "display_name": "Minimum nights",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "max_stay_nights",
          "description": "",
          "display_name": "Maximum nights",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency (ISO 4217)",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "pricing_strategy",
          "description": "",
          "display_name": "Pricing strategy",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "derived_modifier_kind",
          "description": "",
          "display_name": "Derived modifier kind (bar_grid only)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "yieldable",
          "description": "",
          "display_name": "Yieldable by revenue agent",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_rate_recommendation",
          "description": "Apply a suggested rate_recommendation through to its target rate_per_night row. Writes to `amount` for open-pricing rate plans; on bar_grid plans it writes `bar_anchor_amount` and re-derives the row's `amount` — the price that sells — from the new anchor through the row's own modifier (percent: the ratio, absolute: the difference), rounded to the cent. Optional `modified_amount` lets the operator accept the strategy but override the number — the recommendation transitions to `modified_and_applied`, the modification is captured in `applied_amount`, and the override-rate analytics surface picks it up. Rejected if the recommendation isn't in `suggested` state or has expired."
        },
        {
          "api_name": "hospitality.action.reject_rate_recommendation",
          "description": "Decline a suggested rate_recommendation. Transitions `decision_status` to `rejected` and records an optional free-text reason for override-rate analytics. Rejected if the recommendation isn't in `suggested` state."
        }
      ],
      "api_name": "hospitality.rate_recommendation",
      "description": "A typed rate recommendation issued by an agent against a specific (rate_plan, room_type, business_date, occupancy) tuple. Mostly-immutable post-issue: the recommendation payload (recommended_amount, lift, confidence, forecast_snapshot, factors_considered, rationale) is frozen at write time so the audit trail is the rationale at decision time, not against today's forecast. The decision_* fields mutate exactly once on the transition out of `suggested` (apply / modified_and_apply / reject / expire / supersede) — that single mutation is gated by the `decision_status_transitions` cross-link validator.",
      "display_name": "Rate recommendation",
      "domain": "revenue_management",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Recommendation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date the recommendation applies to",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "occupancy",
          "description": "",
          "display_name": "Occupancy (adult count for the target rate)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "target_field",
          "description": "",
          "display_name": "Target rate_per_night field (bar_anchor for bar_grid plans; amount for open-pricing plans)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "current_amount",
          "description": "",
          "display_name": "Snapshot of the target amount at issue time",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "recommended_amount",
          "description": "",
          "display_name": "Proposed new amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "delta_amount",
          "description": "",
          "display_name": "Convenience: recommended_amount - current_amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "lift_pct",
          "description": "",
          "display_name": "Convenience: (recommended_amount / current_amount) - 1",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "confidence",
          "description": "",
          "display_name": "Operator-facing confidence ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "confidence_score",
          "description": "",
          "display_name": "Calibrated probability (0..=1) — set when the model emits one",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "factors_considered",
          "description": "",
          "display_name": "Signals the recommendation conditioned on (the strategy that fired)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "Human-readable explanation surfaced to operators",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "forecast_snapshot",
          "description": "",
          "display_name": "Forecast snapshot frozen at issue time (per §B.2)",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the agent emitted the recommendation",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "After this, decision_status moves to expired",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_on",
          "description": "",
          "display_name": "The house business day this lapses after. When set, the house's open business date decides expiry (not the wall clock) and expires_at is the nominal end of that day in the house's timezone",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "decision_status",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the recommendation transitioned out of suggested",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "applied_amount",
          "description": "",
          "display_name": "The amount actually applied (equals recommended_amount for plain apply; differs for modified_and_applied)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Free-text reason set on reject (the gold-standard decision log pattern — see proposal §6.3)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.rate_recommendation_run",
      "description": "Batched parent of one cadence's worth of recommendations — rate moves and sell restrictions alike — issued by an agent against a property. Append-only, immutable post-finalisation per §B.2 (matching `night_audit_run` and `channel_mix_snapshot` semantics). The four denormalised counters (`recommendations_count`, `applied_count`, `rejected_count`, `expired_count`) are §B.1 projections refreshed by the lifecycle actions; `recommendations_count_computed_at` is the refresh pair.",
      "display_name": "Rate recommendation run",
      "domain": "revenue_management",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the run opened",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "finished_at",
          "description": "",
          "display_name": "When the run finalised — set on completed / failed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cadence",
          "description": "",
          "display_name": "Cadence",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "agent_vendor",
          "description": "",
          "display_name": "Agent vendor (denormalised from Principal::Agent.vendor)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agent_version",
          "description": "",
          "display_name": "Agent version (denormalised from Principal::Agent.version)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agent_model",
          "description": "",
          "display_name": "Model variant if the vendor exposes it",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "target_horizon_days",
          "description": "",
          "display_name": "Forecast horizon the run looked over",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "recommendations_count",
          "description": "",
          "display_name": "Count of recommendations issued in this run (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "applied_count",
          "description": "",
          "display_name": "Count of recommendations applied (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "rejected_count",
          "description": "",
          "display_name": "Count of recommendations rejected (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "expired_count",
          "description": "",
          "display_name": "Count of recommendations expired (denormalised; §B.1)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "recommendations_count_computed_at",
          "description": "",
          "display_name": "Refresh pair for the four counters (§B.1 discipline)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "failure_reason",
          "description": "",
          "display_name": "Set on status = failed",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.lift_rate_restriction",
          "description": "Take one authored restriction off the board: stamps `lifted_at` (business time, defaults to now) and keeps the row as history — the sellability gate, the channel wire and the desk board all treat a lifted row as absent. Lifting a row that is already lifted is a no-op that writes nothing. This is the row-level counterpart of `clear_rate_restrictions`, which opens a window by scope and splices whatever it reaches: a minimum stay lifted with `lift` leaves a stop-sell on the same night standing, where a scoped clear would open both."
        }
      ],
      "api_name": "hospitality.rate_restriction",
      "description": "A date-range constraint on a rate plan / room type pair: closed-to-arrival (CTA), closed-to-departure (CTD), minimum/maximum length-of-stay (MinLOS/MaxLOS), and a hard `closed` stop-sell. Maps to OTA `OTA_HotelAvailNotifRQ` restrictions. Multiple restrictions can apply to the same date — the platform composes them as the conjunction. The validator on this type enforces `start_date <= end_date` and `min_length_of_stay <= max_length_of_stay` on every write.",
      "display_name": "Rate restriction",
      "domain": "distribution",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Restriction id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "start_date",
          "description": "",
          "display_name": "Start date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "end_date",
          "description": "",
          "display_name": "End date (inclusive)",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "closed",
          "description": "",
          "display_name": "Closed (stop-sell)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "closed_to_arrival",
          "description": "",
          "display_name": "Closed to arrival",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "closed_to_departure",
          "description": "",
          "display_name": "Closed to departure",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "min_length_of_stay",
          "description": "",
          "display_name": "Minimum length of stay (nights)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "max_length_of_stay",
          "description": "",
          "display_name": "Maximum length of stay (nights)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "posted_at",
          "description": "",
          "display_name": "Business time the restriction was posted (defaults to record time)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "lifted_at",
          "description": "",
          "display_name": "Business time the restriction was lifted; a lifted row constrains nothing and is kept as history",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Replay key for an authoring source (unique per tenant while present)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.refresh_recipe_pour_cost",
          "description": "Recompute the recipe's `pour_cost` by walking its ingredients and sub-recipes against the active `ingredient_supplier_price` row for the recipe's property. Idempotent; updates `pour_cost_computed_at`."
        }
      ],
      "api_name": "hospitality.recipe",
      "description": "Production-side spec for a saleable item. 1:1 with `service_item` via `recipe_for_service_item`; composes other recipes via `recipe_uses_sub_recipe` (a clarified milk-wash referenced by multiple cocktails); references typed `ingredient` rows for pour-cost rollup. Allergens flow up from ingredient flags; the recipe `allergens` array is the producer- side rolled disclosure.",
      "display_name": "Recipe",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Recipe id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "recipe_kind",
          "description": "",
          "display_name": "Recipe kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "serves_per_batch",
          "description": "",
          "display_name": "Serves per batch",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "pre_batch",
          "description": "",
          "display_name": "Pre-batched",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "ingredients",
          "description": "",
          "display_name": "Ingredients (typed)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "build_method",
          "description": "",
          "display_name": "Build method",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "build_duration_seconds",
          "description": "",
          "display_name": "Build duration (seconds)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "dilution_percent",
          "description": "",
          "display_name": "Dilution percent (drink_*)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "glass_type",
          "description": "",
          "display_name": "Glass type (drink_*)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "glass_specifier",
          "description": "",
          "display_name": "Glass specifier (brand/size override)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "ice_type",
          "description": "",
          "display_name": "Ice type (drink_*)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "garnishes",
          "description": "",
          "display_name": "Garnishes",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "shelf_life_hours",
          "description": "",
          "display_name": "Shelf life (hours)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "shelf_life_storage",
          "description": "",
          "display_name": "Shelf life storage",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "pour_cost",
          "description": "",
          "display_name": "Pour cost (projection)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "pour_cost_computed_at",
          "description": "",
          "display_name": "Pour cost refresh timestamp",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "sell_price",
          "description": "",
          "display_name": "Sell price (cache from service_item)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "allergens",
          "description": "",
          "display_name": "Allergens (rollup; EU 14 list)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "dietary_classification",
          "description": "",
          "display_name": "Dietary classification (lifestyle, not regulatory)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "method_steps",
          "description": "",
          "display_name": "Method steps (ordered prep instructions)",
          "kind": "array",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.reciprocity_agreement",
      "description": "The org-to-org primitive. Yacht-club reciprocity, Soho House Every House, partner-tier drop-ins, LHW Leaders Club at SLH-Hilton-partnership properties. Distinct from `membership_tier` because the agreement is between organisations (with cost structure and access scope) and individual member access flows through the agreement, not through a duplicated tier on the partner organisation's side.",
      "display_name": "Reciprocity agreement",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Agreement code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Agreement name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agreement_kind",
          "description": "",
          "display_name": "Agreement kind (bilateral / unilateral / network)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "access_scope",
          "description": "",
          "display_name": "Access scope (full / shared / drop-in / network)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cost_structure",
          "description": "",
          "display_name": "Cost structure (who pays and how)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "Effective from",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Effective to (null when ongoing)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "is_active",
          "description": "",
          "display_name": "Currently active",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "per_visit_fee_amount",
          "description": "",
          "display_name": "Per-visit fee (required when cost_structure ∈ {member_pays_drop_in, host_pays_per_visit})",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "per_visit_fee_currency",
          "description": "",
          "display_name": "Per-visit fee currency (ISO 4217)",
          "kind": "currency",
          "required": false
        },
        {
          "api_name": "monthly_visit_cap",
          "description": "",
          "display_name": "Monthly visit cap (capped reciprocity)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "agreement_terms_text",
          "description": "",
          "display_name": "Agreement-terms snapshot at signing (design-spec §B.2)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.recognition_event",
      "description": "A recorded recognition of a person — name acknowledged, preference anticipated, relationship remembered, gesture offered, upgrade provided, amenity provided, personal note left, birthday or anniversary marked, repeat visit acknowledged, VIP treatment initiated. Always-on across all loyalty archetypes — the program_type does not gate recognition; Aman runs recognition-only and Maybourne runs LHW + deep internal recognition independently. `delivery_visibility` types the Aman invisible-attention vs Ritz visible-attention axis: the same recognition can be delivered visibly (a hand-written welcome card on arrival) or invisibly (the temperature set to the guest's preference before they enter the room). The pair with proposal 0002's `service_recovery_event` lets the read-side projection surface the operator's recognition-vs-recovery balance.",
      "display_name": "Recognition event",
      "domain": "loyalty",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Recognition event id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "recognition_kind",
          "description": "",
          "display_name": "Recognition kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "delivery_visibility",
          "description": "",
          "display_name": "Delivery visibility",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recognition_subject_text",
          "description": "",
          "display_name": "What was recognised",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "recognition_action_text",
          "description": "",
          "display_name": "What the operator did",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "recogniser",
          "description": "",
          "display_name": "Recogniser principal identifier",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "comp_amount",
          "description": "",
          "display_name": "Comp amount when recognition includes value",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "comp_category",
          "description": "",
          "display_name": "Comp category (toolkit dictionary key)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "happened_at",
          "description": "",
          "display_name": "When the recognition happened",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "stay_id",
          "description": "",
          "display_name": "Stay context (when applicable)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.close_pattern_detection_run",
          "description": "Mark a recovery_pattern_detection_run terminal (completed or failed) once its assessments are dispositioned."
        }
      ],
      "api_name": "hospitality.recovery_pattern_detection_run",
      "description": "A property-scoped run parent for service-recovery pattern detection. Captures scan window, scan signals, agent provenance, status, and lifecycle counters.",
      "display_name": "Recovery pattern detection run",
      "domain": "recovery_pattern_detection",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "Run start",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "finished_at",
          "description": "",
          "display_name": "Run finish",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Run status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cadence",
          "description": "",
          "display_name": "Run cadence",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "agent_vendor",
          "description": "",
          "display_name": "Agent vendor",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agent_version",
          "description": "",
          "display_name": "Agent version",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "agent_model",
          "description": "",
          "display_name": "Agent model",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "scan_window_start",
          "description": "",
          "display_name": "Scan window start",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "scan_window_end",
          "description": "",
          "display_name": "Scan window end",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "scan_signals",
          "description": "",
          "display_name": "Signals included in this scan",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "scan_method",
          "description": "",
          "display_name": "Detector method or rule-set description",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "patterns_count",
          "description": "",
          "display_name": "Patterns emitted",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "applied_count",
          "description": "",
          "display_name": "Patterns applied",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "rejected_count",
          "description": "",
          "display_name": "Patterns rejected",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "escalated_count",
          "description": "",
          "display_name": "Patterns escalated",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "expired_count",
          "description": "",
          "display_name": "Patterns expired",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "patterns_count_computed_at",
          "description": "",
          "display_name": "When counters were last refreshed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "failure_reason",
          "description": "",
          "display_name": "Failure reason when status = failed",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.recovery_toolkit",
      "description": "The property's comp-authority policy + recovery framework + top-cover principle. The `policy_kind` enum spans the observed-in-production patterns (Ritz `flat_universal`, USHG `per_role_per_shift_cumulative`, Aman `training_and_tenure_based`, the strict `escalation_only`); the `role_authorities` array carries per-role rules for the policies that need them (per-role-per-incident, per-role-mixed). The original sketch of 'comp authority per role' could not admit the universal, per-shift cumulative, training-based, or escalation-only shapes — the enum + array pair is the research-led correction. See `products/forge/spec/proposals/0002-service-philosophy.md` §3.3 and §10 open question 1 for reviewer context. Soft validators on over-limit recoveries warn but don't block — over-limit recoveries are exactly the data the operator most needs and would otherwise hide (the literature is explicit that hard blocks produce 'ghost recoveries' where the front line works around the system).",
      "display_name": "Recovery toolkit",
      "domain": "service_philosophy",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Toolkit id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "policy_kind",
          "description": "",
          "display_name": "Comp authority policy kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "flat_universal_per_incident_limit",
          "description": "",
          "display_name": "Flat universal per-incident comp limit",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "flat_universal_per_guest_per_day",
          "description": "",
          "display_name": "Flat universal per-guest-per-day cap",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "role_authorities",
          "description": "",
          "display_name": "Per-role comp authority rules",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "recovery_process_framework",
          "description": "",
          "display_name": "Recovery process framework",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "comp_category_definitions",
          "description": "",
          "display_name": "Property-specific comp categories",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "top_cover_principle_text",
          "description": "",
          "display_name": "Top-cover principle",
          "kind": "text",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.registration_card",
      "description": "The registration card a guest signed at the desk on check-in: the house's terms as they were shown, verbatim, who signed and when, and the signature's strokes as SVG path data. A chargeback's evidence packet attaches the current card as `customer_signature`. One current card per stay; signing again supersedes it.",
      "display_name": "Registration card",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Card id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "signed_at",
          "description": "",
          "display_name": "When the guest signed (the kernel's clock at the desk's write)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "signer_name",
          "description": "",
          "display_name": "The name the guest signed under, as the desk confirmed it",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "terms_version",
          "description": "",
          "display_name": "Which version of the house's terms was shown — a label or a hash of the text, so two cards signed under the same wording say so",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "terms_text",
          "description": "",
          "display_name": "The terms the guest was shown above the signature, verbatim",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "signature_svg_paths",
          "description": "",
          "display_name": "The signature's strokes as SVG path data (`M x y L x y …`, in the pad's own units; at most 64 KB)",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "signature_width",
          "description": "",
          "display_name": "The width of the pad the strokes were drawn on, in the strokes' units",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "signature_height",
          "description": "",
          "display_name": "The height of the pad the strokes were drawn on, in the strokes' units",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "captured_by",
          "description": "",
          "display_name": "The desk principal that recorded the card (`principal.stable_id()`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "device_label",
          "description": "",
          "display_name": "The desk device the guest signed on, as the desk names it",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "superseded_at",
          "description": "",
          "display_name": "When a later signing on the same stay replaced this card",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.annotate_reservation",
          "description": "Set or clear the notes-shaped fields on a reservation: `special_requests`, `channel_reference`, `stay_context_kind` and `stay_context_notes`. Every parameter is optional and at least one must be passed; an empty (or whitespace-only) text clears its field, and the kind can be changed but not cleared. Touches nothing operational — dates, rooms, rates and status keep their own verbs — so it is legal in every reservation state, including after check-out. Notes without a stay-context kind are accepted with the validator's warning, matching the fill-the-kind-in-later workflow."
        },
        {
          "api_name": "hospitality.action.assemble_package_sale",
          "description": "Attach a package to a reservation, freezing the package's current version (components + rules) into the sale's `package_version_snapshot`."
        },
        {
          "api_name": "hospitality.action.assign_room",
          "description": "Assign a specific physical room to a single-room reservation. The handler rejects the action if the room's room_type does not match the reservation's reserved room_type, and rejects multi-room reservations outright — one physical room cannot serve N reservation_rooms; use `assign_room_for_reservation_room` per room instead. Re-assigning the room already assigned is an idempotent no-op."
        },
        {
          "api_name": "hospitality.action.cancel_reservation",
          "description": "Move a reservation to cancelled state. Guest-impacting and destructive, so agent principals cannot apply it directly: an agent stages it via `propose_action` and a human confirms (agent_confirm_required)."
        },
        {
          "api_name": "hospitality.action.check_in",
          "description": "Mark arriving guests as in-house: one stay and one window-1 folio per reservation_room (the multi-room stay model — the folio's opened_at is the actual arrival, business time, not record time). The whole party checks in by default; pass reservation_room_ids for a staggered arrival's subset. Replay-iff-identical per leg: re-applying with the arrival_actual a leg's stay already carries skips that leg, so connector variant steps and crashed-pass retries are safe. A day-use arrival also posts the slot's one room charge (with derived tax lines) to the leg's new folio — the night audit never sees a zero-night stay, so check-in is its posting moment; a zero slot rate is a comp and posts nothing."
        },
        {
          "api_name": "hospitality.action.check_out",
          "description": "Mark departing guests as checked out and close each departing stay's open folio windows (closed_at is the actual departure — business time). The whole party departs by default; pass reservation_room_ids for a staggered departure's subset — the reservation completes only when no leg remains in house, and never-arrived legs cancel at that moment with their unslept nights released back to inventory. Refuses a non-zero folio balance unless `allow_open_balance` is set: front-desk flows settle first, while a connector mirroring a vendor's already-departed guest records the fact and lets the charges / payments streams settle the folio in their own passes. Replay-iff-identical per leg: re-applying with the departure_actual a leg's stay already carries skips that leg. Every pending card hold the departure leaves is decided (`payment.hold_decision`, `payment.hold_decided`): released, taken for what a bill still owes, or — with the house's post-stay window on (`set_post_stay_charging`) — the stay's largest kept until the window's end, the stay stamped `post_stay_window = open`. The card rail carries the decision out at the processor after commit."
        },
        {
          "api_name": "hospitality.action.collect_cancellation_fee",
          "description": "**Card-rail only.** Record what collecting a posted cancellation fee did. The desk posts the fee onto the booking's own account (`post_cancellation_fee`); taking the money happens outside the ontology, against the guest's booking hold and stored card, and this is where the result comes back — the no-show fee's `collect_no_show_fee`, for the other retained fee. `outcome = collected` mints the settled payment (`CXF-`) on the booking and on that account, and needs `amount` and `psp_reference` unless the booking hold paid (`hold_payment_ref`). The card is asked only for what the account still owes after the deposit already on it, so a hold that paid that much has paid the fee. `authentication_required` is **not** a decline: the fee is still owed and collectable once the guest authenticates. `uncollectible` is a declined card or none at all. Idempotent — the same outcome twice is a no-op, a second payment for a fee already `collected` is refused (`fee_already_collected`) and a collected fee refuses to move. Recording an outcome closes the attempt `open_cancellation_fee_attempt` opened, as for the no-show fee. The desk reaches it through `POST /v1/reservations/{id}/cancellation-fee/collection`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.collect_no_show_fee",
          "description": "Record what collecting an assessed no-show fee did. The night audit assesses the fee and posts the charge (`post_no_show_fees`); taking the money happens outside the ontology, against the guest's stored card, and this is where the result comes back. `outcome = collected` mints the settled payment row and needs `amount` and `psp_reference`. `authentication_required` is the one to read carefully: the issuer demanded SCA on a charge with nobody present, which is **not** a decline — the fee is still owed and still collectable once the guest authenticates, and recording it as uncollectible writes off money the house could have taken. `uncollectible` is the honest terminal state for a declined card or no card at all: the receivable stands, but collecting it is now a conversation rather than an API call. Idempotent — recording the same outcome twice is a no-op, and a fee already `collected` refuses to move. Recording an outcome closes the attempt `open_no_show_fee_attempt` opened (`no_show_fee_attempt_card_ref`, `no_show_fee_attempt_open_at`): a change of status closes it unless `attempt_opened_at` names another attempt, and the same status again closes it only when `attempt_opened_at` names it. The card rail's own verb (internal-runner, 2026-10-19): it asserts what the processor took, so the desk reaches it through `POST /v1/reservations/{id}/no-show-fee/collection`, never a tenant's apply."
        },
        {
          "api_name": "hospitality.action.forfeit_deposit",
          "description": "Post a forfeited-deposit charge against the reservation's folio — the stay's when there is one, the booking's own account otherwise, opened here so a cancelled booking's charge is one a bill can show. The deposit already taken moves onto that folio as the credit the charge consumes, up to the amount forfeited. Posts under USALI `miscellaneous_income` with `system_charge_kind = forfeited_deposit`. Idempotent on (reservation, business_date, forfeiture_amount). Tax is derived from the rules the house authored for a `forfeited_deposit` active on the business date — a UK house taxes it as consideration for the room that was booked, an Austrian one does not — and a house with no such rule posts the charge untaxed. A booking's tax exemption reaches it. Refused once a cancellation fee is posted on the booking (`post_cancellation_fee`, 2026-10-27): the deposit on its account already pays toward that fee, and Keep and Charge are one decision."
        },
        {
          "api_name": "hospitality.action.mark_reservation_no_show",
          "description": "Record that the guest never arrived: reservation to the terminal no_show state, its room slices to cancelled, and their room-nights released back to the availability ledger from `as_of` — the business day the no-show belongs to, the arrival day the audit closed, never after the house's today — or, when it is not given, from the house's today forward: the hotel date (the business date the property has open, else its local calendar day). A no-show recorded after the fact without `as_of` therefore releases nothing: its nights have passed. Normally driven during the night-audit sweep of the arrival business day, which passes the day it is closing. Posts the house's no-show posting rule for the booking's guarantee onto its own account, as the audit's sweep does, so a booking the desk marks before the close is posted the same as one the close marks. Guest-impacting and terminal, so agents stage it for human confirmation."
        },
        {
          "api_name": "hospitality.action.modify_reservation_dates",
          "description": "Change a reservation's stay window — the single most common front-desk write. Pre-arrival (held / confirmed / modified) both dates move; in-house only the departure moves (extend or shorten — never below the house's today, the hotel date, and never touching nights already slept). Every leg's nights are re-derived: surviving nights stay, dropped nights soft-delete and release their room-nights back to the availability ledger, added nights mint, consume availability (a sold-out night refuses the whole change atomically, naming the night), and price per the required pricing_policy. Re-applying with the current dates and keep_rate is a no-op; the same dates with requote / override is an explicit in-place re-price. The reservation's rate-plan stay-length window re-checks, and pre-arrival changes re-run the full restriction gate for the new window. A day-use reservation (zero-night window on a day-use plan) moves its slot date but keeps the zero-night shape; keep_rate keeps the leg's slot rate, override re-prices it, and the requote kinds refuse (no per-night grid exists for a day slot)."
        },
        {
          "api_name": "hospitality.action.move_room",
          "description": "Move an in-house guest to another room in one atomic verb (pre-check-in moves are assign_room_for_reservation_room). The target room must be vacant, in the same property, and not out-of-order for the remaining stay; a dirty or pickup room needs an explicit allow_dirty_room acknowledgment. On a cross-type move the remaining (unslept) nights settle the availability ledger atomically — a sold-out night refuses the whole move, naming the night — and the required pricing_policy decides the money: keep_rate pins reservation_room_charged_as_room_type (the comp upgrade — the guest sleeps in the new type, keeps paying the old one); requote re-prices remaining nights from the new type's published grid; override takes the supplied rate with its reason. The old room is vacated into a declared housekeeping status (old_room_status, default dirty), the new room becomes occupied, and the emitted event is the permanent move history."
        },
        {
          "api_name": "hospitality.action.open_cancellation_fee_attempt",
          "description": "**Card-rail only.** Write down a collection of a posted cancellation fee before the card rail asks the processor to charge the guest's stored card: `cancellation_fee_attempt_card_ref` names the card, `cancellation_fee_attempt_provider_card_ref` and `cancellation_fee_attempt_provider_customer_ref` copy its instrument (token-marked), and `cancellation_fee_attempt_open_at` stamps the attempt, from which the processor's idempotency key is derived. An attempt already open is kept as it was, so a retry of one whose outcome never came back reaches the processor as the same request with the same instrument; naming another card while it is open is refused. `collect_cancellation_fee` closes it. Refused for a fee that is not assessed, awaiting authentication or uncollectible, and for a card with no processor token — the no-show fee's `open_no_show_fee_attempt`, for the other retained fee. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.open_no_show_fee_attempt",
          "description": "**Card-rail only.** Write down a collection of an assessed no-show fee before the card rail asks the processor to charge the guest's stored card: `no_show_fee_attempt_card_ref` names the card, `no_show_fee_attempt_provider_card_ref` and `no_show_fee_attempt_provider_customer_ref` copy its instrument (token-marked), and `no_show_fee_attempt_open_at` stamps the attempt, from which the processor's idempotency key is derived. An attempt already open is kept as it was, so a retry of one whose outcome never came back reaches the processor as the same request with the same instrument, whatever the card row or the house's merchant account did in between; naming another card while it is open is refused. `collect_no_show_fee` closes it. Refused for a fee that is not assessed, awaiting authentication or uncollectible, and for a card with no processor token. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.post_cancellation_fee",
          "description": "Charge a cancelled booking what its terms said cancelling would cost. The amount defaults to the figure frozen at booking — the step of the ladder in force when the guest cancelled — so the desk charges what was disclosed rather than a number typed on the day; pass `amount` only to charge less. Posts under USALI miscellaneous income with `system_charge_kind = cancellation_fee`, onto the booking's own account, opened here because a cancelled booking never checks in. Taxed like every retained payment: by the rules the house authored for a `cancellation_fee`, and not at all where it authored none.\n\nThe sibling of the night audit's no-show fee, and deliberately not the same verb: a no-show is swept automatically at the close of the arrival day, while a cancellation is a decision someone makes. Apaleo splits them the same way, as two folio actions posting the same shape. Collecting the money afterwards is the card rail's `POST /v1/reservations/{id}/cancellation-fee/collection` (2026-10-27), which asks the card for what the account still owes after any deposit on it — or the ordinary folio payment surface, since the charge is on an account and an account is billable. Idempotent: a booking whose fee is already posted refuses a second one. A guest who cancelled before any step of the frozen ladder charged anything owes nothing, and is charged only an `amount` the desk states (`cancelled_outside_window` otherwise); an `amount` is capped at the step in force. Refused once the deposit was kept (`forfeit_deposit`): Keep and Charge are one decision."
        },
        {
          "api_name": "hospitality.action.queue_reservation_for_room",
          "description": "The guest is at the desk and the room is not ready: put the reservation on the room queue (OPERA's queue room). Refuses unless the reservation is pre-check-in with its arrival day reached. The rush signal derives instead of being a second manual write: the assigned room's open housekeeping tasks bump to urgent on the spot. A clean / inspected report on that room later emits room_queue.room_ready (the guest-messaging hook), and check_in fulfils the entry the moment the guest gets the room. Re-queueing a reservation already waiting is a no-op."
        },
        {
          "api_name": "hospitality.action.record_authentication_pending",
          "description": "**Card-rail only.** Record that an off-session charge or hold the card rail tried for this booking met the issuer's demand that the cardholder authenticate (Stripe's `authentication_required`) — the one verb every off-session path records it through: the no-show fee, a departed stay's late charges (`authentication_stay_ref`), a booking hold re-placed before the issuer's window, a top-up hold. Mints the chase: a `pending` payment request with reason `authentication_required` for the `amount` (a `payment` for money, a `preauthorization` for a hold — against the booking's own account, opened when there is none, except a `hold_top_up`, which holds against the in-house stay's bill), the issuer's stuck intent (`psp_reference`, which the rail cancels as soon as the chase is recorded), the house's deadline (`authentication_due_by`, `set_sca_chase`: 24 hours unset) and its reminders. The card on file is kept: the page asks again, it replaces nothing. The rail mints the page (`payment_request.requested.recorded`); at a house with the chase on the page goes to the guest when it is recorded. Idempotent: the same purpose already pending for the same amount asks once (naming the latest stuck intent); another amount withdraws the old chase and asks for the new one. Refused when the outcome it follows is not recorded: the fee or the stay's collection awaiting authentication, the re-placed hold refused (`reauthorisation_refused_at`), the stay's top-up refused (`hold_top_up_refusal`). Emits `payment.authentication_pending`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_booking_origin",
          "description": "Record the IP address and device a guest booked from, for a booking they made online themselves. Called by the booking engine at booking time; the chargeback evidence packet reads it to argue a fraud claim under Visa Compelling Evidence 3.0. Refuses any booking whose `source` is not `direct_web`: an OTA booking carries no guest IP, and a desk-keyed card would record the hotel's own. Refuses an address that does not parse as IPv4 or IPv6. A replay with the same values is a no-op; a second origin that disagrees with the first is refused rather than overwriting evidence. Stamps `retain_until` as captured_at plus 485 days."
        },
        {
          "api_name": "hospitality.action.record_deposit_payment",
          "description": "Record a deposit taken before arrival. The money lands on the booking's own account — opened here if this is the first — as an ordinary settled payment, and `deposit_paid_amount` / `prepayment_status` advance on the reservation. At check-in `apply_deposit_to_folio` moves it onto the stay's folio; until then the account is where it is, which is what makes it visible, billable and refundable. Pass `psp_reference` for a deposit taken on a card: without the processor's own reference the payment can be found but not returned, and a refund becomes a ledger gesture instead of money moving."
        },
        {
          "api_name": "hospitality.action.request_payment",
          "description": "Ask the guest for money against this booking (Mews's payment request): mints a `pending` payment request the card rail turns into a hosted pay link. `reason` defaults to `deposit` and `kind` to `payment`; `preauthorization` asks for a hold captured later through the authorise path. `amount` defaults, for a deposit or prepayment, to what the booking still owes (`deposit_required_amount` less `deposit_paid_amount`), and `expires_at` to the booking's `deposit_due_at`; one of each must resolve. Asking for a deposit when nothing is owed, or when a pending request for the same reason and kind already exists, changes nothing — so the booking automation can fire it on every new booking, and a re-delivered event asks once. A pending request is withdrawn by the booking it asks for: cancelling or no-showing the booking cancels it (`reservation_cancelled` / `no_show`), and a modification that re-derives the unpaid deposit cancels a deposit request (`deposit_changed`) without re-asking — the automation or the desk asks again for the new figure. Emits `payment_request.requested.recorded`."
        },
        {
          "api_name": "hospitality.action.revoke_tax_exemption",
          "description": "Stop a booking's tax exemption from now on: stamps the one in force with `revoked_at`. Postings made while it applied keep their (absent) tax — correcting those is a reversal and a re-post, not a rewrite. Refuses a booking with no exemption in force."
        },
        {
          "api_name": "hospitality.action.set_tax_exemption",
          "description": "Record that a booking is exempt from named tax codes — a government or diplomatic guest with a certificate on file (OPERA's tax-exempt profile, held on the booking). From now on every tax line derived for the booking (room nights, day use, forfeited deposits, cancellation and no-show charges) skips a rule whose code is listed; postings already made keep their tax. A booking carries at most one exemption in force: a second call replaces it (the old one is revoked and stays as the trail). Each code must name a tax rule the house has authored."
        },
        {
          "api_name": "hospitality.action.withdraw_authentication_chase",
          "description": "**Card-rail only.** Withdraw this booking's pending authentication chases for `authentication_for` (and, for a post-stay collection, `authentication_stay_ref`) before the card rail takes the same money another way — the desk's Try again on a no-show fee, a departed stay's collection — or because its own hold went on (a re-placed booking hold, a top-up). Each is cancelled (`withdrawal_reason`, default `authentication_taken_another_way`) and its page switched off; a page paid after this lands only where money is still owed, and is otherwise refunded (`record_authentication_page_overpaid`). Nothing pending changes nothing. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        }
      ],
      "api_name": "hospitality.reservation",
      "description": "A confirmed or held intent to occupy one or more rooms over a set of nights. Linked to a property, a primary guest, optionally a company, a rate plan, and a room type.",
      "display_name": "Reservation",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "confirmation_number",
          "description": "",
          "display_name": "Confirmation number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "arrival_date",
          "description": "",
          "display_name": "Arrival date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "departure_date",
          "description": "",
          "display_name": "Departure date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "adults",
          "description": "",
          "display_name": "Adults",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "children",
          "description": "",
          "display_name": "Children",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Source",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "market_segment",
          "description": "",
          "display_name": "Market segment",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "channel_reference",
          "description": "",
          "display_name": "External channel reference",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "visitor_id",
          "description": "",
          "display_name": "Booking engine visitor",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "expected_total",
          "description": "",
          "display_name": "Expected total revenue",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "guarantee",
          "description": "",
          "display_name": "Guarantee",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "special_requests",
          "description": "",
          "display_name": "Special requests",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "booked_at",
          "description": "",
          "display_name": "Booked at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "deposit_required_amount",
          "description": "",
          "display_name": "Deposit required",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "deposit_paid_amount",
          "description": "",
          "display_name": "Deposit paid (accumulating)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "deposit_returned_amount",
          "description": "",
          "display_name": "Deposit returned (accumulating)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "deposit_due_at",
          "description": "",
          "display_name": "Deposit due by",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "prepayment_status",
          "description": "",
          "display_name": "Prepayment status",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "no_show_fee_amount",
          "description": "",
          "display_name": "No-show fee",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "no_show_fee_status",
          "description": "",
          "display_name": "No-show fee status",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "no_show_fee_attempt_card_ref",
          "description": "",
          "display_name": "The stored card an open no-show fee collection is charging — drawn again by its retry whatever the house's merchant account did in between",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "no_show_fee_attempt_open_at",
          "description": "",
          "display_name": "Set while a collection of the no-show fee is open — written before the processor is asked, cleared when its outcome is recorded; an open attempt is asked again under the same key",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "no_show_fee_attempt_provider_card_ref",
          "description": "",
          "display_name": "The processor payment method an open no-show fee collection is charging — the card's `provider_card_ref` when the attempt opened",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "no_show_fee_attempt_provider_customer_ref",
          "description": "",
          "display_name": "The processor customer an open no-show fee collection is charging — the card's `provider_customer_ref` when the attempt opened",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "no_show_posting_basis",
          "description": "",
          "display_name": "No-show posting basis applied",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "no_show_posted_amount",
          "description": "",
          "display_name": "No-show amount posted",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "deposit_policy_ref",
          "description": "",
          "display_name": "Deposit policy (frozen at derivation)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "hold_expires_at",
          "description": "",
          "display_name": "Hold expires at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancellation_policy_ref",
          "description": "",
          "display_name": "Cancellation policy (live ref)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "cancellation_policy_text",
          "description": "",
          "display_name": "Cancellation policy text (snapshot at booking)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cancellation_fee_due_at",
          "description": "",
          "display_name": "Cancellation fee due at (frozen at booking)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancellation_fee_amount",
          "description": "",
          "display_name": "Cancellation fee",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "cancellation_schedule",
          "description": "",
          "display_name": "Cancellation ladder resolved to money (frozen at booking)",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "cancellation_fee_status",
          "description": "",
          "display_name": "Cancellation fee status",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "stay_context_kind",
          "description": "",
          "display_name": "Stay context kind",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "stay_context_notes",
          "description": "",
          "display_name": "Stay context notes",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cancellation_reason",
          "description": "",
          "display_name": "Cancellation reason (set by cancel_reservation)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "no_show_reason",
          "description": "",
          "display_name": "No-show reason (set by mark_reservation_no_show)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cancelled_at",
          "description": "",
          "display_name": "Cancelled at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Source-system key for connector-landed reservations",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "fraud_warning_at",
          "description": "Set by `record_fraud_warning` when an issuer's early fraud warning reaches a payment behind this booking (the payment carries the warning itself). Before arrival the desk decides whether to cancel and refund while the room can still be resold; a voided hold leaves the booking without its guarantee.",
          "display_name": "When an issuer warned of fraud on a payment for this booking",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "fraud_warning_response",
          "description": "",
          "display_name": "What the card rail did about that warning",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "cancellation_fee_attempt_card_ref",
          "description": "",
          "display_name": "The stored card an open cancellation fee collection is charging — drawn again by its retry whatever the house's merchant account did in between",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "cancellation_fee_attempt_open_at",
          "description": "",
          "display_name": "Set while a collection of the cancellation fee is open — written before the processor is asked, cleared when its outcome is recorded; an open attempt is asked again under the same key",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancellation_fee_attempt_provider_card_ref",
          "description": "",
          "display_name": "The processor payment method an open cancellation fee collection is charging — the card's `provider_card_ref` when the attempt opened",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "cancellation_fee_attempt_provider_customer_ref",
          "description": "",
          "display_name": "The processor customer an open cancellation fee collection is charging — the card's `provider_customer_ref` when the attempt opened",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.reservation_night",
      "description": "One night of a `ReservationRoom`. Carries the rate amount, rate plan, and room type *for that specific night* — so a stay across a rate change, a comp night, or a rate-plan switch is naturally representable. Maps to OTA `Rate` elements at `EffectiveDate` granularity.",
      "display_name": "Reservation night",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Reservation night id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "rate_amount",
          "description": "",
          "display_name": "Rate amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "complimentary",
          "description": "",
          "display_name": "Complimentary night",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.assign_room_for_reservation_room",
          "description": "Assign a specific physical room to one `reservation_room` — the multi-room-booking variant of `assign_room`. The room's room_type must match the reservation_room's own reserved room_type, and the parent reservation must be pre-check-in. Re-assigning to a different room is a room move (the prior assignment link is replaced); re-assigning the same room is a no-op."
        },
        {
          "api_name": "hospitality.action.cancel_reservation_room",
          "description": "Cancel one room of a multi-room reservation before arrival. The room goes the way a cancelled booking's rooms go (the same leg cascade `cancel_reservation` runs): status cancelled, its room-nights released to the availability ledger (share-aware), its room assignment cleared. The booking stands with the rest: its guest counts re-derive from the live rooms, it reads `modified`, an unpaid deposit re-derives, and the other rooms and every folio are untouched. Penalty, per room: the booking's frozen cancellation terms are split by this room's share of the booking's room revenue — the room records its part of the fee in force at `cancelled_at`, and the booking keeps the rest of every frozen figure — so the policy is never walked again and the two parts always sum to what was frozen. With `outlet_id`, a non-zero part posts to the booking's own account as a cancellation fee. Refuses the last live room (cancel the reservation), a room in house or checked out, and a booking already in house, cancelled or no-show. Replay-iff-identical on `cancelled_at`. Emits `reservation_room.cancelled.decided`. Guest-impacting and destructive, so agents stage it for human confirmation."
        },
        {
          "api_name": "hospitality.action.change_reservation_occupancy",
          "description": "Change one reservation_room's guest counts — allowed pre-arrival and in-house (an extra guest joining mid-stay is normal operations). The new adults + children must fit the leg's room type max_occupancy. The leg's nights re-price per the required pricing_policy (keep_rate leaves them; requote reads the grid at the new adult count — occupancy-based pricing as a first-class re-price, the Mews gap; override takes the supplied rate with its reason; requote_added_nights_only has no meaning here and is refused). Reservation-level totals re-derive as the sum across legs."
        },
        {
          "api_name": "hospitality.action.change_reservation_room_type",
          "description": "Re-book one reservation_room onto a different room type — per-leg, so a two-room booking upgrades one room without touching the other. Pre-check-in only. Releases the old type's room-nights, consumes the new type's (sold-out nights refuse atomically), re-runs the restriction gate for the new type, re-points reservation_room_for_room_type, and re-prices the leg's nights per the required pricing_policy (requote_added_nights_only has no meaning here and is refused). An assigned physical room of the wrong type is explicitly unassigned — a guest is never left silently booked into a mismatched room — and the reservation-level room-type convenience link re-derives from the legs."
        }
      ],
      "api_name": "hospitality.reservation_room",
      "description": "One physical-room slice of a reservation. A reservation for '3 rooms x 4 nights' has three `ReservationRoom`s. Each carries the booked room type, optional assigned room, and the per-night detail it owns via `reservation_room_has_night`.",
      "display_name": "Reservation room",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Reservation room id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "index",
          "description": "",
          "display_name": "Index within reservation (1-based)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "adults",
          "description": "",
          "display_name": "Adults",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "children",
          "description": "",
          "display_name": "Children",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "day_use_rate",
          "description": "",
          "display_name": "Day-use slot rate",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "cancelled_at",
          "description": "",
          "display_name": "Room cancelled at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancellation_reason",
          "description": "",
          "display_name": "Room cancellation reason",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "cancellation_fee_amount",
          "description": "",
          "display_name": "Room cancellation fee",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "cancellation_fee_status",
          "description": "",
          "display_name": "Room cancellation fee status",
          "kind": "enum",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_restriction_recommendation",
          "description": "Apply a suggested restriction_recommendation: authors the `rate_restriction` row it describes through the same path `set_rate_restriction` takes — links to its scope, the `restriction.set.recorded` feed event the channel wire and the desk board read — with the recommendation's id as the row's replay key, and transitions the recommendation to `applied`. `posted_at` is the business time the restriction goes on the board (defaults to now; must not be in the future). Rejected if the recommendation isn't in `suggested` state or has expired. A `lift` recommendation authors no row: every row standing in exactly its scope over its range is stamped `lifted_at` at `posted_at` and kept, and linked to it; refused when none stands, or when one runs past the range."
        },
        {
          "api_name": "hospitality.action.reject_restriction_recommendation",
          "description": "Decline a suggested restriction_recommendation. Transitions `decision_status` to `rejected` and records an optional free-text reason for override-rate analytics. Rejected if the recommendation isn't in `suggested` state."
        }
      ],
      "api_name": "hospitality.restriction_recommendation",
      "description": "A typed sell-restriction recommendation issued by an agent against a scope (room type, rate plan, or the pair) over a date range: a closure, closed-to-arrival / departure, or a length-of- stay bound, with the reading that motivated it — the rooms left, the protection level, the demand and sell-up the recommender believes — frozen at issue time. Applying it authors a `rate_restriction` row through the same path a hand-posted restriction takes; a `lift` recommendation (the re-open) constrains nothing and its apply lifts the rows standing in its scope over its range, keeping them as history. The decision_* fields mutate exactly once on the transition out of `suggested`.",
      "display_name": "Restriction recommendation",
      "domain": "revenue_management",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Recommendation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "start_date",
          "description": "",
          "display_name": "First night the restriction would cover",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "end_date",
          "description": "",
          "display_name": "Last night the restriction would cover (inclusive)",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "closed",
          "description": "",
          "display_name": "Hard stop-sell recommended",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "closed_to_arrival",
          "description": "",
          "display_name": "Closed to arrival recommended",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "closed_to_departure",
          "description": "",
          "display_name": "Closed to departure recommended",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "min_length_of_stay",
          "description": "",
          "display_name": "Minimum length of stay recommended (nights)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "max_length_of_stay",
          "description": "",
          "display_name": "Maximum length of stay recommended (nights)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "lift",
          "description": "",
          "display_name": "Recommend lifting what stands in the scope over the range (the re-open)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "rooms_remaining",
          "description": "",
          "display_name": "Rooms left to sell on the first night, at issue time",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "protection_level",
          "description": "",
          "display_name": "Rooms the recommender wants kept for the higher rate (the protection level the restriction enforces)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "demand_forecast_rooms",
          "description": "",
          "display_name": "Unconstrained higher-rate demand the recommender believes for the first night, in rooms",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "sell_up_rate",
          "description": "",
          "display_name": "Share of fenced-rate buyers believed to pay the higher rate once the fence closes (0..=1)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "confidence",
          "description": "",
          "display_name": "Operator-facing confidence ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "confidence_score",
          "description": "",
          "display_name": "Calibrated probability (0..=1) — set when the model emits one",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "factors_considered",
          "description": "",
          "display_name": "Signals the recommendation conditioned on (the strategy that fired)",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "Human-readable explanation surfaced to operators",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "forecast_snapshot",
          "description": "",
          "display_name": "Forecast snapshot frozen at issue time (per §B.2)",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the agent emitted the recommendation",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "After this, decision_status moves to expired",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_on",
          "description": "",
          "display_name": "The house business day this lapses after. When set, the house's open business date decides expiry (not the wall clock) and expires_at is the nominal end of that day in the house's timezone",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "decision_status",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the recommendation transitioned out of suggested",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Free-text reason set on reject",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.delist_retail_product",
          "description": "End all active retail_product_available_at_* placements for the target product as of `delisted_at`. Portfolio-wide delisting verb distinct from per-listing effective_to edits."
        }
      ],
      "api_name": "hospitality.retail_product",
      "description": "A SKU-level retail product. Belongs to a retail_product_line and is available at venues / properties via M:N temporal links. Bridges to the venue POS catalogue via `service_item_is_retail_product` (a venue's POS may sell the same SKU as a catalogue item); the two surfaces overlap intentionally so the same physical SKU has one identity across DTC and in-venue channels.",
      "display_name": "Retail product",
      "domain": "brand_venue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "SKU",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Product name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Product kind (mirrors retail_product_line.kind)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "unit_price",
          "description": "",
          "display_name": "Unit price",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "currency",
          "description": "",
          "display_name": "Currency (ISO 4217)",
          "kind": "currency",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "weight_grams",
          "description": "",
          "display_name": "Weight (grams)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "volume_ml",
          "description": "",
          "display_name": "Volume (millilitres)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "image_url",
          "description": "",
          "display_name": "Image URL",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "barcode",
          "description": "",
          "display_name": "Barcode",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "bundle_size",
          "description": "",
          "display_name": "Bundle size (multi-pack SKUs)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "cost_amount",
          "description": "",
          "display_name": "Cost of goods (margin reporting)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "cost_currency",
          "description": "",
          "display_name": "Cost currency (ISO 4217)",
          "kind": "currency",
          "required": false
        },
        {
          "api_name": "is_dtc_only",
          "description": "",
          "display_name": "DTC-only (no in-venue placement)",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.launch_retail_product_line",
          "description": "Stamp the launch date on a retail product line. Operator verb distinct from object-create — a product line may be created during design and launched commercially later."
        },
        {
          "api_name": "hospitality.action.sunset_retail_product_line",
          "description": "Stamp the sunset date on a retail product line. Soft validator (proposal 0004 §7) warns when individual retail_product_available_at_* placements remain active — sunset is upstream of delisting; the placements should be ended in the same operation."
        }
      ],
      "api_name": "hospitality.retail_product_line",
      "description": "A brand-level catalogue of retail products — Cowshed-the-retail-line, Le Labo Santal 33, Edition × Le Labo amenities. Renamed from the pathway's `venue_retail_product_line` because the line is brand-level, not venue-bound — Cowshed exists at Soho House venues, at Liberty London, and at standalone Cowshed shops. Availability is a separate M:N link.",
      "display_name": "Retail product line",
      "domain": "brand_venue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Retail product line code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Retail product line name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Retail product line kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "partner_brand_text",
          "description": "",
          "display_name": "Co-brand partner (free-text; partners may not be Aerandil tenants)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "launched_on",
          "description": "",
          "display_name": "Launched on",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "sunset_on",
          "description": "",
          "display_name": "Sunset on (null when current)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "website_url",
          "description": "",
          "display_name": "Website URL",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.create_housekeeping_task",
          "description": "Mint an ad-hoc housekeeping task against a room — a deep clean, an inspection, a guest-requested refresh. Carries no generation_key, so the generator's convergence sweep never touches it. Refuses a room under an active out-of-order block covering the service date (release decides the state the room re-enters in). `credits` and `standard_minutes` come from the property's most specific active housekeeping credit rule for the task type and the room's type, unless given here. `assignee_id` assigns it in the same write (a staff member at the room's house), so an inspection a supervisor starts is theirs from the first moment rather than on every attendant's open list; a housekeeper or runner session may name only itself — giving work to someone else is the supervisor's assign_housekeeping_task. Stamps `origin` manual (from_triage when from_triage_id is given). A room takes a room's task types only; a public area's work is create_public_area_task's (2026-10-14). A task is a request (2026-10-15): `requested_by` says who asked — when unstated, guest for a named `stay_id` (linked, `housekeeping_task_requested_by_stay`) or `from_triage_id`, else the caller's side: floor for a housekeeper's, runner's, supervisor's or engineer's handset, desk for any other person, automation for an agent or a service. A floor session states only floor — a guest's request is the desk's or triage's to record (403). `due_at` is when it is due, no earlier than the start of the day before the house's hotel date; with no `business_date` the task belongs to its due time's day. Its urgency is the housekeeping_requests read's."
        },
        {
          "api_name": "hospitality.action.create_work_order",
          "description": "Open a maintenance work order against a room, optionally assigning an engineer on the spot. When the room is physically unusable, declare the out-of-order window here (out_of_order_start / out_of_order_end / return_status): the order then holds a real room_status_block minted through the same path as take_room_out_of_order — ledger weight, in-house-guest refusal, conflicting-assignment work list and all — and complete_work_order releases it. Never a severity-driven side effect: downtime is always an explicit declaration. Faults by element (2026-10-15): `element` names the thing in the room that failed (the attendant's word); `fault_type_id` names an entry of the house's or its brand's catalogue and fills `title`, `category`, `severity` and `element` wherever the call states none; with no category from either, the element's trade is the category; with no title, the element's name is the title. An order already open on the room under the same title (case and spacing aside) or the same fault type is that order — the double fault hubOS lists before a new one is raised: nothing is minted, the response carries a `work-order-open-twin` warning naming it, and `work_order.reported_again` records the second report. Two orders naming different assets are not twins; `allow_duplicate` raises another anyway. What the call declares beyond the report lands on the twin (2026-10-17): a declared out-of-order window blocks the room through it when it holds no block (one it holds that already covers the window is said, any other is refused naming it — an order holds one block), and a `staff_member_id` takes it when nobody holds it (a holder it has keeps it — assign_work_order re-points); the warning says which, and `work_order.reported_again` carries `block_id` and `assignee_id`. A fault in a public area is `create_public_area_work_order`."
        },
        {
          "api_name": "hospitality.action.define_maintenance_plan",
          "description": "Put a room on a preventive-maintenance cadence: the work order to raise (title, category, severity, description), every `interval_days`, `fixed` to the calendar or `floating` from the last completion (UpKeep's and Maximo's switch), first due on `first_due_on`. The plan starts `scheduled`; the automation lane raises its order when the date comes. Emits `maintenance_plan.defined`."
        },
        {
          "api_name": "hospitality.action.set_bed_configuration",
          "description": "Record how the room's convertible beds are made up now — joined (one double or king) or split (two singles) — hubOS's beds-configuration write; or clear it when `bed_configuration` is absent (beds that no longer convert). The room's state, which the floor moves when it splits or joins the beds and the desk when a guest asks; the room type's free-text `bed_configuration` is what the type is sold as and is not touched. Any read of the room — the rack, a task's room — carries it. Setting the value the room already carries is a no-op. Emits `room.bed_configuration_set` with the value it replaced."
        },
        {
          "api_name": "hospitality.action.set_room_housekeeping_status",
          "description": "The attendant's one-call report (OPERA's putRoomRelatedStatus): set the cleaning status, the physically observed occupancy, and/or the observed occupant count — at least one. Observed occupancy and person counts compare against system truth on the spot: a mismatch raises a skip / sleep / person room_discrepancy, and a corrected observation auto-resolves what an erroneous one opened. Setting the cleaning status on a room under an active out-of-order block refuses (release decides the status the room re-enters in); observations alone are always allowed."
        },
        {
          "api_name": "hospitality.action.set_room_reader_code",
          "description": "Set the code the room's QR / NFC reader carries — what the handset scans at the door to open the room's card (hubOS's `hk_reader_code`) — or clear it when `reader_code` is absent or blank. Unique within the property: a code another room — or a public area (2026-10-14) — of the house already carries is refused, and two first writes of one code queue on a per-house lock, so the second reads the first. Setting the code the room already carries is a no-op. The object search finds a room by it (an `eq` filter on `reader_code`, narrowed to the house). Emits `room.reader_code_set`."
        },
        {
          "api_name": "hospitality.action.take_room_out_of_order",
          "description": "Block a room from inventory for a date window — burst pipe, refurbishment, seasonal closure. Deducts the room type's availability ledger for the blocked dates (the booking ceiling is sellable - out_of_order + oversell_limit) and records exactly which rows it touched, so release restores exactly those; dates already sold past the reduced ceiling are reported as displaced (overbooking exposure), never silently absorbed. Refuses while a guest is (or will still be) in the room — move_room first — and reports pre-check-in assignments into the window as a work list instead of silently unassigning them (check-in refuses an out-of-order room until each is re-roomed). Overlapping active blocks refuse; extend or release the existing block instead."
        },
        {
          "api_name": "hospitality.action.take_room_out_of_service",
          "description": "Flag a room for a date window without touching inventory — a cosmetic issue, preventive maintenance. The room stays sellable and assignable (the OPERA out-of-service semantics; it remains in the occupancy denominator), so this is an advisory signal for housekeeping and the desk, not a gate. Same block lifecycle as out-of-order: a required reason, an inclusive through-date the night audit honours, and a declared return_status."
        }
      ],
      "api_name": "hospitality.room",
      "description": "A physical room. Rooms always belong to a property and are linked to a room type. State is three orthogonal axes: housekeeping_status (the cleaning axis), occupancy (system truth, written by check-in / check-out / room moves), and the attendant-observed occupancy pair whose disagreement with system occupancy derives a room_discrepancy. Out-of-order / out-of-service are dated room_status_block records, not statuses here.",
      "display_name": "Room",
      "domain": "inventory",
      "interfaces": [],
      "properties": [
        {
          "api_name": "number",
          "description": "",
          "display_name": "Room number",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "floor",
          "description": "",
          "display_name": "Floor",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "housekeeping_status",
          "description": "",
          "display_name": "Housekeeping status (cleaning axis)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "occupancy",
          "description": "",
          "display_name": "System occupancy (front-office truth)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "observed_occupancy",
          "description": "",
          "display_name": "Attendant-observed occupancy (discrepancy comparator)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "observed_persons",
          "description": "",
          "display_name": "Attendant-observed occupant count",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "observed_at",
          "description": "",
          "display_name": "When the observation was reported",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "connecting_to",
          "description": "",
          "display_name": "Connecting to room number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "features",
          "description": "",
          "display_name": "Features",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Vendor-alignment room code (tenant-unique; connector mappings resolve by it)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "reader_code",
          "description": "",
          "display_name": "The code the room's QR / NFC reader carries (unique within the property)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "bed_configuration",
          "description": "",
          "display_name": "How the room's convertible beds are made up now: joined (one double or king) or split (two singles); absent when they do not convert or nobody recorded it",
          "kind": "enum",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.resolve_room_discrepancy",
          "description": "The front-desk verdict on a skip / sleep / person investigation: walk-out confirmed, unregistered occupant registered, observation error, or corrected by the front office. statuses_realigned is reserved for the system's own auto-resolution and refused here. Replaying the same verdict is a no-op; a closed investigation never changes its verdict."
        }
      ],
      "api_name": "hospitality.room_discrepancy",
      "description": "A derived disagreement between system occupancy and what the attendant physically found (OPERA's skip / sleep / person model, plus the resolution audit it lacks): `skip` — system occupied, found vacant; `sleep` — system vacant, found occupied; `person` — occupant count mismatch. Raised and auto-resolved by set_room_housekeeping_status when the views diverge and re-agree; front-desk verdicts land through resolve_room_discrepancy with a typed outcome, so every discrepancy closes with a recorded why.",
      "display_name": "Room discrepancy",
      "domain": "inventory",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Discrepancy id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Discrepancy kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "system_occupancy",
          "description": "",
          "display_name": "System occupancy at raise time",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "observed_occupancy",
          "description": "",
          "display_name": "Observed occupancy at raise time",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "expected_persons",
          "description": "",
          "display_name": "Occupants per the in-house reservation",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "observed_persons",
          "description": "",
          "display_name": "Occupants the attendant counted",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "raised_at",
          "description": "",
          "display_name": "Raised at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "resolved_at",
          "description": "",
          "display_name": "Resolved at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "outcome",
          "description": "",
          "display_name": "How the investigation closed",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Resolution notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.cancel_room_queue_entry",
          "description": "Withdraw the wait — the guest left for lunch, took another room, or the desk queued in error. Refuses a fulfilled entry (check-in already answered it); re-cancelling is a no-op."
        }
      ],
      "api_name": "hospitality.room_queue_entry",
      "description": "A reservation waiting at the desk for its room to turn — OPERA's queue room as a first-class object. Created by `queue_reservation_for_room` (which also rush-bumps the assigned room's open housekeeping task to urgent), fulfilled by `check_in` itself the moment the guest gets the room, or cancelled by the desk. Carries no manual priority: urgency derives from the reservation (arrival time, wait duration) and the rush signal lives on the task, so an agent working the queue reads state instead of maintaining a second copy of it.",
      "display_name": "Room queue entry",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Queue entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "queued_at",
          "description": "",
          "display_name": "When the guest joined the queue",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "fulfilled_at",
          "description": "",
          "display_name": "When check-in handed the guest the room",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancelled_at",
          "description": "",
          "display_name": "When the desk withdrew the wait",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancel_reason",
          "description": "",
          "display_name": "Why the wait was withdrawn",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Desk notes (where the guest is waiting, luggage)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.return_room_to_service",
          "description": "Release an active block early (the night audit releases through-date blocks on its own). Targets the block — a release is a statement about that window. Restores the availability rows the block itself decremented for dates from release_date on (lapsed nights stay counted: they were genuinely out of order), and the room re-enters the block's declared return_status unless overridden here. Releasing a released block is an idempotent no-op."
        }
      ],
      "api_name": "hospitality.room_status_block",
      "description": "A dated out-of-order or out-of-service record against one room — the time-boxed inventory fact OPERA and Mews both model underneath their status enums. `out_of_order` deducts the blocked dates from the room type's sellable inventory (availability_day.out_of_order) and refuses assignment; `out_of_service` is an advisory flag with no inventory effect. `return_status` names the housekeeping status the room re-enters on release — declared at creation, so a block can never expire into an undefined state. `ledger_dates` records exactly which availability rows the block decremented (release restores exactly those); `displaced_dates` records nights where the deduction pushed sold past the new ceiling — overbooking exposure reported, never silently absorbed.",
      "display_name": "Room status block",
      "domain": "inventory",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Block id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Block kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "start_date",
          "description": "",
          "display_name": "First blocked night",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "end_date",
          "description": "",
          "display_name": "Last blocked night (inclusive through-date)",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Reason",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "remarks",
          "description": "",
          "display_name": "Remarks",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "return_status",
          "description": "",
          "display_name": "Housekeeping status the room re-enters on release",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "blocked_at",
          "description": "",
          "display_name": "When the block was recorded",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "released_at",
          "description": "",
          "display_name": "When the block was released",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "ledger_dates",
          "description": "",
          "display_name": "Dates whose availability rows this block decremented",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "displaced_dates",
          "description": "",
          "display_name": "Blocked dates already sold past the reduced ceiling",
          "kind": "array",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.room_type",
      "description": "A category of rooms sharing layout, rate, and amenities. Reservations are typically booked against a room type and only assigned to a specific room at check-in.",
      "display_name": "Room Type",
      "domain": "inventory",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Room type code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Room type name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "max_occupancy",
          "description": "",
          "display_name": "Max occupancy",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "base_rate",
          "description": "",
          "display_name": "Base rate",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "amenities",
          "description": "",
          "display_name": "Amenities",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "view",
          "description": "",
          "display_name": "View",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "bed_configuration",
          "description": "",
          "display_name": "Bed configuration",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "size_sqm",
          "description": "",
          "display_name": "Size (m²)",
          "kind": "decimal",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_round_template",
          "description": "Stop a round template falling due: the sweep starts no more rounds of it, and its name is free for a new template. A round already under way stays open for its walker to complete or cancel. Retiring a retired template is a no-op. Emits `round_template.retired`."
        },
        {
          "api_name": "hospitality.action.start_inspection_round",
          "description": "Start a round from an active template — by hand, or the automation sweep's effect when the template falls due. The round carries the template's name, family, the due date it answers (`due_on`, the template's `next_due_on`) and its checkpoints as they stand now; it starts `open`, `started_by` the principal (and linked to the staff member behind a handset session), and the template's `open_round_id` names it. One open round per template: while one is open, the same caller starting again — or the same `round_id` — is a replay that writes nothing, and anyone else is refused, naming the open round. `round_id` is the round's id when the caller mints it (a v4 or v7 UUID — a handset starting a walk with no signal records its checkpoints against it before the start lands); the same id on another template is refused as a duplicate. Refuses a retired template. `template_revision` (2026-10-17) is the template's `revision` the caller saw — a handset queueing a start names the walk it shows: when the template has been revised since, the start is refused (409 `version_conflict`, `expected_version` the stated revision and `current_version` the template's) and nothing opens, so readings queued against the old positions never land on moved checkpoints; a start already landed under the same `round_id` is a replay whatever it names. The round records the revision it copied (`template_revision`). Emits `inspection_round.started`. A person's start under a minted `round_id` takes over a round the automation sweep opened (`started_by` an automation) that nothing has been recorded on yet (2026-10-17) — the walk queued on a handset that was offline when the template fell due: the sweep's round is cancelled (`cancel_reason` naming the walk that took it; `inspection_round.cancelled` with `taken_over_by`), the caller's round opens for the same due date and stands (`inspection_round.started` with `took_over`; the response lists both and an `inspection-round-taken-over` warning names them). A sweep's round with a checkpoint recorded is someone's walk and is not taken: the start is refused, naming it, as any other open round."
        }
      ],
      "api_name": "hospitality.round_template",
      "description": "A house's planned walk — hubOS's revision structure: a name, a `family` (corrective, preventive, technical_legal, inspection_legal), the maintenance plan's cadence (`fixed` to the calendar or `floating` from the last completion, every `interval_days`) and its next due date, and the `checkpoints` in order — each a room or a piece of equipment of this house (`ref_id`), or a general place named by its `label`, with the items checked there (`check`, `reading` with its unit and optional `min` / `max`, `text`, `photo`, and `section` headings). One active template per house and name: defining the name again revises it in place. The automation lane is its clock: a sweep over active templates whose due instant has come (`next_due_at`, the house's midnight starting `next_due_on`) and that have no round open starts one (`start_inspection_round`); completing the round moves `next_due_on` on. `open_round_id` names the round in progress; `revision` counts the walks it has defined (a re-definition that moves the checkpoints is the next), and a start may name the one it saw.",
      "display_name": "Round template",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Round template id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "What the walk is called — unique among the house's active templates",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "What the walk is for, in words",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "family",
          "description": "",
          "display_name": "corrective, preventive, technical_legal or inspection_legal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "cadence",
          "description": "",
          "display_name": "Fixed to the calendar, or floating from the last completion",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "interval_days",
          "description": "",
          "display_name": "Days between rounds",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "next_due_on",
          "description": "",
          "display_name": "The date the next round is due",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "next_due_at",
          "description": "",
          "display_name": "The instant the house's day reaches next_due_on — its local midnight (UTC midnight when the house names no timezone); what the start sweep compares with the lane's clock",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "checkpoints",
          "description": "",
          "display_name": "The checkpoints, in walking order, each with its items",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Active (falls due) or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "open_round_id",
          "description": "",
          "display_name": "The round of this template in progress, when one is — the sweep starts none while it is set",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "last_completed_on",
          "description": "",
          "display_name": "The property-local date its last round was completed",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the template was retired",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "revision",
          "description": "",
          "display_name": "Which revision of the walk this is — 1 when defined, one more each time a re-definition moves its checkpoints (words and schedule do not); absent on a template defined before revisions, which reads as 1. start_inspection_round's template_revision is checked against it",
          "kind": "int",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_sell_limit_recommendation",
          "description": "Apply a suggested sell_limit_recommendation: on every availability_day row of its room type over its range, `oversell_limit` moves to the recommended headroom — or, for a `reset`, back to the row's `declared_oversell_limit` — through the overlay `set_availability` writes, the declaration kept beside it. Refused, nothing written, when a night has no ledger row, when the headroom exceeds a row's `oversell_ceiling` (the declared limit where none is set), or when a row has already sold past the limit it would set (the ledger never holds `sold` above its ceiling; a lower limit stops the selling, it cannot unsell). Links the rows moved, emits `sell_limit.applied.recorded`, and transitions the recommendation to `applied`. `posted_at` is the business time the limit takes effect (defaults to now; must not be in the future). Rejected if the recommendation isn't in `suggested` state or has expired."
        },
        {
          "api_name": "hospitality.action.reject_sell_limit_recommendation",
          "description": "Decline a suggested sell_limit_recommendation. Transitions `decision_status` to `rejected` and records an optional free-text reason for override-rate analytics; the ledger is untouched. Rejected if the recommendation isn't in `suggested` state."
        }
      ],
      "api_name": "hospitality.sell_limit_recommendation",
      "description": "A typed overbooking recommendation issued by an agent against one room type over a date range: the oversell headroom the type may sell past its sellable rooms on every night of the range (`oversell_limit`), or the explicit `reset` to the operator's declared limit — with the reading behind it frozen at issue: the rooms on the books, the share of them believed to arrive, the walk cost and the marginal rate the limit balances, and the walks expected at it. Applying it moves each ledger row's `oversell_limit` through the overlay `set_availability` writes, bounded by the row's `oversell_ceiling` (the declared limit where none is set) and by what is already sold. The decision_* fields mutate exactly once on the transition out of `suggested`.",
      "display_name": "Sell-limit recommendation",
      "domain": "revenue_management",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Recommendation id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "start_date",
          "description": "First night the limit would cover",
          "display_name": "First night",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "end_date",
          "description": "Last night the limit would cover (inclusive)",
          "display_name": "Last night",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "oversell_limit",
          "description": "Oversell headroom recommended over the sellable rooms on every night of the range (rooms; absent on a reset)",
          "display_name": "Oversell limit",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "reset",
          "description": "Recommend returning the limit to the operator's declared one",
          "display_name": "Reset",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "rooms_on_books",
          "description": "Rooms of the type on the books for the first night, at issue time",
          "display_name": "Rooms on books",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "show_rate",
          "description": "Share of the rooms on the books the recommender believes will arrive on the first night (0..=1)",
          "display_name": "Show rate",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "walk_cost",
          "description": "What the recommender believes walking one arriving guest costs the house, past the night's own revenue",
          "display_name": "Walk cost",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "marginal_rate",
          "description": "The nightly rate one more sale on the first night would earn",
          "display_name": "Marginal rate",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "expected_walks",
          "description": "Guests the recommender expects to walk on the first night at the recommended limit",
          "display_name": "Expected walks",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "confidence",
          "description": "",
          "display_name": "Operator-facing confidence ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "confidence_score",
          "description": "",
          "display_name": "Calibrated probability (0..=1) — set when the model emits one",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "factors_considered",
          "description": "",
          "display_name": "Signals the recommendation conditioned on",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "Human-readable explanation surfaced to operators",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "forecast_snapshot",
          "description": "",
          "display_name": "Forecast snapshot frozen at issue time (per §B.2)",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "When the agent emitted the recommendation",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "After this, decision_status moves to expired",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_on",
          "description": "",
          "display_name": "The house business day this lapses after. When set, the house's open business date decides expiry (not the wall clock) and expires_at is the nominal end of that day in the house's timezone",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "decision_status",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the recommendation transitioned out of suggested",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "applied_oversell_limit",
          "description": "The oversell headroom the apply wrote on the first night (for a reset, the declared limit it restored there)",
          "display_name": "Applied oversell limit",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Free-text reason set on reject",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.service_area",
      "description": "A named region of an outlet's floor — main dining, patio, bar, private room. The floor-plan render groups tables by area. Configuration: CRUD-authored, linked to its outlet via service_area_of_outlet.",
      "display_name": "Service area",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Area id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Area name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Area kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Active",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.service_item",
      "description": "A saleable item or service offered by a venue: a menu item, a spa treatment, an activity booking, a retail product. Charges link to service items so revenue can be analysed by venue and category.",
      "display_name": "Service item",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Item code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Item name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "Category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "price",
          "description": "",
          "display_name": "Price",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "tax_inclusive",
          "description": "",
          "display_name": "Tax inclusive",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.service_philosophy",
      "description": "A property's typed service framework along nine ordinal axes plus its narrative statement and audit-target context. The framework decomposes patterns observed across Ritz-Carlton, Four Seasons, Aman, Soho House, Sydell, Maybourne, USHG, and the wider luxury-hospitality canon — types the difference between operators that look similar on a single axis but operate differently in practice (a Connaught bar and a NoMad bar both read 'warm-informal' on a one-dimensional axis but decompose to observably-different registers across surface, address form, and personality presentation). Recovery culture is deliberately not an axis — it's an outcome of the other axes plus the recovery toolkit, not an input the operator self-scores.",
      "display_name": "Service philosophy",
      "domain": "service_philosophy",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Philosophy id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "anticipatory_responsive",
          "description": "",
          "display_name": "Anticipatory ↔ responsive",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "empowered_scripted",
          "description": "",
          "display_name": "Empowered ↔ scripted",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "surface_register_default",
          "description": "",
          "display_name": "Surface register default",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "address_form_default",
          "description": "",
          "display_name": "Address form default",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "personality_presentation",
          "description": "",
          "display_name": "Personality presentation",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recognition_depth",
          "description": "",
          "display_name": "Recognition depth",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "privacy_visibility_default",
          "description": "",
          "display_name": "Privacy ↔ visibility default",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "hospitality_service_emphasis",
          "description": "",
          "display_name": "Hospitality ↔ service emphasis",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "ex_cx_priority",
          "description": "",
          "display_name": "EX → CX priority (Meyer hierarchy stance)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "audit_target",
          "description": "",
          "display_name": "Audit target",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "philosophy_statement_text",
          "description": "",
          "display_name": "Philosophy narrative",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "recovery_culture_intent",
          "description": "",
          "display_name": "Recovery culture intent (self-declared)",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.service_recovery_event",
      "description": "An instance of service recovery — failure, recovery action, comp authority used, follow-up. The unit of pattern detection: the same failure logged repeatedly against one room reveals an HVAC issue; against one staff member, a training need; against one venue at one time-of-day, a capacity / staffing pattern. The USHG 5 As stages-completed set surfaces structural process gaps (the operator that consistently apologises but never reaches additional generosity is structurally different from the one that consistently acts but rarely apologises). The comp_authority_snapshot freezes the toolkit's role rules at recovery time so a later toolkit edit doesn't retroactively re-evaluate whether the recovery was within authority. Soft validators on over-limit comp warn but don't block — over-limit recoveries are the data the operator most needs and would otherwise hide.",
      "display_name": "Service recovery event",
      "domain": "service_philosophy",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Recovery event id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "failure_category",
          "description": "",
          "display_name": "Failure category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "severity",
          "description": "",
          "display_name": "Severity",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "failure_subject_text",
          "description": "",
          "display_name": "What went wrong",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "recovery_action_category",
          "description": "",
          "display_name": "Recovery action category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recovery_process_stages_completed",
          "description": "",
          "display_name": "USHG 5 As stages completed",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "comp_amount",
          "description": "",
          "display_name": "Comp amount",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "comp_category",
          "description": "",
          "display_name": "Comp category",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "comp_authority_policy_ref",
          "description": "",
          "display_name": "Comp authority policy bound at recovery time (structured ref)",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "comp_authority_snapshot",
          "description": "",
          "display_name": "Comp authority snapshot at recovery time",
          "kind": "struct",
          "required": true
        },
        {
          "api_name": "recovery_authoriser",
          "description": "",
          "display_name": "Authoriser principal",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "time_to_first_response_seconds",
          "description": "",
          "display_name": "Seconds until first response",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "time_to_resolution_seconds",
          "description": "",
          "display_name": "Seconds until resolved",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "interactional_quality",
          "description": "",
          "display_name": "How the recovery landed",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "happened_at",
          "description": "",
          "display_name": "When the failure happened",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "follow_up_due_at",
          "description": "",
          "display_name": "Follow-up due date",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "follow_up_completed_at",
          "description": "",
          "display_name": "Follow-up completed at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "follow_up_outcome",
          "description": "",
          "display_name": "Follow-up outcome",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_pattern_remediation",
          "description": "Apply a suggested service_recovery_pattern by recording the remediation actually taken. For chain_typed_action the host application invokes the typed action separately and passes applied_action_correlation_id; the handler does not recursively invoke the action engine."
        },
        {
          "api_name": "hospitality.action.escalate_via_pattern_assessment",
          "description": "Escalate a suggested service_recovery_pattern to a human decision-maker rather than applying or rejecting it directly, optionally routing to a named staff member."
        },
        {
          "api_name": "hospitality.action.record_pattern_recurrence_outcome",
          "description": "Record whether the failure pattern recurred in an observation window after remediation — the outcome signal that closes the detector's trust-formation loop."
        },
        {
          "api_name": "hospitality.action.reject_pattern_assessment",
          "description": "Reject a suggested service_recovery_pattern — the operator judges the detected recurring failure not real or not worth acting on. Terminal transition out of suggested."
        }
      ],
      "api_name": "hospitality.service_recovery_pattern",
      "description": "A property-scoped recurring-service-failure pattern detected by an agent or model. The subject, evidence snapshot, hypotheses, severity, confidence, and recommended remediation are frozen at issue time. Decision, applied, escalation, rejection, supersedence, and recurrence fields mutate through explicit lifecycle actions.",
      "display_name": "Service recovery pattern",
      "domain": "recovery_pattern_detection",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Pattern id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "subject_kind",
          "description": "",
          "display_name": "What operational subject the pattern clusters",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "subject_id",
          "description": "",
          "display_name": "Opaque id of the operational subject within the property",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "property_ref",
          "description": "",
          "display_name": "Property anchor for the pattern",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "venue_ref",
          "description": "",
          "display_name": "Optional venue anchor when the pattern is venue-scoped",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "detection_window_start",
          "description": "",
          "display_name": "Start of the frozen detection window",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "detection_window_end",
          "description": "",
          "display_name": "End of the frozen detection window",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "pattern_kind",
          "description": "",
          "display_name": "Detected pattern class",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "event_count",
          "description": "",
          "display_name": "Number of clustered events",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "severity_band",
          "description": "",
          "display_name": "Operator-facing severity band",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "severity_score",
          "description": "",
          "display_name": "Optional calibrated severity score [0, 1]",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "failure_categories_observed",
          "description": "",
          "display_name": "Observed service-failure categories",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "failure_subject_themes",
          "description": "",
          "display_name": "Free-text theme labels for the clustered subject",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "vendor_id",
          "description": "",
          "display_name": "Detector vendor identity",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "vendor_model_version",
          "description": "",
          "display_name": "Detector model version when exposed",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "confidence",
          "description": "",
          "display_name": "Operator-facing confidence ordinal",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "confidence_score",
          "description": "",
          "display_name": "Optional calibrated confidence score [0, 1]",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "factors_considered",
          "description": "",
          "display_name": "Signals considered by the detector",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "root_cause_hypotheses",
          "description": "",
          "display_name": "Structured root-cause hypotheses",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "recommended_remediation_kind",
          "description": "",
          "display_name": "Recommended remediation class",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "recommended_remediation_action_ref",
          "description": "",
          "display_name": "API name of the host-orchestrated chained action, when applicable",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "recommended_remediation_params",
          "description": "",
          "display_name": "Proposed chained-action parameters or workflow payload",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "rationale_text",
          "description": "",
          "display_name": "Human-readable explanation",
          "kind": "text",
          "required": true
        },
        {
          "api_name": "pattern_context_snapshot",
          "description": "",
          "display_name": "Frozen context snapshot captured at issue time",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "issued_at",
          "description": "",
          "display_name": "Issue time",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "Expiry time",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "decision_status",
          "description": "",
          "display_name": "Lifecycle state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "decided_at",
          "description": "",
          "display_name": "When the pattern left suggested state",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "decided_by",
          "description": "",
          "display_name": "stable_id of the deciding principal",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "applied_remediation_kind",
          "description": "",
          "display_name": "Remediation class actually applied",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "applied_action_correlation_id",
          "description": "",
          "display_name": "Correlation id of host-orchestrated chained remediation",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "escalation_reason",
          "description": "",
          "display_name": "Reason recorded on escalation",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "escalation_target_staff_ref",
          "description": "",
          "display_name": "Optional named staff member assigned on escalation",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "rejection_reason",
          "description": "",
          "display_name": "Free-text reason on reject",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "superseded_by_pattern_ref",
          "description": "",
          "display_name": "Newer pattern that supersedes this one",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "policy_outcomes",
          "description": "",
          "display_name": "Compliance and operational policy flags observed during detection",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "recurrence_outcome",
          "description": "",
          "display_name": "Observed recurrence state after remediation",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "recurrence_outcome_window_start",
          "description": "",
          "display_name": "Start of recurrence measurement window",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "recurrence_outcome_window_end",
          "description": "",
          "display_name": "End of recurrence measurement window",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "recurrence_outcome_event_count",
          "description": "",
          "display_name": "Event count in recurrence measurement window",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "pattern_payload_hash",
          "description": "",
          "display_name": "Optional SHA-256 of canonical detector payload",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.service_register",
      "description": "The register dialect that applies inside a specific venue — front-desk discreet-professional, signature-restaurant formal-warm, cocktail-bar crafted-theatre, spa focused-coaching. Inherits unstated axes from the property's `service_philosophy` defaults (the philosophy's `*_default` fields seed the register's matching fields when a venue-scoped override isn't recorded). When Tier 1.4 lands, the register may inherit from a `venue_brand` instead; the Property → Venue inheritance is fine for §1.2 alone (proposal §10 open question 7).",
      "display_name": "Service register",
      "domain": "service_philosophy",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Register id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "surface_register",
          "description": "",
          "display_name": "Surface register",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "address_form",
          "description": "",
          "display_name": "Address form",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "speech_rhythm",
          "description": "",
          "display_name": "Speech rhythm",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "recommendation_style",
          "description": "",
          "display_name": "Recommendation style",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "personality_presentation",
          "description": "",
          "display_name": "Personality presentation",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "body_language_norm",
          "description": "",
          "display_name": "Body language norm",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "register_spec_text",
          "description": "",
          "display_name": "Register narrative",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.clear_table",
          "description": "The table is bussed and reset: a `dirty` service table — marked dirty when its check settled — goes back to `available`, so the floor plan can seat it again (Toast's and Simphony's table status after bussing). Idempotent on an available table; any other status (seated, reserved, blocked) is refused — a seated table clears by settling its check."
        }
      ],
      "api_name": "hospitality.service_table",
      "description": "A seatable unit within a service area — table, booth, bar-stool group. Carries capacity, a stable label, and an x/y position for the floor-plan render. Status follows the industry floor vocabulary: open_check seats it, settle_check marks it dirty, and dirty → available is a bussing update on the CRUD surface.",
      "display_name": "Service table",
      "domain": "fnb",
      "interfaces": [],
      "properties": [
        {
          "api_name": "label",
          "description": "",
          "display_name": "Table label",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "capacity",
          "description": "",
          "display_name": "Seats",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Floor status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "position_x",
          "description": "",
          "display_name": "Floor-plan x position",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "position_y",
          "description": "",
          "display_name": "Floor-plan y position",
          "kind": "decimal",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.separate_share",
          "description": "Break a share group: every member becomes an independent reservation again. Each date covered by k members re-consumes k-1 room-nights through the guarded availability path, so separating into a sold-out house refuses atomically instead of overselling silently. Pre-arrival members only — an in-house break forces a room move (deferred). Rates are not restored after a split; re-pricing is an explicit modify_reservation_dates requote/override. Membership links are end-dated, keeping the share as auditable history; separating an already separated group is a no-op."
        }
      ],
      "api_name": "hospitality.share_group",
      "description": "Two or more reservations sharing one physical room, each with its own account (OPERA's share). One room-night is deducted per covered date regardless of member count; members check in and out independently. Distribution `full` keeps every member's booked rates; `split` divides the primary's rate per shared night (remainder to the joiner). Terminal status `separated` re-establishes the members as independent reservations, re-consuming their availability with a live sellability check.",
      "display_name": "Share group",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Share group id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "distribution",
          "description": "",
          "display_name": "Rate distribution",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "primary_reservation_ref",
          "description": "",
          "display_name": "Primary reservation",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "combined_at",
          "description": "",
          "display_name": "Combined at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "separated_at",
          "description": "",
          "display_name": "Separated at",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.spa_service",
      "description": "A bookable treatment offering: price, service duration, and the provider-blocking buffer pair — pre time (room setup) and clean-up time (turnover) — that every major spa system models (Book4Time Pre Time / Clean-up Time, Zenoti recovery_time, Mindbody PrepTime / FinishTime). All three are snapshotted onto the appointment at booking, so catalogue edits never rewrite booking history. Distinct from `service_item` (POS catalogue row, no duration or resourcing) and from `wellness_modality` (taxonomy + licensing, proposal 0005) — the optional spa_service_for_modality link carries the clinical-licensing gate into book_appointment.",
      "display_name": "Spa service",
      "domain": "spa",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Service code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Service name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Treatment taxonomy (ISPA / CBRE reporting mix)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "price",
          "description": "",
          "display_name": "List price (snapshotted onto the appointment at booking)",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "duration_minutes",
          "description": "",
          "display_name": "Service duration in minutes (guest-facing treatment time)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "pre_time_minutes",
          "description": "",
          "display_name": "Provider-blocking setup time before the service (room prep)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "cleanup_time_minutes",
          "description": "",
          "display_name": "Provider-blocking turnover time after the service (linen / sanitise)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "couples",
          "description": "",
          "display_name": "Couples service (two guests, one capacity-2 room, two appointments)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Bookable",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Guest-facing description",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.revoke_staff_certification",
          "description": "Revoke a certification grant (stamps revoked_at + reason). Non-destructive — the row stays as history."
        },
        {
          "api_name": "hospitality.action.verify_staff_certification",
          "description": "Sign off a certification grant as verified, stamping the verifier principal and timestamp. Idempotent on an already-verified grant."
        }
      ],
      "api_name": "hospitality.staff_certification",
      "description": "A credential grant held by a staff member — the certification kind, issuing authority, issue / expiry dates, and a verification sign-off. Renewals append a new grant; the current one is the latest non-revoked, non-expired row of its kind.",
      "display_name": "Staff certification",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Certification id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Certification kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "issuing_authority",
          "description": "",
          "display_name": "Issuing authority (e.g. ServSafe, WSET)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "credential_reference",
          "description": "",
          "display_name": "Certificate / badge number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "issue_date",
          "description": "",
          "display_name": "Issue date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "expiry_date",
          "description": "",
          "display_name": "Expiry date (null = non-expiring)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "verification_status",
          "description": "",
          "display_name": "Verification status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "verified_by",
          "description": "",
          "display_name": "Verifier (staff principal)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "verified_at",
          "description": "",
          "display_name": "Verified at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "revoked_at",
          "description": "",
          "display_name": "Revoked at (null = active)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "revoked_reason",
          "description": "",
          "display_name": "Revocation reason",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.close_staff_employment_period",
          "description": "Close the staff member's current open employment period. Updates `staff_member.active = false` when no successor period is opened in the same transaction."
        }
      ],
      "api_name": "hospitality.staff_employment_period",
      "description": "Per-period employment history for a staff member. The current period has null `termination_date`. Lifetime-tenure projection sums durations across all periods.",
      "display_name": "Staff employment period",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Period id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "hire_date",
          "description": "",
          "display_name": "Hire date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "termination_date",
          "description": "",
          "display_name": "Termination date (null = current)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "termination_reason",
          "description": "",
          "display_name": "Termination reason",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "role_at_hire",
          "description": "",
          "display_name": "Role at hire",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.clock_in",
          "description": "Open a worked shift for the target staff member. `department` and the management/non-management split are stamped at clock-in (a cross-department day is two shifts; a later promotion does not rewrite history). Refused while the member already has an open shift or is inactive."
        },
        {
          "api_name": "hospitality.action.open_staff_employment_period",
          "description": "Open a new employment period for a staff member; auto- closes the prior open period if present. Updates `staff_member.most_recent_hire_date`."
        },
        {
          "api_name": "hospitality.action.record_staff_certification",
          "description": "Record a credential grant for a staff member. Renewals are a fresh grant, not a mutation; the current credential of a kind is the latest non-revoked, non-expired row."
        },
        {
          "api_name": "hospitality.action.refresh_staff_projections",
          "description": "Recompute `tenure_years_at_current_property`, `tenure_years_lifetime`, and `comp_authority_summary` from `staff_employment_period` rows + active `comp_authority_policy` rows."
        },
        {
          "api_name": "hospitality.action.set_staff_handset_passcode",
          "description": "Record (or clear) the Argon2id hash of the PIN an employee types on a shared floor handset. `POST /v1/handset/session` verifies the PIN against it and mints a user token whose principal is this staff_member. The hash is secret-marked: never readable through the HTTP surface, never in an event image. Omit `passcode_hash` to clear the PIN (the employee can no longer sign in on a handset). Emits `staff.handset_passcode.set` / `staff.handset_passcode.cleared` with the staff_member id only."
        },
        {
          "api_name": "hospitality.action.set_staff_push_topics",
          "description": "Choose the push topics a staff member's handset is nudged for (hubOS's notification topics): cleaning_tasks (a task assigned or reclaimed to me, my task made urgent), faults (a work order assigned or reclaimed to me), urgent_faults (a blocking or safety fault raised or escalated at my house — to its engineers), postponed_faults (a fault postponed — to its assignee and the house's engineers) and delegated_faults (a fault handed to my role at my house). Omit `topics` to go back to the default — cleaning_tasks and faults; the fault topics are heard only when named. A staff member's session sets only its own; a supervisor sets any colleague's at their house. Emits `staff_member.push_topics.set`."
        },
        {
          "api_name": "hospitality.action.set_staff_sort_preference",
          "description": "Set a staff member's own order for one board (hubOS: each user keeps their own pacing-report sort) — the same keys set_board_sort_pattern takes, held to the same rules; omit `keys` to clear it and read the house's pattern again. Stored on the member (`sort_preferences`, one entry per board) and returned by `board_sort` ahead of the house's pattern. A staff member's session sets only its own; a supervisor (housekeeping or front-office supervisor, manager on duty, general manager, owner) sets any colleague's at their house. The same order again writes nothing. Emits `staff_member.sort_preference.set`."
        },
        {
          "api_name": "hospitality.action.update_staff_member_profile",
          "description": "Update the target staff member's profile fields. Every parameter is optional but at least one must be provided; omitted fields keep their current values. `employee_id` is identity and cannot change here. A call whose values all match the current profile is an idempotent no-op (connector re-loads retry safely without version churn). Emits `staff.profile_updated`."
        }
      ],
      "api_name": "hospitality.staff_member",
      "description": "An employee. Linked to a property; for chains a single staff member can be a manager across multiple properties via additional links in higher-level extensions.",
      "display_name": "Staff member",
      "domain": "people",
      "interfaces": [],
      "properties": [
        {
          "api_name": "employee_id",
          "description": "",
          "display_name": "Employee ID",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "given_name",
          "description": "",
          "display_name": "Given name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "family_name",
          "description": "",
          "display_name": "Family name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "email",
          "description": "",
          "display_name": "Email",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "handset_passcode_hash",
          "description": "",
          "display_name": "Handset PIN hash (Argon2id, PHC string)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "identity_subject",
          "description": "",
          "display_name": "Identity-provider subject (SSO)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "role",
          "description": "",
          "display_name": "Role",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "department",
          "description": "",
          "display_name": "Department",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "discipline",
          "description": "",
          "display_name": "Discipline",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "pathway_stage_code",
          "description": "",
          "display_name": "Pathway stage code (operator vocabulary)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "most_recent_hire_date",
          "description": "",
          "display_name": "Most recent hire date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "tenure_years_at_current_property",
          "description": "",
          "display_name": "Tenure years at current property (projection)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "tenure_years_lifetime",
          "description": "",
          "display_name": "Tenure years lifetime (projection)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "tenure_computed_at",
          "description": "",
          "display_name": "Tenure refresh timestamp",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "comp_authority_summary",
          "description": "",
          "display_name": "Resolved comp authority summary (cache)",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Active",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "sort_preferences",
          "description": "",
          "display_name": "The staff member's own order for a board, one entry per board — wins over the house's pattern in `board_sort`",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "push_topics",
          "description": "",
          "display_name": "The push topics the staff member hears; absent = cleaning_tasks and faults (the nudges every handset has had), and the fault topics (urgent, postponed, delegated) only when named",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "contracted_weekly_hours",
          "description": "",
          "display_name": "The hours a week the staff member is contracted for (more than 0, at most 168) — what the pacing report's percentage_worked is of",
          "kind": "decimal",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.clock_out",
          "description": "Close the target open shift. Computes `worked_hours` (`actual_end - actual_start` minus unpaid break, decimals preserved per Schedule 15) and stamps the property-local `business_date` from `actual_start`. Emits `labour.shift_closed.recorded` for the `daily_labour` projection."
        },
        {
          "api_name": "hospitality.action.set_shift_credit_target",
          "description": "Set the housekeeping credits an attendant is expected to clean on an open shift — OPERA's per-sheet maximum credits, hubOS's pacing target. The pacing report (`housekeeping_pacing`) counts the attendant's completed credits against it. Omit `credit_target` to clear it. Refuses a closed or voided shift: the target is the shift's plan, and a closed shift is history. Also settable at `clock_in`. The target the shift already has writes nothing. Emits `labour.shift_target.set` when it moves."
        },
        {
          "api_name": "hospitality.action.void_shift",
          "description": "Mis-punch correction: void the target shift (open or closed) and re-record if needed — hours are never silently edited. Voiding a closed shift emits `labour.shift_voided.decided` so the `daily_labour` projection folds the hours back out; voiding a never-closed shift has no analytics impact."
        }
      ],
      "api_name": "hospitality.staff_shift",
      "description": "One worked (or in-progress) shift. `department` and `management` are stamped at clock-in — a cross-department day is two shifts, and a later promotion does not rewrite history. Hours are computed at clock-out (`actual_end - actual_start` minus unpaid break) and report under the property-local `business_date` of `actual_start`. Immutable post-close except `voided` (mis-punch correction: void + re-record).",
      "display_name": "Staff shift",
      "domain": "labour",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Shift id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "department",
          "description": "",
          "display_name": "USALI 12 Schedule 15 department",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "management",
          "description": "",
          "display_name": "Management",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "scheduled_start",
          "description": "",
          "display_name": "Scheduled start",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "scheduled_end",
          "description": "",
          "display_name": "Scheduled end",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "actual_start",
          "description": "",
          "display_name": "Actual start",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "actual_end",
          "description": "",
          "display_name": "Actual end",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "worked_hours",
          "description": "",
          "display_name": "Worked hours",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "unpaid_break_minutes",
          "description": "",
          "display_name": "Unpaid break minutes",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Source",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Notes",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "credit_target",
          "description": "",
          "display_name": "Housekeeping credits the attendant is expected to clean this shift (zero or more)",
          "kind": "decimal",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.apply_deposit_to_folio",
          "description": "Carry the booking's deposit onto the stay's folio at check-in: the payments move account-to-folio, both balances follow, and `prepayment_status` becomes `applied_to_folio`. A deposit asserted on the reservation without a payment behind it — an import's shape — is credited as one instead, so the folio is right either way. Idempotent on `stay`."
        },
        {
          "api_name": "hospitality.action.close_post_stay_window",
          "description": "**Card-rail only.** End a departed stay's post-stay window once its end has passed and the card rail has decided its kept hold (captured for what the bill owed, else released): the stay reads `closed`, and a bill the window reopened that now owes nothing is closed again (a stay with no bill closes its window all the same). Refused before the window's end. Closing a closed window changes nothing. Emits `post_stay_window.closed` with the bill's balance. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.issue_access_credential",
          "description": "Bind a physical credential (room key card, wristband) to an in-house stay by its opaque vendor token — the enrollment tap: after cutting the key, staff tap it on any reader and the UID lands here, no lock-vendor integration required. At most one active credential may hold a token (database invariant); if the same card's previous binding is stale (its stay checked out or the credential expired) it is auto-revoked and the token re-issued, otherwise the issue is refused. Caps bound what the credential can post. Emits `access.credential_issued.recorded`."
        },
        {
          "api_name": "hospitality.action.open_hold_top_up",
          "description": "**Card-rail only.** Write down an in-house stay's top-up attempt before the card rail asks the processor for anything: an attempt id (`hold_top_up_attempt`, which the rail's processor idempotency keys are derived from), what it is (`kind`: `top_up`, a second hold or an increment; `overage_charge`, the credit-limit charge), what it holds (`amount`) and, for a charge, what it takes from the holds (`capture`), the holds it acts on (`holds`, largest first: a charge takes those and no other, a top-up goes on the first's card — each a pending card hold on the stay's bills; required for a charge), and when it was opened. An attempt already open is kept as it was, whatever this call asks — a retry reaches the processor as the same requests under the same keys, and the bill's growth since is the next audit's. `record_hold_top_up` ends it; an attempt open 23 hours, inside Stripe's 24-hour memory of a key, is abandoned by the rail (`attempt_abandoned`) and a new one opened. Refused for a stay that is not in house. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.open_post_stay_collection",
          "description": "**Card-rail only.** Write down a departed stay's collection attempt before the card rail asks the processor for anything: an attempt id (`post_stay_collection_attempt`, which the rail's processor idempotency keys are derived from) and what it asks for (`post_stay_collection_amount`). An attempt already open is kept as it was, `amount` notwithstanding — a retry after a late charge moved the bill reaches the processor as the same requests under the same keys, and the late charge is the next attempt's. `record_post_stay_collection` ends it. Refused for a stay that has not checked out. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.post_departed_charge",
          "description": "Post charges found after the guest left — OPERA's Delayed Charges — to the departed stay's bill, inside the house's post-stay window (`set_post_stay_charging`). Each line posts as an ordinary charge on today's business date, through the house's sole outlet in the line's `bucket`, with the house's tax rules for that bucket; the bill is reopened first (`reopen_folio`, reason `late_charge`) when check-out closed it. A line naming the floor's `uncharged` minibar consumption (`movement_id`) is joined to the charge it became (`charge_for_stock_movement`), and a consumption already posted is skipped, so the same lines posted twice post once. Refused for a stay still in house, when the house has the window off, after the window closed, and when the bill has been invoiced (correct a sealed bill with `open_rebate_folio`). Taking the money is the card rail's (`POST /v1/stays/{id}/post-stay-charges/collection`). Emits `post_stay_charge.posted` with the bill's new balance."
        },
        {
          "api_name": "hospitality.action.record_breakfast_taken",
          "description": "The breakfast roster's tick-off: `covers` of the target in-house stay took the breakfast their package includes, with no bill. The stay's breakfast is the first component (by the package's order) of a live package sale whose frozen `daypart` is `breakfast` or `brunch`, eaten at `outlet_id` — one of its alternate outlets, or any outlet of the house when it names none — and due on `business_date` (default: the house's hotel date): a breakfast is eaten the morning after the night that carries it, so the first morning of a stay has none and the departure morning does. `covers` is how many of the room have taken it so far that day, at most the covers the package feeds (the room's adults for a per-adult allowance, everyone in it otherwise): the same number again is a no-op, a lower one is refused, a higher one records the new covers. Each records a `package_component_consumption` through the one package write `consume_package_component` uses — consumed equal to the allowance it draws (`consumed`), the covers' share of the day's allowance, the last cover taking what is left — with its balance-neutral package line on the stay's guest folio at the outlet, and the package ledger's movements. Refused when the stay has no breakfast that day at this outlet, or when the day's allowance is already used up (a room charge may have consumed it). Emits `fnb.breakfast_taken`."
        },
        {
          "api_name": "hospitality.action.record_hold_top_up",
          "description": "**Card-rail only.** Record what an in-house stay's top-up came to. `placed`: a second hold (`payment_ref`, a pending hold on one of the stay's bills) — the stay's top-up count goes up. `incremented`: the stay's hold `payment_ref` raised at the processor to `amount`, which the payment now holds. `charged`: the holds taken for the bill (`captured_amount`; each capture is its own `record_hold_release`) and a fresh hold (`payment_ref`) placed for what is left plus the allowance — or, with a `refusal`, not placed. `refused`: nothing done, and why (`refusal`, the processor's `detail`; `attempt_abandoned` when the rail gave up an attempt it could no longer carry out as written). Every outcome but `refused` names the open attempt it ends (`attempt`), and a hold it names must be a pending card hold on the bills of a stay still in house; a record for an attempt no longer open is a replay and changes nothing, and the same refusal for the same balance (`balance`) is said once. Every outcome stamps the stay as tried at this audit. Emits `hold_top_up.recorded` for what was done and `hold_top_up.refused` for a refusal. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.record_post_stay_collection",
          "description": "**Card-rail only.** Record what the card rail took for a departed stay's late charges, or why it took nothing. The kept hold captured (`hold_payment_id`, `captured_amount`), the card on file charged off-session (`payment_card_id`, `card_amount`, `psp_reference` — a settled `credit_card` payment on the bill, drawn on the card), or both; a `refusal` says why the rest was not taken (`authentication_required` keeps the charge owed until the guest authenticates). Refused (`over_collection`) when what it says was taken is more than the bill owes — a collection racing another, or a stale attempt — before anything is credited. A bill the collection brought to zero is closed again, and the stay's open collection attempt (`open_post_stay_collection`) ends. Recording the same legs twice changes nothing, and the same refusal for the same balance is said once. Emits `post_stay_charge.collected` and/or `post_stay_charge.refused`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.set_green_stay",
          "description": "Set or clear the stay's green stay: the guest declined the day's clean for sustainability (hubOS's leaf, beside and never one of its do-not-disturb states). While set, generate_housekeeping_tasks mints no stayover clean for the stay's room — a shared room only when every guest in house in it declined — and the departure clean, turndown and every other task are untouched. Setting it retires today's generated stayover on the room that nobody has started (skipped, with the reason); clearing it re-mints today's stayover when the house's board has been generated for today (reviving the skipped one in place, through the generator's own key and price), so the room is back on the sheet without a re-run. Today is the house's housekeeping service date (its local calendar day). A manual task is never touched. Refused on a stay that has checked out, did not show or was cancelled; setting the value the stay already carries is a no-op. The flag is not guest PII: the floor sets it through the room's in-house stay. Emits `stay.green_stay_set`."
        },
        {
          "api_name": "hospitality.action.set_posting_rules",
          "description": "The desk's posting rules for an in-house stay — what an outlet's till may put on the guest's bill. `no_post: true` is OPERA's NOPOST: every room charge and charge-to-key to the stay is refused (`no_post`). `credit_limit` is the most the stay's bill may reach, the room still to come included — OPERA's credit limit for the guest's payment method; absent, it is cleared. A till's posting is held to the greater of the limit and what the stay's live card holds hold, less the stay's bills' balance and the room still due for the nights to come; a posting that would pass it is refused (`no_credit`). A stay with neither a limit nor a hold has no credit rule (no_post is the house's lever there). An override on the payment (`override_reason`) taken under a manager's own session lets either refusal through, and is recorded on the payment with the session's staff member as its authoriser. Never moves anything already posted. Re-setting the same rules is a no-op. Emits `fnb.posting_rules_set` with the reason."
        },
        {
          "api_name": "hospitality.action.sign_registration_card",
          "description": "Record the registration card the guest signed at the desk for this stay: the house's terms exactly as shown (`terms_text`, with a `terms_version` label or hash), the name signed under, and the signature's strokes as SVG path data (`signature_svg_paths`: `M` and `L` commands with numbers only, at most 64 KB — nothing but the strokes is stored). Linked to the stay and to its reservation, signed at the kernel's clock, captured by the calling principal. One current card per stay: signing again replaces the current card, which is kept as `superseded`. A chargeback's evidence packet attaches the current card as `customer_signature`. Refuses a stay that is not checked in or checked out, empty or malformed strokes, and strokes over the limit. Emits `registration_card.signed` (never the strokes or the name)."
        }
      ],
      "api_name": "hospitality.stay",
      "description": "The execution of a reservation: the actual arrival, room assignment, in-house period, and departure. Charges and payments hang off the stay's folio.",
      "display_name": "Stay",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Stay id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Stay state",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "arrival_actual",
          "description": "",
          "display_name": "Actual arrival",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "departure_actual",
          "description": "",
          "display_name": "Actual departure",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "room_assignment_status",
          "description": "",
          "display_name": "Room assignment status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "green_stay",
          "description": "",
          "display_name": "The guest declined the day's clean for sustainability — while set, generate_housekeeping_tasks mints no stayover clean for the room (a shared room only when every guest in it declined)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "green_stay_set_at",
          "description": "",
          "display_name": "When the green stay was set (present while green_stay is true)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "post_stay_window",
          "description": "",
          "display_name": "open while charges found after departure may still post; closed after",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "post_stay_window_until",
          "description": "",
          "display_name": "When the post-stay window closes — the house's midnight after the window's last business day",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "post_stay_window_closed_at",
          "description": "",
          "display_name": "When the card rail closed the post-stay window",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "post_stay_collection_attempt",
          "description": "",
          "display_name": "The card rail's open collection attempt — the id its processor keys are derived from; cleared when the attempt is recorded",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "post_stay_collection_amount",
          "description": "",
          "display_name": "What the open collection attempt asks for",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "post_stay_refusal",
          "description": "",
          "display_name": "Why the last post-stay collection took nothing, until one takes something",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "post_stay_refusal_balance",
          "description": "",
          "display_name": "The bill's balance when that refusal was recorded",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hold_top_up_attempt",
          "description": "",
          "display_name": "The card rail's open top-up attempt — the id its processor keys are derived from; cleared when the attempt is recorded",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "hold_top_up_attempt_amount",
          "description": "",
          "display_name": "What the open top-up attempt holds (or, for a credit-limit charge, holds afresh)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hold_top_up_attempt_capture",
          "description": "",
          "display_name": "What an open credit-limit charge takes from the holds",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hold_top_up_attempt_kind",
          "description": "",
          "display_name": "top_up (a second hold or an increment) or overage_charge",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "hold_top_ups",
          "description": "",
          "display_name": "How many top-ups the card rail has placed for this stay",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "hold_top_up_last_at",
          "description": "",
          "display_name": "When the last top-up was placed",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "hold_top_up_refusal",
          "description": "",
          "display_name": "Why the last top-up took nothing, until one takes something",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "hold_top_up_refusal_detail",
          "description": "",
          "display_name": "The processor's words for that refusal",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "hold_top_up_refusal_balance",
          "description": "",
          "display_name": "The bill's balance when that refusal was recorded",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "hold_top_up_refused_at",
          "description": "",
          "display_name": "When that refusal was recorded",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "hold_top_up_attempt_opened_at",
          "description": "",
          "display_name": "When the open top-up attempt was written down; past 23 hours it is abandoned and a new one opened",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "hold_top_up_attempt_holds",
          "description": "",
          "display_name": "The holds the open attempt acts on, largest first: those a credit-limit charge takes, or the hold whose card a top-up goes on",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "no_post",
          "description": "OPERA's NOPOST: while true, a till's room charge or charge-to-key to this stay is refused (`no_post`) unless a manager's override is recorded on the payment. Set by the desk with set_posting_rules; unset reads as false.",
          "display_name": "No outlet may post to the stay's bill",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "credit_limit",
          "description": "The most the desk lets the stay's bill reach, the room still to come included — OPERA's credit limit set for the guest's payment method. A till's posting is held to the greater of this and what the stay's live card holds hold, less the bills' balance and the room still due; over it the posting is refused (`no_credit`) unless a manager overrides. With neither a limit nor a hold, no credit rule applies. Set by the desk with set_posting_rules.",
          "display_name": "The stay's credit limit, beside its card holds",
          "kind": "money",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.stay_offer",
      "description": "One room type on one rate plan the house quoted a shopper and kept: the stay (`arrival_date`, `departure_date`), the party (`adults`), the published arrival-night rate (`quoted_rate`) and stay total the admission gate priced, when it was quoted (`offered_at`, business time), the channel the shopper came through, the shopping session the house issued (`session_id`) and the visitor key it issued for the browser (`visitor_id`). A search stays the read `quote_stay` is; the offer is the room and rate a shopper opened. A reservation made in the session links to its first offer (`reservation_booked_in_session`), and a session that booked nothing is a regret, derived — `regret_record` — never recorded. Holds no inventory.",
      "display_name": "Stay offer",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Offer id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "offered_at",
          "description": "",
          "display_name": "When the house quoted it (business time; defaults to now)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "arrival_date",
          "description": "",
          "display_name": "Arrival night",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "departure_date",
          "description": "",
          "display_name": "Departure date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "adults",
          "description": "",
          "display_name": "Adults — the occupancy the grid priced",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "quoted_rate",
          "description": "",
          "display_name": "Rate quoted",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "total",
          "description": "",
          "display_name": "Stay total quoted",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Channel the shopper came through",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "session_id",
          "description": "",
          "display_name": "Shopping session",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "visitor_id",
          "description": "",
          "display_name": "Visitor key the house issued",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.recount_stock_level",
          "description": "Check a stock level's cached on_hand against its movements — the sum of their quantities, the truth the cache keeps — and write the sum back when they disagree, emitting `stock_level.reconciled` with the previous figure, the derived one and the difference. A level that agrees is left alone and nothing is said. The folio balance's reconcile, for stock (2026-10-04): the cache is checked, never trusted."
        }
      ],
      "api_name": "hospitality.stock_level",
      "description": "How much of one material one store holds — one row per material × store, linked `stock_level_for_material` and `stock_level_in_store` (the ids ride on the row too, so a store's board filters without a walk). `on_hand` is a cache with one writer: every movement verb records its `stock_movement` and moves this figure in the same transaction, under the level's lock; the movements are the truth and `recount_stock_level` folds them to check it. Never below zero unless the store allows it. Carries the `reorder_point` (OPERA's par) and when it was last counted. Minted by the first movement (or reorder point) for the pair.",
      "display_name": "Stock level",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Stock level id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "material_id",
          "description": "",
          "display_name": "The material counted (its `stock_level_for_material` edge, on the row)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "store_id",
          "description": "",
          "display_name": "The store it is kept in (its `stock_level_in_store` edge, on the row)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "on_hand",
          "description": "",
          "display_name": "How much the store holds, in the material's unit — the sum of the level's movements",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "reorder_point",
          "description": "",
          "display_name": "At or below this, the store's stock of the material is reordered (OPERA's par)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "last_counted_at",
          "description": "",
          "display_name": "When it was last physically counted",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "last_movement_at",
          "description": "",
          "display_name": "When its last movement was recorded",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.stock_movement",
      "description": "One movement of stock into or out of a store, on its level (`stock_movement_for_level`): a `receipt`, an `issue`, a `consumption` (on a fault — `stock_movement_for_work_order` — or a minibar task — `stock_movement_for_housekeeping_task`), an `adjustment` with its reason, a `count` (the level set to what was counted; the movement carries the difference), or one leg of a `transfer` (the receiving leg names the issuing one, `stock_movement_transfer_from`). `quantity` is signed, in the material's unit; `on_hand_before` and `on_hand_after` are the level either side of it, so a store's stock card reads off its movements. Each kind is pinned to its sign and its references by validator. `recorded_by` names the principal; a staff member who recorded it is linked (`stock_movement_recorded_by`). Never edited: a correction is another movement.",
      "display_name": "Stock movement",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Stock movement id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "receipt, issue, consumption, adjustment, count, transfer",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "quantity",
          "description": "",
          "display_name": "How much moved, signed (in positive, out negative), in the material's unit",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "on_hand_before",
          "description": "",
          "display_name": "The level before this movement",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "on_hand_after",
          "description": "",
          "display_name": "The level after it — on_hand_before + quantity",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "counted",
          "description": "",
          "display_name": "What a count found (a count movement only; equals on_hand_after)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Why — required for an adjustment (breakage, expiry, theft)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "reference",
          "description": "",
          "display_name": "An outside reference — the supplier's delivery note, an invoice number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "charged",
          "description": "",
          "display_name": "What the guest was charged for it (a minibar consumption)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "uncharged",
          "description": "",
          "display_name": "What it would have charged — a minibar consumption recorded with nobody in house, the desk's late charge (never beside charged)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "recorded_at",
          "description": "",
          "display_name": "When it was recorded — the handset's own time when it queued offline",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "recorded_by",
          "description": "",
          "display_name": "The principal that recorded it (`user:…`, `agent:…`, `service:…`, `system`)",
          "kind": "string",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.adjust_stock",
          "description": "Correct a store's stock of a material, one of two ways. A count (`counted`): what is physically there — the level is set to it, a `count` movement carries the difference (zero when the count agrees) and the level's last_counted_at is stamped. A correction (`quantity`, signed, with the reason — breakage, expiry, theft): an `adjustment` movement by that much. Exactly one of the two. Either is refused when it would leave the level below zero, unless the store allows negative stock."
        },
        {
          "api_name": "hospitality.action.issue_stock",
          "description": "Record stock leaving a store for use — to a room (room_id), a department, an event — as an `issue` movement, moving the level down by it. Refused when it would take the level below zero, unless the store allows negative stock. Stock used on a fault is logged on its thread (record_work_order_entry); stock moved to another store is transfer_stock."
        },
        {
          "api_name": "hospitality.action.receive_stock",
          "description": "Record stock arriving at a store — a delivery — as a `receipt` movement, and move the store's level for the material up by it in the same transaction (minting the level on the pair's first movement). A retired material refuses. Replay-safe under the apply route's Idempotency-Key."
        },
        {
          "api_name": "hospitality.action.set_stock_reorder_point",
          "description": "Set the level at or below which this store's stock of a material is reordered (OPERA's par), or clear it (no reorder_point). A movement that takes the level from above the point to at or below it emits `stock_level.below_reorder_point`. Mints the level when the store holds none of it yet. Setting the point it already has is a no-op."
        },
        {
          "api_name": "hospitality.action.transfer_stock",
          "description": "Move stock of a material from this store to another of the house's (to_store_id) — the stores room to a floor cupboard, the minibar cart to a room's minibar. Two `transfer` movements in one transaction, the issuing leg here and the receiving leg there (`stock_movement_transfer_from` names the first), each moving its own level. Refused when it would take this store below zero, unless it allows negative stock."
        }
      ],
      "api_name": "hospitality.stock_store",
      "description": "A place a house keeps stock: the stores room, a floor cupboard, the minibar cart, one room's minibar (linked `stock_store_in_room`), the laundry, the workshop. Keyed by `code` within the house; `define_stock_store` with a code the house holds re-states it in place. `is_default` marks the house's store of its kind that work draws from when it names none — a minibar task in a room with no minibar of its own draws from the default minibar cart, a laundry task from the default laundry, a fault's material line from the default workshop (one default per kind). A store's stock may go below zero only when it says so (`allow_negative`): a house that does not count its minibar carts lets them run negative rather than refuse a guest's charge.",
      "display_name": "Stock store",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "The house's code for the store — unique within the house",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "The store in words (\"Stores\", \"Floor 3 linen\", \"Minibar 101\")",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "main_store, floor_store, minibar_cart, room_minibar, laundry, workshop",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "allow_negative",
          "description": "",
          "display_name": "Whether its stock may go below zero (an uncounted store)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "is_default",
          "description": "",
          "display_name": "The house's store of its kind that work draws from when it names none",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "defined_at",
          "description": "",
          "display_name": "When it was first defined",
          "kind": "datetime",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.delete_stored_object",
          "description": "Soft-delete a stored_object reference (status = deleted). The bytes are reaped by an object-storage lifecycle rule; this action only retires the reference."
        },
        {
          "api_name": "hospitality.action.finalize_object_upload",
          "description": "Confirm a pending stored_object after its bytes have landed and the API has HEAD-verified them. Records the verified size / checksum / etag and flips the row to available."
        }
      ],
      "api_name": "hospitality.stored_object",
      "description": "Typed reference to a binary blob held in Aerandil's S3-compatible object storage. Holds the storage key, content type, size, and checksum — never the bytes, which are uploaded and downloaded directly via presigned URLs. The structural contract mirrors external_record_reference: a reference, never a payload.",
      "display_name": "Stored object",
      "domain": "stored_objects",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Stored-object id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "storage_key",
          "description": "",
          "display_name": "Storage key",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "backend",
          "description": "",
          "display_name": "Storage backend",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "bucket",
          "description": "",
          "display_name": "Bucket",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "content_type",
          "description": "",
          "display_name": "Content type (MIME)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "host_kind",
          "description": "",
          "display_name": "Host entity kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "host_id",
          "description": "",
          "display_name": "Host entity id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "content_category",
          "description": "",
          "display_name": "Content category",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "original_filename",
          "description": "",
          "display_name": "Original filename",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "max_bytes",
          "description": "",
          "display_name": "Declared maximum size in bytes",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "byte_size",
          "description": "",
          "display_name": "Verified size in bytes",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "checksum_sha256",
          "description": "",
          "display_name": "Verified SHA-256 (base64)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "etag",
          "description": "",
          "display_name": "Storage ETag",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "uploaded_at",
          "description": "",
          "display_name": "Upload confirmed at",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.tax_application",
      "description": "One tax line applied to a `Charge`. Required for fiscal reporting and OTA tax-and-fee disclosure: a single charge typically has several (city tax, occupancy tax, VAT, resort fee), each with its own jurisdiction, rate, and computed amount.",
      "display_name": "Tax application",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Tax application id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Tax code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "jurisdiction",
          "description": "",
          "display_name": "Jurisdiction",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "rate",
          "description": "",
          "display_name": "Rate (decimal, e.g. 0.10 = 10%)",
          "kind": "decimal",
          "required": true
        },
        {
          "api_name": "basis_amount",
          "description": "",
          "display_name": "Basis amount taxed",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Tax amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "inclusive",
          "description": "",
          "display_name": "Inclusive of charge amount",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.tax_exemption",
      "description": "A reservation's exemption from named tax codes — a government or diplomatic guest's certificate on file. Every tax line the kernel derives for the booking (room nights, day use, retained payments) skips a rule whose code is listed, from the moment it is set until it is revoked; postings already made keep their tax, as an invoice already printed would.",
      "display_name": "Tax exemption",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Tax exemption id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Why the booking is exempt",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reference",
          "description": "",
          "display_name": "The exemption certificate or authority reference an auditor asks for",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "exempt_codes",
          "description": "",
          "display_name": "The tax rule codes the booking is exempt from",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "granted_at",
          "description": "",
          "display_name": "When the exemption was recorded",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "revoked_at",
          "description": "",
          "display_name": "When the exemption stopped applying; absent = in force",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing note",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.end_tax_rule",
          "description": "Close (or shorten) a tax rule's effective window: sets `effective_to`. Refuses a date before the rule's effective_from, and refuses a window that would overlap a later rule in the same code's succession. A gap in the timeline is legal — an untaxed window is a legitimate jurisdictional fact (a suspended levy), and the authoring event records who opened it."
        }
      ],
      "api_name": "hospitality.tax_rule",
      "description": "One property-authored tax rule: jurisdiction, what it applies to (a USALI bucket, one retained-payment `system_charge_kind`, or an F&B `tax_class` — exactly one), percent / flat-per-night / per-person basis, inclusive/exclusive posture, an optional long-stay reduced value, and an effective-date window. The night audit derives each room posting's `tax_application` lines from the rooms rules active on the audited business date; a forfeited deposit, cancellation fee or no-show charge is taxed only by a rule targeting its kind. Rules sharing a `code` and a target form a non-overlapping succession timeline (a rate change end-dates the predecessor and mints the successor).",
      "display_name": "Tax rule",
      "domain": "finance",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Tax rule id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "code",
          "description": "",
          "display_name": "Tax code — the rule's identity within the property; rules sharing a code form one succession timeline",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "jurisdiction",
          "description": "",
          "display_name": "Taxing jurisdiction (e.g. `US-NY-NYC`, `DE`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "bucket",
          "description": "",
          "display_name": "USALI bucket whose charges this rule taxes. Absent on a rule that targets a retained-payment kind instead",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "system_charge_kind",
          "description": "",
          "display_name": "The retained payment this rule taxes (forfeited deposit, cancellation fee, no-show fee, no-show revenue) — what the house keeps for a stay that did not happen. Nothing taxes one of these unless a rule targets its kind. Percent and inclusive only",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "tax_class",
          "description": "",
          "display_name": "The F&B tax class this rule taxes (an `item_class.tax_class` code — Standard, Cold food, Children's meal). Percent only. A check line takes every rule in force for its class on the check's business date",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "basis",
          "description": "",
          "display_name": "How the tax computes",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "rate",
          "description": "",
          "display_name": "Rate for `percent` rules (decimal in [0, 1]; 0.10 = 10%). Required on percent rules, forbidden on flat rules",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "flat_amount",
          "description": "",
          "display_name": "Amount per room night (`flat_per_night`) or per guest per night (`per_person_per_night`). Required on those bases, forbidden on percent rules",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "adults_only",
          "description": "",
          "display_name": "`per_person_per_night` only: count adults and not children (absent = everyone in the room)",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "reduced_after_nights",
          "description": "",
          "display_name": "`percent` rooms rules only: from the night after this many nights of a continuous stay, the rule taxes only `reduced_value_share` of the night's value (the UK 28-day rule)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "reduced_value_share",
          "description": "",
          "display_name": "The share of a long-stay night's value the rule still taxes (decimal in (0, 1]; 0.20 = the UK's 20% facilities minimum)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "inclusive",
          "description": "",
          "display_name": "Whether the tax lives inside the posted amount (VAT carve-out) or on top of it (US-style add-on)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "effective_from",
          "description": "",
          "display_name": "First business date the rule applies (inclusive)",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "effective_to",
          "description": "",
          "display_name": "Last business date the rule applies (inclusive); absent = open-ended",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Operator-facing description",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.training_cohort",
      "description": "Append-only record of a single training run's composition for unlearning + audit. `shard_assignment` records the SISA shard → tenants map (Bourtoule et al. 2021) so an Article 17 erasure retrains only the affected shards; `manifest_hash` is the sha256 over the canonicalised manifest. `epsilon_budget` / `delta_budget` are recorded, never enforced (proposal 0026 §6).",
      "display_name": "Training cohort",
      "domain": "consent",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Cohort id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "model_name",
          "description": "",
          "display_name": "Model this cohort trained",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "model_version",
          "description": "",
          "display_name": "Model version this run produced",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "run_id",
          "description": "",
          "display_name": "MLflow run id (uuid) — the lineage anchor back to training",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "assembled_at",
          "description": "",
          "display_name": "When the cohort was assembled",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "shard_assignment",
          "description": "",
          "display_name": "SISA shard id → tenants assigned to that shard",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "epsilon_budget",
          "description": "",
          "display_name": "DP epsilon for this run (recorded, not enforced)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "delta_budget",
          "description": "",
          "display_name": "DP delta for this run (recorded, not enforced)",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "exclusion_list_size",
          "description": "",
          "display_name": "Count of erased records explicitly excluded from this cohort",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "manifest_hash",
          "description": "",
          "display_name": "sha256 of the canonicalised cohort manifest",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "model_card_uri",
          "description": "",
          "display_name": "Opaque pointer to the model card (the serving service owns the doc)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.treatment_room",
      "description": "A bookable treatment space under a spa venue — massage room, couples suite, facial room, salon station, class studio (Book4Time's Facility). `capacity` bounds how many appointments may overlap in it: 1 for a standard room, 2 for a couples suite, more for class-style studios. Occupancy is derived from appointment windows (including the pre/cleanup buffers), so the room carries no live floor status. Configuration: CRUD-authored, linked to its venue via treatment_room_at_venue; which services it can host is the declared-optional treatment_room_supports_service map.",
      "display_name": "Treatment room",
      "domain": "spa",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Room code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Room name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Room kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "capacity",
          "description": "",
          "display_name": "Concurrent appointments the room admits (couples suite = 2)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "active",
          "description": "",
          "display_name": "Active",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.turnaway",
      "description": "A request for rooms the house refused — the lost-demand record. Not a reservation and not a waitlist entry: nothing traverses a turnaway, so it holds no inventory. Carries the night asked for, the stay length, the instant the guest asked (`requested_at`, business time), why the desk said no (`reason`), the minimum stay in force if one was, the segment and channel when known, for a guest who declined the rate (`rate`, a regret) the rate quoted, and the booking engine's key for the browser that asked (`visitor_id`) when its tag read one. A revenue-management forecast reads turnaways beside sales to see the demand a full house or a restriction hid; without them it is forecasting from censored data. A regret's quoted rate is how it reads its own price back out of what it saw, and the visitor key — on the reservation too — is how it tells one shopper's earlier looks from a stranger's.",
      "display_name": "Turnaway",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Turnaway id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "requested_at",
          "description": "",
          "display_name": "When the guest asked (business time; defaults to now)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "arrival_date",
          "description": "",
          "display_name": "The arrival night asked for",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "nights",
          "description": "",
          "display_name": "Nights asked for",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Why the request was refused",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "min_stay_in_force",
          "description": "",
          "display_name": "The minimum stay posted for the arrival when the request was refused",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "market_segment",
          "description": "",
          "display_name": "Market segment of the demand, when the desk could tell",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Channel the request arrived through, when known",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "adults",
          "description": "",
          "display_name": "Adults asked for",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "quoted_rate",
          "description": "",
          "display_name": "Rate quoted",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "visitor_id",
          "description": "",
          "display_name": "Booking engine visitor",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "What the desk noted",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "Replay key for the recording source (unique per tenant while present)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.void_utility_cost",
          "description": "Void the target entry — terminal, the amounts stay immutable as the audit record. Emits `utility.cost_voided.decided` so the `daily_eww` projection folds the cost and consumption back out."
        }
      ],
      "api_name": "hospitality.utility_ledger_entry",
      "description": "One posted utility cost line (USALI 12 Schedule 9 — Energy, Water, and Waste), with the metered consumption when the invoice carries it. `category` is stamped from `kind` (the four Schedule 9 groupings); the consumption quantity/unit pair stays in its source unit. Immutable except the terminal void.",
      "display_name": "Utility ledger entry",
      "domain": "eww",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "USALI Schedule 9 account",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "USALI Schedule 9 category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "amount",
          "description": "",
          "display_name": "Amount",
          "kind": "money",
          "required": true
        },
        {
          "api_name": "consumption_quantity",
          "description": "",
          "display_name": "Metered consumption quantity",
          "kind": "decimal",
          "required": false
        },
        {
          "api_name": "consumption_unit",
          "description": "",
          "display_name": "Consumption unit",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "business_date",
          "description": "",
          "display_name": "Business date",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "period_start",
          "description": "",
          "display_name": "Billing-period start",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "period_end",
          "description": "",
          "display_name": "Billing-period end",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "source",
          "description": "",
          "display_name": "Source",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "external_ref",
          "description": "",
          "display_name": "External ref",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "posted_at",
          "description": "",
          "display_name": "Posted at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "voided",
          "description": "",
          "display_name": "Voided",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.assign_brand_to_venue",
          "description": "Link a venue_brand to the target venue via the M:N `venue_has_brand` link with effective dates. A venue can carry multiple concurrent brands (a multi-concept day-cafe / night-bar venue carries two active links); a single venue brand attaches to many venues across many properties (Cecconi's at 15+ Soho House locations). The temporal link tracks the participation window so brand transitions can be expressed without losing history."
        },
        {
          "api_name": "hospitality.action.assign_operator_to_venue",
          "description": "Attach a white-label operator company to the target venue via the temporal `venue_has_operator` link. Distinct from `assign_brand_to_venue` (the brand customers see) and from property ownership — three independent concerns. The operator concern surfaces in Hakkasan-at-Wynn and Catch-at-W patterns; the operator company is a `company` row, not a new object type."
        },
        {
          "api_name": "hospitality.action.end_residency",
          "description": "Close an active venue_residency link by setting `effective_to`. Supersedes-not-mutates per design-spec §B.2 — the historical residency stays queryable via `as_of` lookups so an operator can audit which chef ran the venue during a given season."
        },
        {
          "api_name": "hospitality.action.list_retail_product_at_venue",
          "description": "List a retail product as available at the target venue, with effective dates. Hard validator (proposal 0004 §7) enforces `retail_product.is_dtc_only = false` — DTC-only products cannot be listed in-venue."
        },
        {
          "api_name": "hospitality.action.log_pre_shift_lineup",
          "description": "Record a pre-shift lineup event (or its scheduled-but-skipped absence) for the target venue. High-frequency by design — the shape is the simplest possible event-recording verb so the friction cost doesn't suppress the data the cadence-drift pattern-review query depends on."
        },
        {
          "api_name": "hospitality.action.record_service_recovery",
          "description": "Log a service-recovery event against the target venue. The comp_authority_snapshot freezes the toolkit's role rules at recovery time so a later toolkit edit doesn't retroactively re-evaluate authorisation. Soft validators warn on over-limit comp but never block — over-limit recoveries are the data the operator most needs and would otherwise hide."
        },
        {
          "api_name": "hospitality.action.record_venue_visit",
          "description": "Log a non-reservation visit to the target venue by a person — the bar host's book turning into structured data. Independent of recognition: a visit may pass without a typed gesture, in which case no recognition event is recorded. The companion `recognise_person` action handles recognition-tier ladder transitions, with `record_recognition_event` from proposal 0003 handling the typed gesture itself."
        },
        {
          "api_name": "hospitality.action.start_residency",
          "description": "Start a residency at the target venue — chef pop-up, partner-brand activation, talent residency. Creates a temporal `venue_residency` link from the venue to the partner company with an `effective_from` date. End the residency via `end_residency` (sets `effective_to`)."
        },
        {
          "api_name": "hospitality.action.transition_venue_brand",
          "description": "Transition the target venue from one venue_brand to another, closing the existing `venue_has_brand` link with an `effective_to` and opening a new link with `effective_from`. Supersedes-not-mutates per design-spec §B.2 — the historical brand attachment stays queryable via `as_of` lookups."
        }
      ],
      "api_name": "hospitality.venue",
      "description": "A sub-area of a property: room block, restaurant, bar, spa, pool, ballroom, fitness centre, retail outlet, or activity desk. Multi-venue resorts typically have many; a boutique hotel may have one (the rooms block).",
      "display_name": "Venue",
      "domain": "organization",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Venue code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Venue name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Venue kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "capacity",
          "description": "",
          "display_name": "Capacity (integer; meaning declared by `capacity_basis`)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "capacity_basis",
          "description": "",
          "display_name": "Capacity basis (what `capacity` measures)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.venue_brand",
      "description": "A brand identity scoped to a venue, distinct from `brand` (the property/group-level brand). NoMad-the-restaurant, Cecconi's, Cowshed-the-spa, the Connaught Bar, Hélène Darroze — each carries positioning, target-customer mix, creative direction, and relationships to a parent brand (when one exists), a design DNA (when applicable), and an owning company (for white-label / third-party-owned venue brands like Hakkasan-the-brand). N:N with `venue` (Cecconi's at 15 locations; a multi-concept day-cafe/night-bar venue carries two concurrent brands) via the temporal `venue_has_brand` link.",
      "display_name": "Venue brand",
      "domain": "brand_venue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Venue brand code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Venue brand name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Venue brand kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "target_mix",
          "description": "",
          "display_name": "Target customer mix",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "positioning",
          "description": "",
          "display_name": "Market positioning",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "creative_director",
          "description": "",
          "display_name": "Creative director",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "founded_date",
          "description": "",
          "display_name": "Founded on",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "sunset_date",
          "description": "",
          "display_name": "Sunset on (null when active)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "website_url",
          "description": "",
          "display_name": "Website URL",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.venue_visit",
      "description": "A non-reservation visit to a venue by a person — a regular at the Connaught Bar who isn't staying at the hotel that night, a local who pops into Cecconi's for lunch every Wednesday, an out-of-town visitor who stopped in once and tipped well. The bar host's book turning into structured data. Pairs with `recognition_event` from proposal 0003 — visits are the substrate that recognition events are recorded against, but not every visit produces a typed recognition gesture.",
      "display_name": "Venue visit",
      "domain": "brand_venue",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Venue visit id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "visited_on",
          "description": "",
          "display_name": "Visit date (venue local timezone)",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "host_recogniser",
          "description": "",
          "display_name": "Host / maître d' / bartender principal identifier",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "seated_at_text",
          "description": "",
          "display_name": "Seating location",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "party_size",
          "description": "",
          "display_name": "Party size",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "observed_companions_text",
          "description": "",
          "display_name": "Observed companions (free text — see §11 open question 6)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "host_stand_notes",
          "description": "",
          "display_name": "Host stand notes (preferences, usuals, observations)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "arrived_at",
          "description": "",
          "display_name": "Arrived at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "departed_at",
          "description": "",
          "display_name": "Departed at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "covers_count",
          "description": "",
          "display_name": "Covers count (F&B venues)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Free-form host notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.accept_waitlist_entry",
          "description": "Turn the wait into a booking. Replays the entry's request through `create_reservation` itself — not a bypass — so the full availability check, rate restrictions and stay bounds re-run at accept time against the house as it is *now*. The queue is never trusted: an entry offered this morning refuses this afternoon if the room went. The rate is re-decided here (`nightly_rate`, falling back to the entry's indicative `quoted_rate`; with neither, it refuses) because a rate quoted three weeks ago against a sold-out night is not a price the house still owns. An offered entry's held room-nights go back in the same transaction just before the create runs, so the room the offer kept is the room it books. On success the entry goes `accepted` and points at the reservation it became. Refuses an entry already accepted, cancelled, or expired."
        },
        {
          "api_name": "hospitality.action.cancel_waitlist_entry",
          "description": "Withdraw the wait — the guest booked elsewhere, or the desk is clearing a stale list. Re-cancelling is a no-op. Refuses an accepted entry (the reservation it became is the thing to cancel, through `cancel_reservation`) and an expired one (the night audit already closed it)."
        },
        {
          "api_name": "hospitality.action.claim_waitlist_offer",
          "description": "**Claim-route only.** The guest took the room their waitlist offer's link holds. Checks the link's secret against the entry's stored hash and that the offer is still `offered` and inside its deadline — one refusal for every way it can fail — then books through `accept_waitlist_entry`, so availability, restrictions and stay bounds re-run against the house as it is now. Stamps `claimed_at`; the same link again answers as the first claim did and books nothing more. Emits `waitlist.entry.claimed`. **Must not be exposed through OpenAPI, SDKs, or MCP.**"
        },
        {
          "api_name": "hospitality.action.lapse_waitlist_offer",
          "description": "An offer left past its deadline: give its held room-nights back to the ledger and return the entry to `waiting`, stamped `offer_lapsed_at`. The release is what passes the room on — the matcher sees the night free and offers it to the next entry in working order — and the matcher passes over a lapsed entry from then on, so an unanswered phone cannot keep the same room round and round. A no-op on an entry that is not offered or whose deadline has not passed, so a stale or repeated firing is harmless."
        },
        {
          "api_name": "hospitality.action.offer_waitlist_entry",
          "description": "The desk reached the guest about a room that has freed up — OPERA's Can Move followed by the phone call — and keeps it for them: one room-night of the asked type on every ledger night of the stay goes into the ledger's `blocked` count until `offer_deadline` (the desk's own, or the property's `waitlist_offer_hold_minutes`, 30 unset). Refuses when a night of the stay has nothing left to keep. Re-offering re-stamps `offered_at` and restarts the clock but keeps the hold it has rather than taking a second room. Accept books the kept room; cancel gives it back; an offer left past its deadline lapses (`lapse_waitlist_offer`) and the room goes to the next entry. Refuses an entry that is already accepted, cancelled, or expired. At a house with `waitlist_offer_messaging` on, the offer is also sent to the guest — by SMS, or email when there is no mobile number — with a claim link, unless `notify_guest` is false."
        }
      ],
      "api_name": "hospitality.waitlist_entry",
      "description": "A reservation request the house could not take, waiting for inventory to free up. Not a reservation status and not a reservation: nothing traverses a waitlist entry, so it consumes no availability by construction. Carries the request (property, guest, room type, rate plan, dates, occupancy) so `accept_waitlist_entry` can replay it through the ordinary guarded create path, plus the wait's own bookkeeping — `reason` for turnaway analysis, `priority` tier and `requested_at` for the working order (tier, then FIFO within tier), and `telephone` for OPERA's call-to-confirm flow. An offer — the desk's, or the automation lane's matcher on a freed room-night — keeps the room-nights for the guest in the ledger's `blocked` count until `offer_deadline`, listing them in `held_nights` so accept, cancel, a lapse and expiry give back exactly what was held; a lapsed offer returns the entry to waiting and the matcher passes the room to the next. The night audit expires the entry two business days after its requested departure.",
      "display_name": "Waitlist entry",
      "domain": "booking",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Waitlist entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reason",
          "description": "",
          "display_name": "Why the request could not be booked",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "priority",
          "description": "",
          "display_name": "Priority tier (FIFO applies within a tier)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "requested_at",
          "description": "",
          "display_name": "When the guest asked (defaults to now; the FIFO key)",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "arrival_date",
          "description": "",
          "display_name": "Requested arrival",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "departure_date",
          "description": "",
          "display_name": "Requested departure",
          "kind": "date",
          "required": true
        },
        {
          "api_name": "adults",
          "description": "",
          "display_name": "Adults",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "children",
          "description": "",
          "display_name": "Children",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "quoted_rate",
          "description": "",
          "display_name": "Rate quoted when the request was taken (indicative only)",
          "kind": "money",
          "required": false
        },
        {
          "api_name": "telephone",
          "description": "",
          "display_name": "Number to call when the room frees (the call-to-confirm flow)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Desk notes on the request",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "offered_at",
          "description": "",
          "display_name": "When the desk last reached the guest with an offer",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "offer_notes",
          "description": "",
          "display_name": "What was offered on the last contact, and what the guest said",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "accepted_at",
          "description": "",
          "display_name": "When the entry became a reservation",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancelled_at",
          "description": "",
          "display_name": "When the wait was withdrawn",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "cancel_reason",
          "description": "",
          "display_name": "Why the wait was withdrawn",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "expired_at",
          "description": "",
          "display_name": "When the night audit aged the entry out",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "offer_deadline",
          "description": "",
          "display_name": "When the current offer lapses and its held nights go back",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "offer_origin",
          "description": "",
          "display_name": "Who made the current offer: the desk, or the matcher on a freed room-night",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "held_nights",
          "description": "",
          "display_name": "The ledger nights the current offer holds (JSON array of dates)",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "offer_lapsed_at",
          "description": "",
          "display_name": "When an offer last lapsed unanswered; the matcher passes over the entry after",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "guest_notified_at",
          "description": "",
          "display_name": "When the current offer was sent to the guest",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "guest_notice_ref",
          "description": "",
          "display_name": "The outbound message that told the guest of the current offer",
          "kind": "reference",
          "required": false
        },
        {
          "api_name": "guest_notice_note",
          "description": "",
          "display_name": "Why the guest was not sent the current offer, when they were not",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "claim_token_hash",
          "description": "",
          "display_name": "SHA-256 of the current offer's claim-link secret",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "claimed_at",
          "description": "",
          "display_name": "When the guest took the offer through its claim link",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.wellness_equipment",
      "description": "A physical instance of a wellness modality. Decoupled from the modality concept because operators carry portable equipment that moves between venues and equipment that supports multiple modalities. Operator-side maintenance discipline (serial, install, service cadence) lives on the equipment row for compliance reporting on regulated equipment (HBOT chambers, cryo units).",
      "display_name": "Wellness equipment",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Equipment code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Equipment name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "modality_ref",
          "description": "",
          "display_name": "Primary modality this equipment provides",
          "kind": "reference",
          "required": true
        },
        {
          "api_name": "is_portable",
          "description": "",
          "display_name": "Portable (doesn't carry a stable venue link)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "serial_number",
          "description": "",
          "display_name": "Serial number",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "manufacturer",
          "description": "",
          "display_name": "Manufacturer",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "model",
          "description": "",
          "display_name": "Model",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "installed_at",
          "description": "",
          "display_name": "Installed on",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "last_serviced_at",
          "description": "",
          "display_name": "Last service date",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "next_service_due",
          "description": "",
          "display_name": "Next service due (regulator inspection or maintenance)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "service_cadence_days",
          "description": "",
          "display_name": "Service cadence (days)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "notes",
          "description": "",
          "display_name": "Operator notes",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.wellness_modality",
      "description": "A type of wellness service available at one or more venues. Distinct from `service_item` because modalities carry licensing context, evidence-level metadata, and capacity-model classification that the POS-catalogue shape does not type. The `requires_medical_licensing` flag plus the assessment side's structural payload-empty contract (commit 5) is the legal-architecture seam — clinical records stay in the external system; Aerandil holds only the opaque pointer.",
      "display_name": "Wellness modality",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "code",
          "description": "",
          "display_name": "Modality code",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "name",
          "description": "",
          "display_name": "Modality name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "Modality kind",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "evidence_level",
          "description": "",
          "display_name": "Evidence level (substantive-vs-gimmick filter)",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "capacity_model",
          "description": "",
          "display_name": "Capacity model",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "requires_medical_licensing",
          "description": "",
          "display_name": "Booking requires external_record_reference (clinical seam)",
          "kind": "bool",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "typical_duration_minutes",
          "description": "",
          "display_name": "Typical duration (minutes)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "licensing_jurisdiction",
          "description": "",
          "display_name": "Licensing jurisdiction (free text — regulator authority)",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "contraindications_text",
          "description": "",
          "display_name": "Operator's contraindications summary (not clinical screening)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "evidence_source_url",
          "description": "",
          "display_name": "Evidence source URL (when the operator cites a specific base)",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.wellness_protocol",
      "description": "A prescribed pattern of modality sessions for a member. The non-clinical equivalent of a prescription. Distinct from `coaching_plan` because the protocol is a typed modality-pattern prescription (modality X, frequency Y, duration Z) and the coaching plan is a narrative trajectory. Both ship because operator practice observed treats them as separate.",
      "display_name": "Wellness protocol",
      "domain": "membership",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Protocol id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "prescribed_at",
          "description": "",
          "display_name": "When the protocol was prescribed",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "frequency_per_week",
          "description": "",
          "display_name": "Sessions per week (0 admissible for one-off protocols)",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "duration_weeks",
          "description": "",
          "display_name": "Protocol duration in weeks (null for open-ended)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "prescribing_practitioner_text",
          "description": "",
          "display_name": "Practitioner who prescribed (free text — staff or contractor)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "goals_text",
          "description": "",
          "display_name": "Member's stated goals",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "progress_notes",
          "description": "",
          "display_name": "Operator's running notes on member adherence + response",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "target_completion_date",
          "description": "",
          "display_name": "Target completion",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "Actual completion",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "is_active",
          "description": "",
          "display_name": "Currently active (false on completion or early termination)",
          "kind": "bool",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.advance_maintenance_plan",
          "description": "Move a plan on once the work order it raised has closed — the completion monitor's effect. Complete: the next due date is the last plus the interval for a `fixed` plan (skipping slots a late completion overran, never raising a backlog), or the completion date plus the interval for a `floating` one; the plan returns to `scheduled`. Void: the order was withdrawn, not done — the plan returns to `scheduled` on the same due date, owed again. A no-op for an order no plan raised, an order still open, or a plan not waiting on it, so a repeated firing moves nothing twice. Emits `maintenance_plan.advanced`."
        },
        {
          "api_name": "hospitality.action.assign_work_order",
          "description": "Assign (or reassign) the work order to an engineer — the edge re-points, so an order has at most one current assignee and the engineering board is a projection. Refuses an order already complete or void. Name `group_id` instead of `staff_member_id` (exactly one, 2026-10-14) to give the order to a worker group — an active maintenance or other group at the order's house, whose `floors` (when it names any) include the floor of the order's room; the member who starts it takes it. An order is assigned to a person or a group, never both: each assignment replaces the other edge. Emits `work_order.assigned`, or `work_order.assigned_to_group` with the group's members."
        },
        {
          "api_name": "hospitality.action.block_work_order",
          "description": "The work is waiting on something outside the engineer's control — parts on order, a contractor, guest access. The order stays on the board with the reason attached; schedule_work_order or start_work_order is the resume."
        },
        {
          "api_name": "hospitality.action.complete_work_order",
          "description": "The work is done: terminal complete, with what was actually done recorded as the resolution. If the order holds an out-of-order block (work_order_holds_block), the block releases through its own release path — ledger rows restored, the room re-entering its declared return status (overridable here) — so finishing the repair and returning the room to inventory is one verb, never two writes to forget. A resolution is also written as the `resolution` line on the order's thread (2026-10-13), in the same transaction. Completing a postponed order clears the postponement. Re-completing a complete order is a no-op."
        },
        {
          "api_name": "hospitality.action.postpone_work_order",
          "description": "Defer the work to a named date, with the reason — hubOS's postponed fault. open / scheduled / blocked → postponed, stamping postponed_until (a day still to come at the house: never before the hotel date, and refused once its local midnight has passed — so tomorrow or later; a wait within the day is block_work_order), postponed_until_at (the house's local midnight starting that date) and postpone_reason on the order and writing a `postponement` line on its thread in the same transaction. Postponing a postponed order again moves the date (another line); repeating the same date and reason is a no-op. resume_work_order brings it back to open — the standing sweep fires it when postponed_until_at passes — and schedule_work_order or start_work_order resume it too; complete and void still apply. A held out-of-order block keeps its own dates — postponing the repair does not move the room's downtime."
        },
        {
          "api_name": "hospitality.action.reclaim_work_order",
          "description": "Take an assigned work order that has not been started from whoever holds it (hubOS's reclaim): the assignment edge re-points to the acting staff member in one write. Refused once the order is under way (in progress, or blocked or postponed after a start), complete or void; refused on an order nobody holds and on one held by a worker group; refused to a runner or a housekeeper — a fault is worked by engineering. A supervisor (housekeeping or front-office supervisor, manager on duty, general manager, owner) — or a caller that is not a staff member — may name `assignee_id` to reclaim it onto someone at the order's house; anyone else reclaims only for themself. Reclaiming onto the current holder is a no-op, checked before the state — a Take sent again after the start lands once. `expected_holder_id` names the holder the caller saw (a Take queued on a handset): refused when someone else holds the order by the time it lands, so a queued Take replays safely. Two Takes or assigns of one order queue on its holder lock. Emits `work_order.reclaimed` with the previous assignee; the new assignee's handset is nudged as for an assignment."
        },
        {
          "api_name": "hospitality.action.record_work_order_entry",
          "description": "Add a line to a work order's thread: a `note`, `work` (with minutes_spent), `material` (with the parts used), or a `delegation` (with the role the order is handed to). Any line may also carry time and parts. The author is the staff member behind the principal when there is one (a handset session), linked through `work_order_entry_recorded_by`; `recorded_by` names the principal either way. A delegation also stamps the role on the order (`delegated_to_role`, the board's delegated filter) until complete_work_order or void_work_order clears it; one to the role the order already stands with writes the line and moves nothing (its event says `role_changed: false`, so nobody is nudged, 2026-10-17). Refused on a voided order; a completed order takes notes only. Postponement and resolution lines are written by postpone_work_order and complete_work_order, never here. Replay-safe under the apply route's Idempotency-Key — the handset's offline queue sends one per line. A part naming a catalogued `material_id` (2026-10-14) is consumed from its `store_id` — or the house's default workshop — in the same transaction (a `consumption` movement on the order, `stock_movement_for_work_order`), refused when it would take the store below zero unless the store allows it; its description defaults to the material's name and its unit is the material's. A free-text part moves no stock. The part comes out of the acting role's own stores, as on every stock verb (2026-10-17): an engineer's session from a workshop, a housekeeping supervisor's from any store but one — the default workshop included — else the line is refused (403)."
        },
        {
          "api_name": "hospitality.action.resume_work_order",
          "description": "Bring a postponed work order back onto the board: postponed → open, clearing postponed_until, postponed_until_at and postpone_reason (and a scheduled_for the postponement overtook), and writing a `note` line on its thread that says it came back and from which date, in the same transaction. The standing automation `ops_automations::postponed_work_order_resume` fires it when the house's day reaches the date (postponed_until_at); an engineer may bring one back early. Refused on an order that is not postponed — so a second firing, or a replay after the order moved on, changes nothing. Emits `work_order.resumed`."
        },
        {
          "api_name": "hospitality.action.schedule_work_order",
          "description": "Commit the work to a date: open / blocked / postponed → scheduled (re-scheduling a scheduled order just moves the date). Clears any block reason and postponement — a scheduled order is no longer waiting, and scheduling a postponed order is its resume."
        },
        {
          "api_name": "hospitality.action.start_work_order",
          "description": "The engineer is on the job: open / scheduled / blocked / postponed → in_progress, stamping started_at and clearing any block reason or postponement (starting a blocked order is the retry after parts arrived; starting a postponed one is its resume). An order assigned to a worker group (2026-10-14) is taken by the member who starts it — the group edge re-points to them — and a staff-member session outside the group is refused (403); a caller that is not a staff member starts it and leaves it the group's."
        },
        {
          "api_name": "hospitality.action.void_work_order",
          "description": "Withdraw an order raised in error, with the reason. Completed work is history and refuses. A held out-of-order block releases with the void — a withdrawn order never leaves the room silently blocked. Re-voiding is a no-op."
        }
      ],
      "api_name": "hospitality.work_order",
      "description": "A maintenance request, typically against a room or venue. Tracks severity (cosmetic through to safety) through an explicit lifecycle: assign, schedule, start, complete (with a resolution), void (with a reason). Whether the room is out of order while the order is open is never a flag here — it is a real `room_status_block` held via `work_order_holds_block`, minted through the same code path as `take_room_out_of_order` and released on completion. The order is a thread (2026-10-13): its `work_order_entry` lines log notes, time, parts, hand-offs, the postponement and the resolution; it may name the equipment it is for (`work_order_for_asset`, its id carried as `asset_id`) and the guest stay that reported it (`work_order_reported_by_stay`), and `postpone_work_order` defers it to a named date (`resume_work_order` brings it back when the date comes). A hand-off on the thread stamps `delegated_to_role` until the order closes. Since 2026-10-15 a fault is for a room (`work_order_for_room`) or a public area (`work_order_for_area`, `create_public_area_work_order`), never both; it may name the room `element` it is in and the `fault_type` it was raised from; and it carries its holder's id (`assignee_id`, or a worker group's `group_id`) beside the edge every assigning verb moves.",
      "display_name": "Work order",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Work order id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "title",
          "description": "",
          "display_name": "Title",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Description",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "category",
          "description": "",
          "display_name": "Category",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "severity",
          "description": "",
          "display_name": "Severity",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "reported_at",
          "description": "",
          "display_name": "Reported at",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "scheduled_for",
          "description": "",
          "display_name": "Date the work is committed to",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "When the engineer started",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "block_reason",
          "description": "",
          "display_name": "What the work is waiting on while blocked",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "resolved_at",
          "description": "",
          "display_name": "Resolved at",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "resolution",
          "description": "",
          "display_name": "What was actually done",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "void_reason",
          "description": "",
          "display_name": "Why a voided order was withdrawn",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "maintenance_plan_id",
          "description": "",
          "display_name": "The maintenance plan that raised this order (its object id); absent otherwise",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "postponed_until",
          "description": "",
          "display_name": "The date a postponed order is deferred to (present while postponed)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "postpone_reason",
          "description": "",
          "display_name": "Why the order was postponed (present while postponed)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "postponed_until_at",
          "description": "",
          "display_name": "The instant the house's day reaches postponed_until (its local midnight) — when the standing sweep brings the order back with resume_work_order (present while postponed)",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "reported_by_guest",
          "description": "",
          "display_name": "Whether a guest reported the fault — true when the order names the reporting stay (`work_order_reported_by_stay`) or the triaged guest message that raised it",
          "kind": "bool",
          "required": false
        },
        {
          "api_name": "asset_id",
          "description": "",
          "display_name": "The asset the order is for (its object id) — the `work_order_for_asset` edge's target; absent when the order names no equipment",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "delegated_to_role",
          "description": "",
          "display_name": "The staff role the order was last handed to by a delegation line on its thread (present until it completes or is voided)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "element",
          "description": "",
          "display_name": "The room element the fault is in (light, air_conditioning, shower …) — the attendant's word for it; `category` is the trade that mends it",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "fault_type_id",
          "description": "",
          "display_name": "The fault type the order was raised from (its object id) — the `work_order_of_fault_type` edge's target",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "area_id",
          "description": "",
          "display_name": "The public area the fault is in (its object id) — the `work_order_for_area` edge's target; absent for a room's order",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "assignee_id",
          "description": "",
          "display_name": "The staff member who holds the order (its object id) — the `work_order_assigned_to` edge's target; written by every verb that moves the edge",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "group_id",
          "description": "",
          "display_name": "The worker group that holds the order (its object id) — the `work_order_assigned_to_group` edge's target; never beside `assignee_id`",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [],
      "api_name": "hospitality.work_order_entry",
      "description": "One line on a work order's thread — hubOS's fault logbook thread as typed data: a `note`, `work` (minutes spent), `material` (parts used), a `delegation` to another role, the `postponement` (written by postpone_work_order, with the date and reason) or the `resolution` (written by complete_work_order). Linked to its order (`work_order_entry_for_work_order`) and to the staff member who wrote it when one did (`work_order_entry_recorded_by`); `recorded_by` names the principal either way. Append-only: a correction is another line. Photos hang off an entry as attachments. The order's time and parts are the sum of its entries — a query, not a stored total.",
      "display_name": "Work order entry",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Work order entry id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "kind",
          "description": "",
          "display_name": "What the line records: note, work, material, delegation, postponement, resolution",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "text",
          "description": "",
          "display_name": "The words on the line (required for a note, a postponement's reason, a resolution)",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "minutes_spent",
          "description": "",
          "display_name": "Time spent on the work this line logs, in minutes (at most a week per line)",
          "kind": "int",
          "required": false
        },
        {
          "api_name": "materials",
          "description": "",
          "display_name": "Parts used: description, quantity, optionally a unit, and the catalogued material and store a consumed part came from",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "delegated_to_role",
          "description": "",
          "display_name": "The role the order is handed to (a delegation line)",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "postponed_until",
          "description": "",
          "display_name": "The date the order was postponed to (a postponement line)",
          "kind": "date",
          "required": false
        },
        {
          "api_name": "recorded_at",
          "description": "",
          "display_name": "When the line was written — the handset's own time when it queued offline",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "recorded_by",
          "description": "",
          "display_name": "The principal that wrote the line (`user:…`, `agent:…`, `service:…`, `system`)",
          "kind": "string",
          "required": true
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.retire_worker_group",
          "description": "Retire a worker group: it takes no more work, and its open work — housekeeping tasks not complete or skipped, work orders not complete or void — goes back to the open board, unassigned, so nothing is left waiting on a group nobody works in. Its membership is kept as history. Retiring a retired group is a no-op; `define_worker_group` with its name revives it. Emits `worker_group.retired` naming the work it released."
        },
        {
          "api_name": "hospitality.action.set_worker_group_members",
          "description": "Set who is in the group: `member_ids` is the whole membership — staff members missing from it leave the group, new ones join, and an empty list empties it. Every member works at the group's house (a staff member held above the house — a brand's, a tenant's — may join). The same membership again writes nothing. Refuses a retired group. Work already assigned to the group stays the group's: a member who leaves can no longer start it. Emits `worker_group.members_set` with who joined and who left."
        }
      ],
      "api_name": "hospitality.worker_group",
      "description": "A named group of staff at a house that work is assigned to — the third-floor runners, the public-areas team, the night maintenance crew (hubOS's workers groups and its valets by floor). `name` is unique among the house's groups (`define_worker_group` upserts by it); `department` says whose work it takes — a housekeeping task goes to a `housekeeping`, `public_areas` or `other` group, a work order to a `maintenance` or `other` one; `floors`, when given, are the floors it covers, and a task in a room or area on another floor is refused to it. Members are `worker_group_has_member` edges (`set_worker_group_members`). Work assigned to the group (`housekeeping_task_assigned_to_group`, `work_order_assigned_to_group`) is taken by the member who starts it; a staff member outside the group may not start it. A `retired` group takes no work, and its open work returns to the open board.",
      "display_name": "Worker group",
      "domain": "operations",
      "interfaces": [],
      "properties": [
        {
          "api_name": "name",
          "description": "",
          "display_name": "What the house calls the group (unique among its groups)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "department",
          "description": "",
          "display_name": "Whose work the group takes: housekeeping, maintenance, public areas or other",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "floors",
          "description": "",
          "display_name": "The floors the group covers, in the rooms' own words; absent: every floor",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Active (takes work) or retired",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "retired_at",
          "description": "",
          "display_name": "When the group was retired (present while retired)",
          "kind": "datetime",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.deprecate_workflow_definition",
          "description": "Prevents new runs without disturbing existing runs. Validates the target is `active` or `draft`; sets `status = deprecated` and stamps `deprecated_at`. New runs against the deprecated version are rejected by `workflow_run_requires_active_definition`; existing runs continue under their pinned `definition_hash`."
        },
        {
          "api_name": "hospitality.action.start_workflow_run",
          "description": "Start a durable execution pinned to an active definition version. Inserts a `workflow_run` (status `pending`), copies `definition_hash`, freezes `input_snapshot` and `target_refs_snapshot`, inserts initial `workflow_step_run` rows with non-root steps in `planned` and root steps (`depends_on = []`) synchronously promoted to `ready` before commit (AWS Step Functions `StartAt` precedent), creates concrete `workflow_run_targets_*` links per supported target type, and emits §9.1 events `workflow_run.started`, `workflow_step.planned`, `workflow_step.ready`. Does not start a runner — advancement requires the future executor actions."
        }
      ],
      "api_name": "hospitality.workflow_definition",
      "description": "Versioned, declarative workflow template. A definition pins step graph, schemas, retry/timeout/compensation policies, and the `definition_hash` over the canonical JSON form so every run can prove what shape it executes. After `status = active` all step/schema/policy fields are immutable; changes create a new version (proposal 0020 §4.1).",
      "display_name": "Workflow definition",
      "domain": "workflows",
      "interfaces": [],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Definition id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "workflow_key",
          "description": "",
          "display_name": "Stable key under the tenant namespace (e.g. `late_checkout_exception`)",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "version",
          "description": "",
          "display_name": "Operator-controlled semantic version or date. Immutable after activation.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "title",
          "description": "",
          "display_name": "Human-readable name",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "description",
          "description": "",
          "display_name": "Bounded to 2048 characters",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "trigger_kind",
          "description": "",
          "display_name": "What kind of caller starts runs of this definition",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "input_schema",
          "description": "",
          "display_name": "JSON-Schema-shaped typed parameter contract for `start_workflow_run`",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "output_schema",
          "description": "",
          "display_name": "JSON-Schema-shaped result contract for completed runs",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "step_definitions",
          "description": "",
          "display_name": "Ordered step definitions per §4.1.1. Stored as JSON for v1 — the typed step-definition shape (step_key, step_type, depends_on, action_name, parameter_template, idempotency_scope, retry_policy, compensation, approval_policy, wait_policy, branch_policy, subworkflow_ref, external_task_kind, executor_policy, risk_tier) is enforced by the workflow_definition validators in commit 9.",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "default_retry_policy",
          "description": "",
          "display_name": "Default retry settings inherited by steps unless overridden",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "timeout_policy",
          "description": "",
          "display_name": "Run-level timeout / expiry settings",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "compensation_policy",
          "description": "",
          "display_name": "How the definition handles compensation after a failed terminal run",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "high_risk_policy",
          "description": "",
          "display_name": "How the definition treats steps with high `risk_tier`",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "owner_principal",
          "description": "",
          "display_name": "`Principal::stable_id()` of the maintainer / service owning the definition",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "published_at",
          "description": "",
          "display_name": "Set when draft becomes active",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "deprecated_at",
          "description": "",
          "display_name": "Set when status becomes deprecated",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "definition_hash",
          "description": "",
          "display_name": "SHA-256 over canonical JSON of the definition. Runs pin this value.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "changelog_note",
          "description": "",
          "display_name": "Human explanation for active/deprecated versions",
          "kind": "text",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.cancel_workflow_run",
          "description": "Request cancellation and optional compensation of a running workflow. Transitions the run to `cancel_requested`; cancels ready / waiting steps; enters `compensating` when `compensate_completed_steps = true` and any successful terminal steps are compensatable, otherwise transitions directly to `canceled`. Advancing from `compensating` to `compensated` / `compensation_failed` requires the future runner or a trusted host to call the declared compensations and then `record_compensation_result`. The cancellation event uses the calling principal; per-compensation events carry their proximate executor with `delegated_from_principal` pointing back to the canceller."
        },
        {
          "api_name": "hospitality.action.expire_workflow_run",
          "description": "Expire a non-terminal workflow run whose `expires_at` has elapsed. Pending steps (planned / ready / waiting_*) transition to `expired` with `workflow_step.expired` events; the run transitions to `expired` and emits `workflow_run.expired`. Rejected while `expires_at` is absent or still in the future. No compensation is attempted — an operator who wants compensation cancels with `compensate_completed_steps = true` before expiry."
        }
      ],
      "api_name": "hospitality.workflow_run",
      "description": "One durable execution pinned to exactly one active definition version. `definition_hash` is copied at start so the run can prove the exact shape it executed even if the definition is later deprecated. `target_refs_snapshot` is a redacted convenience snapshot — graph traversal uses concrete `workflow_run_targets_*` links. The runner that advances steps is a separate proposal (§9.2).",
      "display_name": "Workflow run",
      "domain": "workflows",
      "interfaces": [
        "hospitality.interface.expirable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "definition_id",
          "description": "",
          "display_name": "Workflow definition object id. Part of the durable idempotency key.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "run_key",
          "description": "",
          "display_name": "Host-supplied business / dedupe key. Immutable.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "workflow_key",
          "description": "",
          "display_name": "Copied from definition at start time",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "workflow_version",
          "description": "",
          "display_name": "Copied from definition at start time",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "definition_hash",
          "description": "",
          "display_name": "Copied from definition at start time. Drift rejects step execution.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "trigger_kind",
          "description": "",
          "display_name": "Copied from start request or definition default",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "input_snapshot",
          "description": "",
          "display_name": "Frozen, redacted start input",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "request_fingerprint",
          "description": "",
          "display_name": "SHA-256 over canonical start request fields. Same run_key with a different fingerprint is rejected.",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "output_snapshot",
          "description": "",
          "display_name": "Frozen completion output",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "target_refs_snapshot",
          "description": "",
          "display_name": "Frozen `{object_type, object_id}` targets requested at start. Convenience snapshot only — graph traversal uses concrete `workflow_run_targets_*` links and never embeds payloads.",
          "kind": "array",
          "required": false
        },
        {
          "api_name": "current_step_keys",
          "description": "",
          "display_name": "Active / waiting step keys for fast display",
          "kind": "array",
          "required": true
        },
        {
          "api_name": "initiated_by",
          "description": "",
          "display_name": "`Principal::stable_id()` of caller that started the run",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "delegated_agent_installation_id",
          "description": "",
          "display_name": "Set when an installed agent initiated the run; mirrors the `workflow_run_started_by_agent_installation` link target",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "Wall-clock start",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "last_transition_at",
          "description": "",
          "display_name": "Updated on every lifecycle transition",
          "kind": "datetime",
          "required": true
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "Terminal success / failure / cancellation / compensation terminal",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "expires_at",
          "description": "",
          "display_name": "Run timeout",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "correlation_id",
          "description": "",
          "display_name": "Root correlation id for event joins",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "idempotency_key",
          "description": "",
          "display_name": "Key used to start the run",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "failure_summary",
          "description": "",
          "display_name": "Bounded terminal failure summary; full details stay on step runs",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "compensation_summary",
          "description": "",
          "display_name": "Bounded compensation outcome",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "contestability_ref",
          "description": "",
          "display_name": "External support / appeal reference for guest-impacting automated decisions",
          "kind": "string",
          "required": false
        }
      ]
    },
    {
      "actions": [
        {
          "api_name": "hospitality.action.dispatch_workflow_step_to_agent",
          "description": "Task an agent installation with a workflow step over the proposal 0022 wire: enqueue one pending `participant_dispatch_attempt` (kind `workflow_step_invocation`) the dispatcher worker signs and POSTs to the installation's `dispatch_endpoint_url`. The payload carries the installation's registry `agent_key` plus the caller-supplied natural-language `task`; completion flows back through the 0022 receiver routes, which advance this step via the internal step-attempt/result actions. The installation must be enabled, dispatch-configured, and — when it carries an `allowed_workflow_keys` allowlist — allowlisted for this run's workflow_key. One in-flight dispatch per step: re-dispatch is rejected while a prior dispatch row is still pending/in-flight/acknowledged. Emits `workflow_step.dispatched_to_agent`."
        },
        {
          "api_name": "hospitality.action.record_compensation_result",
          "description": "Records the outcome of a declared compensating action / function / manual step against the original successful terminal step. When `compensation_step_run_id` is set the handler creates the `workflow_step_run_compensates_step_run` self-link from the compensating row to the original. Recomputes the parent run's compensation state and emits `workflow_step.compensated` / `workflow_step.compensation_failed`. Failed compensation is terminal evidence, never an excuse to delete the original step."
        },
        {
          "api_name": "hospitality.action.record_workflow_step_attempt",
          "description": "**Executor-only.** Records the start of a step attempt. The action engine accepts this transition under `internal:workflow:step_attempt` so a future runner / trusted host executor can claim work without bypassing single-write- surface discipline. **Must not be exposed through OpenAPI, SDKs, or MCP**; the `workflow_internal_scopes_not_publicly_issuable` validator (commit 11) rejects tenant-managed principals that present the scope. Increments `attempt_count`, sets status `running`."
        },
        {
          "api_name": "hospitality.action.record_workflow_step_result",
          "description": "**Executor-only.** Records the terminal or waiting outcome of a step attempt. Same internal-scope discipline as `record_workflow_step_attempt`. Updates step status, result/error snapshots, parent run status, and emits §9.1 events `workflow_step.succeeded` / `.failed_retryable` / `.failed_terminal` / `.waiting_for_event` / `.waiting_for_retry` / `.skipped`. Terminal failure on a run with compensatable steps transitions the parent to `failed_pending_compensation` or `compensating` per the definition's compensation policy."
        }
      ],
      "api_name": "hospitality.workflow_step_run",
      "description": "One step's runtime state inside a workflow run. A retry increments `attempt_count`; a new step run row is created only for fan-out branches or subworkflow children. `context_hash` is the canonical JCS+SHA-256 over the proposal §4.4.2 input set; drift supersedes any held approval. The self-reference for compensation is on the `workflow_step_run_compensates_step_run` link, not an inline UUID.",
      "display_name": "Workflow step run",
      "domain": "workflows",
      "interfaces": [
        "hospitality.interface.approvable"
      ],
      "properties": [
        {
          "api_name": "id",
          "description": "",
          "display_name": "Step run id",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "step_key",
          "description": "",
          "display_name": "Matches the definition step key",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "step_type",
          "description": "",
          "display_name": "Copied from definition",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "status",
          "description": "",
          "display_name": "Lifecycle status",
          "kind": "enum",
          "required": true
        },
        {
          "api_name": "action_name",
          "description": "",
          "display_name": "Copied from definition for action steps",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "function_name",
          "description": "",
          "display_name": "Copied from definition for function steps",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "target_ref",
          "description": "",
          "display_name": "`{object_type, object_id}` resolved at runtime",
          "kind": "struct",
          "required": false
        },
        {
          "api_name": "parameter_snapshot",
          "description": "",
          "display_name": "Frozen parameters after template resolution",
          "kind": "json",
          "required": true
        },
        {
          "api_name": "context_hash",
          "description": "",
          "display_name": "Canonical JCS+SHA-256 over the §4.4.2 input set",
          "kind": "string",
          "required": true
        },
        {
          "api_name": "idempotency_key",
          "description": "",
          "display_name": "Durable key used for mutating action/function/external call",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "attempt_count",
          "description": "",
          "display_name": "Starts at 0; increments before each attempt",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "max_attempts",
          "description": "",
          "display_name": "From retry policy",
          "kind": "int",
          "required": true
        },
        {
          "api_name": "next_attempt_at",
          "description": "",
          "display_name": "Set while waiting for retry",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "started_at",
          "description": "",
          "display_name": "First attempt start",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "completed_at",
          "description": "",
          "display_name": "Terminal step time",
          "kind": "datetime",
          "required": false
        },
        {
          "api_name": "last_error_code",
          "description": "",
          "display_name": "Structured failure code",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "last_error_summary",
          "description": "",
          "display_name": "Bounded failure summary",
          "kind": "text",
          "required": false
        },
        {
          "api_name": "result_snapshot",
          "description": "",
          "display_name": "Frozen, redacted step result",
          "kind": "json",
          "required": false
        },
        {
          "api_name": "last_result_status",
          "description": "",
          "display_name": "Raw result_status last recorded by the runner; failed_retryable normalizes to status waiting_for_retry.",
          "kind": "enum",
          "required": false
        },
        {
          "api_name": "external_reference",
          "description": "",
          "display_name": "PSP, PMS, Slack, Linear, webhook, or job reference",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "executed_by",
          "description": "",
          "display_name": "`Principal::stable_id()` that executed or resolved the step",
          "kind": "string",
          "required": false
        },
        {
          "api_name": "compensation_role",
          "description": "",
          "display_name": "Marker for rows participating in a compensation relationship. Self-reference uses `workflow_step_run_compensates_step_run`, not an inline UUID.",
          "kind": "enum",
          "required": false
        }
      ]
    }
  ],
  "version": "0.0.1"
}