openapi: 3.1.0
info:
  title: Tagungsregister API
  version: 1.5.0
  description: >-
    Read access to release-bound Tagungshotel and Tagungslocation facts. Publicly
    listed venues may contain explicit gaps. Publication status and eligibility for a
    specific query are separate. Unknown values remain null, imply neither zero nor
    false, and never satisfy a hard constraint. The API does not expose live availability,
    booking, or paid ranking. Agent feedback is a separate authenticated, moderated write channel.
servers:
  - url: /
    description: Current origin
tags:
  - name: Venues
  - name: Evidence
  - name: Releases
  - name: Feedback
paths:
  /v1/event-venues/search:
    post:
      tags: [Venues]
      operationId: searchEventVenues
      summary: Deterministically evaluate eligibility across publicly listed venues
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
            examples:
              offsite:
                value:
                  event:
                    participants: 60
                    seating_style: parliament
                    bedrooms_required: 40
                  required:
                    - criterion: hybrid_setup
                      value: true
                  unknown_policy: separate
                  limit: 10
      responses:
        '200':
          description: Release-bound search result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '400':
          $ref: '#/components/responses/Problem'
        '409':
          $ref: '#/components/responses/Problem'
        '413':
          $ref: '#/components/responses/Problem'
        '414':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/event-venues/{venue_id}:
    get:
      tags: [Venues]
      operationId: getEventVenue
      summary: Get one publicly listed venue by stable ID or slug
      description: Optional planning query returns a bounded decision alongside the profile. Follow decision.page.nextOffset with identical criteria and release to retrieve further options. Planning responses are not cached.
      parameters:
        - $ref: '#/components/parameters/VenueId'
        - { name: participants, in: query, schema: { type: integer, minimum: 1, maximum: 10000 } }
        - { name: seating, in: query, schema: { type: string, enum: [theatre, parliament, banquet, uShape, boardroom, reception], default: parliament } }
        - { name: bedrooms, in: query, schema: { type: integer, minimum: 0 } }
        - { name: daylight, in: query, schema: { type: string, enum: ['true', 'false'] } }
        - { name: origin, in: query, schema: { type: string } }
        - { name: travel, in: query, schema: { type: integer, minimum: 0, maximum: 120 } }
        - { name: amenity, in: query, schema: { type: array, items: { type: string } }, style: form, explode: true }
        - { name: preference, in: query, schema: { type: array, items: { type: string } }, style: form, explode: true, description: Repeated JSON objects with criterion/value/weight as in PlanningContext. }
        - { name: unknown_policy, in: query, schema: { type: string, enum: [include, separate, exclude] } }
        - { name: decision_limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: decision_offset, in: query, schema: { type: integer, minimum: 0, maximum: 10000, default: 0 } }
        - { name: release, in: query, schema: { type: string }, description: A stale release pin returns 409. }
        - name: include
          in: query
          schema:
            type: string
          description: Comma-separated optional sections.
        - name: evidence
          in: query
          schema:
            type: string
            enum: [none, summary, full]
            default: summary
      responses:
        '200':
          description: Published venue profile
          headers:
            ETag:
              schema: { type: string }
            X-Data-Release:
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VenueDetailResponse'
        '304':
          description: Not modified
        '400':
          $ref: '#/components/responses/Problem'
        '414':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/event-venues/compare:
    post:
      tags: [Venues]
      operationId: compareEventVenues
      summary: Compare two to five venues without selecting a winner
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [venue_ids]
              additionalProperties: false
              properties:
                venue_ids:
                  type: array
                  minItems: 2
                  maxItems: 5
                  uniqueItems: true
                  items: { type: string }
                planning:
                  $ref: '#/components/schemas/PlanningContext'
                criteria:
                  type: array
                  maxItems: 25
                  items: { type: string }
                release:
                  type: string
      responses:
        '200':
          description: Field-wise comparison bound to the active release
          content:
            application/json:
              schema:
                type: object
                required: [meta, input, venues, matrix, winner, winner_reason]
                properties:
                  meta: { $ref: '#/components/schemas/Meta' }
                  decision_version: { type: string }
                  decisions:
                    type: array
                    maxItems: 5
                    items: { $ref: '#/components/schemas/VenueDecision' }
                  input: { type: object, additionalProperties: true }
                  venues:
                    type: array
                    items: { $ref: '#/components/schemas/EntitySummary' }
                  matrix:
                    type: array
                    items:
                      type: object
                      required: [criterion, label, unit, is_demo, values]
                      properties:
                        criterion: { type: [string, 'null'] }
                        label: { type: [string, 'null'] }
                        unit: { type: [string, 'null'] }
                        is_demo: { type: boolean }
                        values: { type: object, additionalProperties: true }
                  winner: { type: 'null' }
                  winner_reason: { type: string }
        '400':
          $ref: '#/components/responses/Problem'
        '409':
          $ref: '#/components/responses/Problem'
        '413':
          $ref: '#/components/responses/Problem'
        '414':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/claims/{claim_id}/evidence:
    get:
      tags: [Evidence]
      operationId: getClaimEvidence
      summary: Resolve a published claim to evidence fragments and sources
      parameters:
        - name: claim_id
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: release
          in: query
          description: Optional current-release guard. A different release returns 409; this endpoint does not silently substitute a newer fact or serve historical releases.
          schema: { type: string }
      responses:
        '200':
          description: Claim with addressable evidence and source metadata
          content:
            application/json:
              schema:
                type: object
                required: [meta, claim, evidence]
                properties:
                  meta: { $ref: '#/components/schemas/Meta' }
                  claim: { type: object, additionalProperties: true }
                  citation:
                    type: object
                    description: Release-bound subject names and stable relative paths into the profile. Resolve paths against this API origin.
                    additionalProperties: true
                  evidence:
                    type: array
                    items: { $ref: '#/components/schemas/PublicEvidence' }
                  sources:
                    type: array
                    items: { type: object, additionalProperties: true }
        '304':
          description: Not modified
        '414':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '409':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/event-venues/{venue_id}/changes:
    get:
      tags: [Venues]
      operationId: getVenueChanges
      summary: Get public changes for one venue
      parameters:
        - $ref: '#/components/parameters/VenueId'
      responses:
        '200':
          description: Public venue change log
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '304':
          description: Not modified
        '414':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/changes:
    get:
      tags: [Releases]
      operationId: getCurrentChanges
      summary: Get the global public release change log
      responses:
        '200':
          description: Current release changes
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '304':
          description: Not modified
        '414':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/releases/current:
    get:
      tags: [Releases]
      operationId: getCurrentRelease
      summary: Get the active immutable release envelope
      responses:
        '200':
          description: Active release and contract versions
          headers:
            ETag:
              schema: { type: string }
          content:
            application/json:
              schema:
                type: object
                required: [meta, release]
                properties:
                  meta: { $ref: '#/components/schemas/Meta' }
                  release: { type: object, additionalProperties: true }
        '304':
          description: Not modified
        '414':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/methodology:
    get:
      tags: [Releases]
      operationId: getCurrentMethodology
      summary: Get the current matching methodology
      responses:
        '200':
          description: Current methodology
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '304':
          description: Not modified
        '414':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/methodologies/{version}:
    get:
      tags: [Releases]
      operationId: getMethodologyVersion
      summary: Get current or explicitly named methodology
      parameters:
        - name: version
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Methodology contract
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '304':
          description: Not modified
        '414':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/Problem'
  /v1/agent-feedback:
    get:
      tags: [Feedback]
      operationId: listAgentFeedback
      summary: Latest 50 published agent comments; no private moderation fields
      responses:
        '200':
          description: Moderated feedback feed
          content:
            application/json:
              schema:
                type: object
                required: [items, limit, moderation]
                properties:
                  items:
                    type: array
                    maxItems: 50
                    items: { $ref: '#/components/schemas/AgentFeedback' }
                  limit: { type: integer, const: 50 }
                  moderation: { type: string, const: prepublication }
        '429':
          $ref: '#/components/responses/Problem'
        '503':
          $ref: '#/components/responses/FeedbackError'
    post:
      tags: [Feedback]
      operationId: submitAgentFeedback
      summary: Submit a comment for moderation, with current release and same-venue claim references
      description: Integration attribution is assigned by the operator. Five submissions per rolling 24 hours; requestId is idempotent per integration. No model identity or actual consumption is attested. Body limit 8192 bytes.
      security: [{ AgentFeedbackKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentFeedbackSubmission' }
      responses:
        '202':
          description: Pending submission or previously received requestId; never auto-published
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  status: { type: string, enum: [pending, received] }
        '400': { $ref: '#/components/responses/FeedbackError' }
        '401': { $ref: '#/components/responses/FeedbackError' }
        '413': { $ref: '#/components/responses/FeedbackError' }
        '415': { $ref: '#/components/responses/FeedbackError' }
        '422': { $ref: '#/components/responses/FeedbackError' }
        '429':
          description: Integration daily limit or global request limit exceeded
          content:
            application/json:
              schema: { type: object, properties: { error: { type: string } } }
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
        '503': { $ref: '#/components/responses/FeedbackError' }
components:
  securitySchemes:
    AgentFeedbackKey:
      type: http
      scheme: bearer
  parameters:
    VenueId:
      name: venue_id
      in: path
      required: true
      schema: { type: string }
      description: Stable venue UUID or slug.
  responses:
    FeedbackError:
      description: Feedback-specific error code (no submitted text or credentials)
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error: { type: string }
    Problem:
      description: RFC 9457-style problem details
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    AgentFeedbackSubmission:
      type: object
      additionalProperties: false
      required: [requestId, release, venueId, claimIds, task, usefulness, comment]
      properties:
        requestId: { type: string, format: uuid }
        release: { type: string, minLength: 1, maxLength: 150 }
        venueId: { type: string, format: uuid }
        claimIds:
          type: array
          minItems: 1
          maxItems: 10
          items: { type: string, format: uuid }
        task: { type: string, minLength: 10, maxLength: 300 }
        usefulness: { type: string, enum: [useful, partly, not_useful] }
        comment: { type: string, minLength: 20, maxLength: 1000 }
    AgentFeedback:
      type: object
      additionalProperties: false
      required: [id, integration, origin, createdAt, release, venueId, claimIds, task, usefulness, comment]
      properties:
        id: { type: string, format: uuid }
        integration: { type: string }
        origin: { type: string, enum: [own_test, external] }
        createdAt: { type: string, format: date-time }
        release: { type: string }
        venueId: { type: string, format: uuid }
        claimIds:
          type: array
          items: { type: string, format: uuid }
        task: { type: string }
        usefulness: { type: string, enum: [useful, partly, not_useful] }
        comment: { type: string }
    CertificationDetailValue:
      type: object
      additionalProperties: false
      required: [certificateClaimId, value]
      description: >-
        Value of an event.venue.certification detail Claim (issuer, proof_url, scope,
        valid_from or valid_until). The anchor is the exact same-venue certification
        name Claim, not a shared label. Dates require an explicit valid YYYY-MM-DD;
        proof URLs require HTTP(S) without credentials. No authenticity is inferred.
      properties:
        certificateClaimId: { type: string, format: uuid }
        value: { type: string, minLength: 1, maxLength: 2000 }
    CertificationField:
      type: object
      additionalProperties: false
      required: [state, value, claimIds]
      properties:
        state: { type: string, enum: [recorded, conflict, unknown] }
        value: { type: [string, 'null'], minLength: 1, maxLength: 2000 }
        claimIds: { type: array, uniqueItems: true, items: { type: string, format: uuid } }
      allOf:
        - if:
            properties: { state: { const: recorded } }
          then:
            properties:
              value: { type: string }
              claimIds: { minItems: 1 }
          else:
            properties: { value: { type: 'null' } }
    CertificationProjection:
      type: object
      additionalProperties: false
      required: [nameClaimId, name, nameState, fields, validityPeriod]
      description: >-
        Deterministically recomputed from exact Claims at the release projection
        instant. Missing values remain unknown; conflicting values are not selected
        by recency. The date interval is not a confirmation by the certifier.
      properties:
        nameClaimId: { type: string, format: uuid }
        name: { type: string, minLength: 1 }
        nameState: { type: string, enum: [recorded, unknown] }
        fields:
          type: object
          additionalProperties: false
          required: [issuer, proof_url, scope, valid_from, valid_until]
          properties:
            issuer: { $ref: '#/components/schemas/CertificationField' }
            proof_url: { $ref: '#/components/schemas/CertificationField' }
            scope: { $ref: '#/components/schemas/CertificationField' }
            valid_from: { $ref: '#/components/schemas/CertificationField' }
            valid_until: { $ref: '#/components/schemas/CertificationField' }
        validityPeriod:
          type: string
          enum: [within_stated_period, not_yet_started, ended, conflict, unknown]
          description: UTC calendar-day comparison, end day inclusive. A current period requires both explicit boundaries; review or freshness dates are never substituted.
    Meta:
      type: object
      required:
        - data_release
        - schema_version
        - methodology_version
        - matching_version
      properties:
        request_id: { type: string }
        data_release: { type: string }
        schema_version: { oneOf: [{ type: string }, { type: integer }] }
        methodology_version: { type: string }
        matching_version: { type: string }
        source_matching_version: { type: string, description: Immutable source release version when a decision projection is requested. }
        source_schema_version: { oneOf: [{ type: string }, { type: integer }] }
        decision_version: { type: string, const: tagungsregister-decision-1.0.0 }
        generated_at: { type: [string, 'null'], format: date-time }
    AmenityKey:
      type: string
      enum:
        - hybrid_setup
        - outdoor_area
        - bar_in_house
        - parking
        - ev_charging
        - restaurant
        - wifi
    VenueType:
      type: string
      enum: [conference_hotel, venue_without_rooms, castle_estate, conference_center]
    RoutingOriginId:
      type: string
      enum: [de-berlin-hbf, de-ber, de-potsdam-hbf, de-leipzig-hbf, de-hamburg-hbf]
    TravelRing:
      type: string
      enum: [hbf_ring, umland, rueckzug, outside, undetermined]
    MeasurementStatus:
      type: string
      enum: [accepted, unknown, conflict, stale, expired]
    PublicationStatus:
      type: string
      enum: [listed, partially_verified, comparison_ready]
      description: Public data-maturity stage. Blocked venues are never returned.
    ComparisonArea:
      type: string
      enum:
        - coordinates
        - space_inventory
        - capacities
        - room_features
        - lodging
        - parking
        - technology
        - accessibility
        - amenities
        - travel_berlin_hbf
        - travel_other_origins
        - travel_reference_points
        - rail_access
        - motorway_access
    VenuePublication:
      type: object
      required: [status, comparison_ready, missing_for_comparison, missing_for_comparison_count]
      additionalProperties: false
      properties:
        status: { $ref: '#/components/schemas/PublicationStatus' }
        comparison_ready:
          type: boolean
          description: True only when every comparison area required by the active methodology is handled.
        missing_for_comparison:
          type: array
          uniqueItems: true
          description: Comparison areas that still contain unprocessed requirements under the active methodology.
          items: { $ref: '#/components/schemas/ComparisonArea' }
        missing_for_comparison_count:
          type: integer
          minimum: 0
          description: Number of entries in missing_for_comparison.
    Coordinates:
      type: object
      required: [latitude, longitude]
      additionalProperties: false
      properties:
        latitude: { type: number, minimum: -90, maximum: 90 }
        longitude: { type: number, minimum: -180, maximum: 180 }
    CoordinatePair:
      type: object
      required: [origin, destination]
      additionalProperties: false
      properties:
        origin: { $ref: '#/components/schemas/Coordinates' }
        destination: { $ref: '#/components/schemas/Coordinates' }
    SnapDistancePair:
      type: object
      required: [origin, destination]
      additionalProperties: false
      properties:
        origin: { type: integer, minimum: 0 }
        destination: { type: integer, minimum: 0 }
    DriveMeasurement:
      type: object
      required:
        - originId
        - mode
        - direction
        - status
        - durationSeconds
        - durationMinutes
        - distanceMeters
        - distanceKm
        - claimId
        - routingRunId
        - inputCoordinates
        - snappedCoordinates
        - snapDistanceMeters
        - calculatedAt
        - reviewDueAt
        - staleAt
        - expiresAt
        - errorCode
        - method
      additionalProperties: false
      properties:
        originId: { $ref: '#/components/schemas/RoutingOriginId' }
        mode: { type: string, const: driving }
        direction: { type: string, const: origin_to_venue }
        status: { $ref: '#/components/schemas/MeasurementStatus' }
        durationSeconds: { type: [integer, 'null'], minimum: 1 }
        durationMinutes: { type: [integer, 'null'], minimum: 1 }
        distanceMeters: { type: [integer, 'null'], minimum: 0 }
        distanceKm: { type: [number, 'null'], minimum: 0 }
        claimId: { type: [string, 'null'], format: uuid }
        routingRunId: { type: [string, 'null'] }
        inputCoordinates:
          oneOf:
            - { $ref: '#/components/schemas/CoordinatePair' }
            - { type: 'null' }
        snappedCoordinates:
          oneOf:
            - { $ref: '#/components/schemas/CoordinatePair' }
            - { type: 'null' }
        snapDistanceMeters:
          oneOf:
            - { $ref: '#/components/schemas/SnapDistancePair' }
            - { type: 'null' }
        calculatedAt: { type: [string, 'null'], format: date-time }
        reviewDueAt: { type: [string, 'null'], format: date-time }
        staleAt: { type: [string, 'null'], format: date-time }
        expiresAt: { type: [string, 'null'], format: date-time }
        errorCode: { type: [string, 'null'] }
        method: { type: [object, 'null'], additionalProperties: true }
    Travel:
      type: object
      required: [drive, rail, motorway, ring]
      properties:
        drive:
          type: array
          minItems: 5
          maxItems: 5
          items: { $ref: '#/components/schemas/DriveMeasurement' }
        rail: { type: object, additionalProperties: true }
        motorway: { type: object, additionalProperties: true }
        ring: { $ref: '#/components/schemas/TravelRing' }
    RequiredCriterion:
      type: object
      required: [criterion, value]
      additionalProperties: false
      properties:
        criterion: { $ref: '#/components/schemas/AmenityKey' }
        value: { type: boolean, const: true }
    PreferenceCriterion:
      type: object
      required: [criterion, value]
      additionalProperties: false
      properties:
        criterion: { $ref: '#/components/schemas/AmenityKey' }
        value: { type: boolean }
        weight:
          type: number
          exclusiveMinimum: 0
          maximum: 1
    SearchRequest:
      type: object
      required: [event]
      additionalProperties: false
      properties:
        room_requirements: { $ref: '#/components/schemas/RoomRequirements' }
        own_lodging_required: { type: boolean, const: true, description: 'Require own overnight accommodation independently of a known bedroom count.' }
        include_decision:
          type: boolean
          description: Include up to three proof-bound room options per result. Automatically enabled when room_requirements is provided.
        event:
          type: object
          required: [participants, seating_style]
          additionalProperties: false
          properties:
            participants: { type: integer, minimum: 1, maximum: 10000 }
            seating_style:
              type: string
              enum: [theatre, parliament, banquet, uShape, boardroom, reception]
            bedrooms_required: { type: integer, minimum: 0 }
        origin:
          type: object
          required: [place_id]
          additionalProperties: false
          properties:
            place_id:
              type: string
              enum:
                - de-berlin-hbf
                - de-ber
                - de-potsdam-hbf
                - de-leipzig-hbf
                - de-hamburg-hbf
        travel:
          type: object
          additionalProperties: false
          properties:
            mode:
              type: string
              const: driving
            max_minutes: { type: integer, minimum: 1, maximum: 120 }
        required:
          type: array
          maxItems: 25
          items: { $ref: '#/components/schemas/RequiredCriterion' }
          default: []
        preferences:
          type: array
          maxItems: 25
          items: { $ref: '#/components/schemas/PreferenceCriterion' }
          default: []
        unknown_policy:
          type: string
          enum: [separate, include, exclude]
          default: separate
        include_ineligible:
          type: boolean
          default: false
        limit:
          type: integer
          minimum: 1
          maximum: 25
          default: 10
        cursor: { type: string }
        release: { type: string }
    SearchResponse:
      type: object
      required: [meta, input, page, results]
      properties:
        decision_version: { type: string }
        meta: { $ref: '#/components/schemas/Meta' }
        input: { type: object, additionalProperties: true }
        page:
          type: object
          required: [limit, returned, total, next_cursor]
          properties:
            limit: { type: integer }
            returned: { type: integer }
            total: { type: integer }
            next_cursor: { type: [string, 'null'] }
        results:
          type: array
          items:
            type: object
            required: [venue, eligibility, match, constraints, unknowns]
            properties:
              venue: { $ref: '#/components/schemas/EntitySummary' }
              decision: { $ref: '#/components/schemas/VenueDecision' }
              eligibility:
                type: string
                enum: [confirmed_match, needs_verification, ineligible]
                description: Result for this request, independent of the venue publication status.
              match:
                type: object
                additionalProperties: true
              constraints:
                type: array
                description: Venue-wide matching constraints. In planning responses scope is venue; capacity is the maximum across currently proven configurations, not the capacity of a selected room. Use decision.options[].capacity, criteria and evidence for room-specific statements.
                items:
                  type: object
                  additionalProperties: true
                  properties:
                    scope: { type: string, const: venue, description: Added to venue-wide constraints when a decision projection is requested. }
              unknowns:
                type: array
                items: { type: string }
    RoomRequirements:
      type: object
      additionalProperties: false
      properties:
        daylight: { type: boolean, const: true }
    PlanningContext:
      type: object
      required: [event]
      additionalProperties: false
      properties:
        event: { $ref: '#/components/schemas/SearchRequest/properties/event' }
        origin: { $ref: '#/components/schemas/SearchRequest/properties/origin' }
        travel: { $ref: '#/components/schemas/SearchRequest/properties/travel' }
        required: { $ref: '#/components/schemas/SearchRequest/properties/required' }
        preferences: { $ref: '#/components/schemas/SearchRequest/properties/preferences' }
        unknown_policy: { $ref: '#/components/schemas/SearchRequest/properties/unknown_policy' }
        own_lodging_required: { $ref: '#/components/schemas/SearchRequest/properties/own_lodging_required' }
        room_requirements: { $ref: '#/components/schemas/RoomRequirements' }
    VenueDecision:
      type: object
      required: [venueId, request, context, status, roomStatus, houseConstraints, options, counts, completeInventory, hasMore, questions, page]
      properties:
        venueId: { type: string }
        request: { $ref: '#/components/schemas/NormalizedPlanningRequest' }
        context:
          type: object
          required: [dataRelease, sourceMatchingVersion, matchingVersion, projectionVersion, evaluatedAt, validUntil]
          properties:
            dataRelease: { type: string }
            sourceMatchingVersion: { type: string, description: Historical source release version, not the new decision algorithm version. }
            matchingVersion: { type: string, const: tagungsregister-match-1.2.0 }
            projectionVersion: { type: string, const: tagungsregister-decision-1.0.0 }
            evaluatedAt: { type: string, format: date-time }
            validUntil: { type: [string, 'null'], format: date-time }
        status: { type: string, enum: [PASS, UNKNOWN, FAIL] }
        roomStatus: { type: string, enum: [PASS, UNKNOWN, FAIL] }
        houseConstraints: { type: array, items: { $ref: '#/components/schemas/DecisionConstraint' } }
        options:
          type: array
          maxItems: 100
          description: Status first, then ascending proven requested capacity, then stable ID. First is not a best-room endorsement; configurations cannot be added as independent rooms.
          items:
            type: object
            required: [id, name, kind, capacity, daylight, status, criteria, evidence]
            properties:
              id: { type: string }
              name: { type: string }
              kind: { type: string, enum: [physical_room, combination, subdivision, unknown] }
              capacity: { type: [integer, 'null'] }
              daylight: { type: [boolean, 'null'] }
              status: { type: string, enum: [PASS, UNKNOWN, FAIL] }
              criteria:
                type: array
                maxItems: 2
                items:
                  type: object
                  additionalProperties: false
                  required: [attribute, status, claimIds]
                  properties:
                    attribute: { type: string }
                    status: { type: string, enum: [PASS, UNKNOWN, FAIL] }
                    claimIds: { type: array, items: { type: string, format: uuid } }
              evidence: { type: array, items: { $ref: '#/components/schemas/DecisionEvidenceRef' }, description: Published claim, source and locator references; no private revisions or excerpts. }
        counts:
          type: object
          required: [total, confirmed, needsVerification, excluded]
          properties:
            total: { type: integer }
            confirmed: { type: integer }
            needsVerification: { type: integer }
            excluded: { type: integer }
        completeInventory: { type: boolean }
        hasMore: { type: boolean }
        page:
          type: object
          additionalProperties: false
          required: [offset, limit, returned, nextOffset]
          properties:
            offset: { type: integer, minimum: 0, maximum: 10000 }
            limit: { type: integer, minimum: 1, maximum: 100 }
            returned: { type: integer, minimum: 0, maximum: 100 }
            nextOffset: { type: [integer, 'null'], minimum: 0 }
        questions: { type: array, items: { type: string } }
    NormalizedPlanningRequest:
      type: object
      additionalProperties: false
      required: [participants, seatingStyle, travelOriginId, requiredAmenities, preferences, unknownPolicy, includeIneligible, offset, limit]
      properties:
        participants: { type: integer, minimum: 1, maximum: 10000 }
        seatingStyle: { type: string, enum: [theatre, parliament, banquet, uShape, boardroom, reception] }
        bedroomsRequired: { type: integer, minimum: 0 }
        ownLodgingRequired: { type: boolean, const: true }
        travelOriginId: { type: string, enum: [de-berlin-hbf, de-ber, de-potsdam-hbf, de-leipzig-hbf, de-hamburg-hbf] }
        maxTravelMinutes: { type: integer, minimum: 1, maximum: 120 }
        requiredAmenities: { type: array, items: { $ref: '#/components/schemas/AmenityKey' } }
        preferences: { type: array, items: { $ref: '#/components/schemas/PreferenceCriterion' } }
        unknownPolicy: { type: string, enum: [include, separate, exclude] }
        includeIneligible: { type: boolean }
        offset: { type: integer, minimum: 0 }
        limit: { type: integer, minimum: 1, maximum: 50 }
        roomRequirements: { $ref: '#/components/schemas/RoomRequirements' }
    DecisionConstraint:
      type: object
      required: [criterion, status, required, observed, claimIds, reason]
      properties:
        criterion: { type: string }
        status: { type: string, enum: [PASS, UNKNOWN, FAIL] }
        required: { type: [number, string, boolean] }
        observed: { type: [number, string, boolean, 'null'] }
        claimIds: { type: array, items: { type: string, format: uuid } }
        reason: { type: string }
    DecisionEvidenceRef:
      type: object
      additionalProperties: false
      required: [claimId, subjectId, attribute, observedAt, verifiedAt, references]
      properties:
        claimId: { type: string, format: uuid }
        subjectId: { type: string, format: uuid }
        attribute: { type: string }
        observedAt: { type: string, format: date-time }
        verifiedAt: { type: string, format: date-time }
        references:
          type: array
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required: [sourceId, evidenceId, url, locator]
            properties:
              sourceId: { type: string, format: uuid }
              evidenceId: { type: string, format: uuid }
              url: { type: string, format: uri }
              locator: { type: string }
    EntitySummary:
      type: object
      required: [id, slug, name, canonical_url, publication]
      properties:
        id: { type: [string, 'null'] }
        slug: { type: [string, 'null'] }
        name: { type: string }
        canonical_url: { type: [string, 'null'], format: uri }
        publication: { $ref: '#/components/schemas/VenuePublication' }
        aliases:
          type: array
          items: { type: string }
        venue_type:
          oneOf:
            - { $ref: '#/components/schemas/VenueType' }
            - { type: 'null' }
        city: { type: [string, 'null'] }
        spaces_total: { type: [integer, 'null'], minimum: 0 }
        spaces_breakout_20_plus: { type: [integer, 'null'], minimum: 0 }
        max_capacities: { type: object, additionalProperties: true }
        gaps:
          type: array
          description: Optional scoped gaps for summary capacity, lodging and driving fields; use the profile endpoint for the full gap inventory.
          items:
            type: object
            properties:
              attribute: { type: string }
              state: { type: string }
              subjectId: { type: [string, 'null'] }
        lodging: { type: object, additionalProperties: true }
        parking: { type: object, additionalProperties: true }
        amenities:
          type: array
          items: { $ref: '#/components/schemas/AmenityKey' }
        travel: { $ref: '#/components/schemas/Travel' }
        quality: { type: object, additionalProperties: true }
    VenueResponse:
      type: object
      required: [meta, entity, publication, profile, facts, gaps, evidence, sources, source_summaries]
      properties:
        meta: { $ref: '#/components/schemas/Meta' }
        entity: { $ref: '#/components/schemas/EntitySummary' }
        publication: { $ref: '#/components/schemas/VenuePublication' }
        profile:
          type: object
          additionalProperties: true
          properties:
            roomTopology: { $ref: '#/components/schemas/PublishedRoomTopology' }
            certificationDetails:
              type: array
              description: Present in public release contract 2.4.0. Exact-Claim certificate summaries, not authenticity badges.
              items: { $ref: '#/components/schemas/CertificationProjection' }
        facts:
          type: array
          description: Published Claim objects only; every non-derived item carries Evidence IDs.
          items: { type: object, additionalProperties: true }
        gaps:
          type: array
          items: { type: object, additionalProperties: true }
        evidence:
          type: array
          items: { $ref: '#/components/schemas/PublicEvidence' }
        sources:
          type: array
          items: { type: object, additionalProperties: true }
        source_summaries:
          type: array
          items: { type: object, additionalProperties: true }
    VenueDetailResponse:
      type: object
      required: [meta, venue, identity, evidence_level, evidence_scope]
      properties:
        meta: { $ref: '#/components/schemas/Meta' }
        decision: { $ref: '#/components/schemas/VenueDecision' }
        venue:
          type: object
          additionalProperties: true
          properties:
            roomTopology: { $ref: '#/components/schemas/PublishedRoomTopology' }
            certificationDetails:
              type: array
              description: Present in public release contract 2.4.0; older releases omit this property.
              items: { $ref: '#/components/schemas/CertificationProjection' }
        identity: { $ref: '#/components/schemas/EntitySummary' }
        evidence_level:
          type: string
          enum: [none, summary, full]
        evidence_scope: { type: string }
    PublicEvidence:
      type: object
      additionalProperties: false
      required: [id, sourceId, locator, excerptPublic]
      description: A published fragment. Contract 2.3.0 requires sourceRevision on every fragment; older contracts omit it. Missing dates never imply a recent document or hotel confirmation.
      properties:
        id: { type: string, format: uuid }
        sourceId: { type: string, format: uuid }
        locator:
          type: object
          additionalProperties: false
          required: [kind, value]
          properties:
            kind: { type: string, enum: [page, section, table, document] }
            value: { type: string }
        excerpt: { type: [string, 'null'], maxLength: 280, description: Omitted when public excerpt rights are absent. }
        excerptPublic: { type: boolean }
        sourceRevision: { $ref: '#/components/schemas/SourceRevisionDates' }
    SourceRevisionDates:
      type: object
      additionalProperties: false
      required: [id, retrievedAt, statedDates, technicalDates]
      description: Exact revision used by this fragment, frozen with the release. RetrievedAt is retrieval time, not document publication or hotel confirmation. Dates retain source precision and conflicting observations; no preferred date is inferred. All observation and retrieval timestamps must be at or before the release projectionAsOf.
      properties:
        id: { type: string, format: uuid }
        retrievedAt: { type: string, format: date-time }
        statedDates:
          type: array
          maxItems: 1000
          items: { $ref: '#/components/schemas/StatedSourceDate' }
        technicalDates:
          type: array
          maxItems: 1000
          items: { $ref: '#/components/schemas/TechnicalSourceDate' }
    StatedSourceDate:
      type: object
      additionalProperties: false
      required: [value, precision, basis, observedAt]
      description: Date explicitly stated in the content or document, not necessarily its publication date. Value must be a valid calendar date matching precision (YYYY, YYYY-MM or YYYY-MM-DD); missing components are never invented. Duplicate basis/precision/value entries are disallowed within a revision.
      properties:
        value: { type: string, pattern: '^[0-9]{4}(-[0-9]{2})?(-[0-9]{2})?$' }
        precision: { type: string, enum: [year, month, day] }
        basis: { type: string, enum: [content_statement, document_statement] }
        observedAt: { type: string, format: date-time }
    TechnicalSourceDate:
      type: object
      additionalProperties: false
      required: [value, precision, basis, observedAt]
      description: Technical or discovery date, not a confirmed document publication date. Calendar precision and duplicate rules are the same as for statedDates. ObservedAt is the first recorded observation of this basis/precision/value in the revision.
      properties:
        value: { type: string, pattern: '^[0-9]{4}(-[0-9]{2})?(-[0-9]{2})?$' }
        precision: { type: string, enum: [year, month, day] }
        basis: { type: string, enum: [pdf_metadata, http_last_modified, copyright, archive_first_seen, register_first_seen] }
        observedAt: { type: string, format: date-time }
    PublishedRoomTopology:
      type: object
      description: Present in public contracts 2.2.0 and 2.3.0 (release generations 4 and 5). Pinned to this release, never live editorial state. Unknown relationships do not prove independent simultaneous use.
      additionalProperties: false
      required: [releaseKey, reviewRevisionId, completenessClaimId, configurations]
      properties:
        releaseKey: { type: string }
        reviewRevisionId: { type: [string, 'null'], format: uuid }
        completenessClaimId: { type: [string, 'null'], format: uuid }
        configurations:
          type: array
          maxItems: 500
          items:
            type: object
            additionalProperties: false
            required: [spaceId, kind, atomIds, componentSpaceIds, topologyClaimId]
            properties:
              spaceId: { type: string, format: uuid }
              kind: { type: string, enum: [physical_room, combination, subdivision, unknown] }
              atomIds: { type: array, items: { type: string, format: uuid }, maxItems: 500 }
              componentSpaceIds: { type: array, items: { type: string, format: uuid }, maxItems: 500 }
              topologyClaimId: { type: [string, 'null'], format: uuid }
    Problem:
      type: object
      required: [type, title, status, detail, instance, request_id]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer, minimum: 400, maximum: 599 }
        detail: { type: string }
        instance: { type: string }
        request_id: { type: string }
        errors: {}
