openapi: 3.1.0
info:
  title: AssetLab API
  version: "1.0"
  description: |

    AssetLab is a multi-tenant asset management platform for tracking equipment, work orders, preventive maintenance, and compliance across your facilities.

    The AssetLab API gives you programmatic access to your organization's data — use it to build integrations, automate workflows, feed dashboards, or connect AI assistants to your asset management data.


    ## Common Use Cases

    - **BI & Analytics** — Sync asset and work order data with Power BI, Tableau, or Looker
    - **System Integration** — Feed external CMMS, ERP, or ITSM systems
    - **Custom Dashboards** — Build reporting pipelines tailored to your operations
    - **Automation** — Programmatically create work orders and track status changes
    - **AI Integration** — Connect AI assistants to your AssetLab data via MCP (see below)


    ## Connect Your AI Assistant (MCP)

    The [AssetLab MCP Server](https://www.npmjs.com/package/@assetlab/mcp-server) lets AI assistants like Claude and ChatGPT work with your entire AssetLab account — every resource available in this API is accessible through MCP, including assets, work orders, PM schedules, projects, contracts, vendors, parts inventory, compliance records, invoices, purchase orders, sites, buildings, and more.

    ```bash
    npx @assetlab/mcp-server
    ```

    All you need is an API key from **Settings > API Keys**. Setup instructions for each assistant are available in the app, or see the [npm package](https://www.npmjs.com/package/@assetlab/mcp-server) for full documentation.


    ## Getting Started

    API keys are scoped to a single organization and can be restricted to specific resources. Create and manage them in **Settings > API Keys** within the AssetLab app.

    > **Questions or access requests?** Email [support@assetlab.ca](mailto:support@assetlab.ca)

    ---

    ## Authentication

    All requests require a valid API key passed as a Bearer token:

    ```
    Authorization: Bearer al_live_...
    ```

    API keys are created in **Settings > API Keys** within the AssetLab app.
    Each key is scoped to a single tenant and can be restricted to specific resources.

    ## Pagination

    List endpoints return paginated results. Use `page` and `per_page` query parameters:

    | Parameter  | Default | Max | Description              |
    |------------|---------|-----|--------------------------|
    | `page`     | 1       | —   | Page number (1-based)    |
    | `per_page` | 25      | 1000 | Items per page          |

    Every list response includes a `pagination` object:

    ```json
    {
      "data": [...],
      "pagination": {
        "page": 1,
        "per_page": 25,
        "total": 142,
        "total_pages": 6
      }
    }
    ```

    ## Rate Limiting

    Each API key has a configurable rate limit (default: 60 requests/minute).
    Rate limit status is returned in response headers:

    | Header                | Description                        |
    |-----------------------|------------------------------------|
    | `X-RateLimit-Limit`     | Max requests per minute            |
    | `X-RateLimit-Remaining` | Remaining requests in this window  |
    | `X-RateLimit-Reset`     | Unix timestamp when window resets  |

    ## Scopes

    API keys use scopes to control access. A key with `assets:read` can only access the Assets endpoints.
    The wildcard scope `*:*` grants access to all resources.

    ## Errors

    Errors return a JSON object with an `error` string:

    ```json
    { "error": "Asset not found" }
    ```

    | Status | Meaning                          |
    |--------|----------------------------------|
    | 400    | Bad request (invalid UUID, etc.) |
    | 401    | Missing or invalid API key       |
    | 403    | Key inactive, expired, or missing scope |
    | 404    | Resource not found               |
    | 405    | Method not allowed (only GET)    |
    | 429    | Rate limit exceeded              |
    | 500    | Internal server error            |

    &nbsp;

    &nbsp;

    &nbsp;

    &nbsp;

    &nbsp;
  contact:
    name: AssetLab Support
    url: https://assetlab.ca
    email: support@assetlab.ca
  license:
    name: Proprietary

servers:
  - url: https://ksirnjovowpvamgcnapt.supabase.co/functions/v1/api-gateway/v1
    description: Production

security:
  - BearerAuth: []

tags:
  - name: Assets
    description: Equipment and asset records
  - name: Work Orders
    description: Corrective, preventive, and emergency work orders
  - name: Sites
    description: Physical site locations
  - name: Buildings
    description: Buildings within sites
  - name: Locations
    description: Rooms and areas within buildings
  - name: Systems
    description: Asset classification — systems
  - name: System Groups
    description: Asset classification — system groups
  - name: System Classes
    description: Asset classification — system classes (top-level)
  - name: PM Schedules
    description: Preventive maintenance schedules
  - name: PM Templates
    description: Reusable preventive maintenance templates
  - name: Forms
    description: Form templates, items, and read-only inspection/checklist responses
  - name: Projects
    description: Capital projects and renovations
  - name: Contracts
    description: Vendor service contracts
  - name: Compliance
    description: Regulatory compliance items
  - name: Dashboard
    description: Aggregated statistics
  - name: Parts
    description: Spare parts and inventory
  - name: Vendors
    description: Vendor and supplier records
  - name: Work Requests
    description: Work requests from requesters
  - name: Invoices
    description: Project and work order invoices
  - name: Purchase Orders
    description: Purchase orders for procurement
  - name: Asset Type Groups
    description: Groups that organize asset types into logical categories
  - name: Asset Types
    description: Asset type classifications
  - name: Asset Statuses
    description: Lifecycle states for assets (e.g. Active, Inactive, Decommissioned)
  - name: Work Categories
    description: Work order categorization
  - name: Manufacturers
    description: Equipment manufacturers
  - name: Building Types
    description: Building type classifications
  - name: Location Types
    description: Location type classifications
  - name: Project Phase Categories
    description: Tenant-configurable project phase definitions (e.g. Planning, Design, Execution)
  - name: Cost Categories
    description: Cost categorization for financial tracking
  - name: Expenses
    description: Expense records linked to work orders and assets
  - name: Budgets
    description: Annual funding budgets
  - name: Asset Comments
    description: Comments and notes on assets
  - name: Asset Costs
    description: Cost records linked to assets
  - name: Asset Replacement Plans
    description: Lifecycle replacement planning for assets
  - name: Asset Risk History
    description: Risk assessment history for assets
  - name: Work Order Comments
    description: Comments and notes on work orders
  - name: Work Order Schedules
    description: Work orders on a technician's calendar; stop_order rows form the day plan
  - name: Project Tasks
    description: Project task breakdown structure
  - name: Project Milestones
    description: Project milestone tracking
  - name: Project Phases
    description: Project phase management
  - name: Project Budget Items
    description: Project budget line items
  - name: Project Time Entries
    description: Time tracking for project tasks
  - name: Project Comments
    description: Comments and discussions on projects
  - name: Project Risks
    description: Project risk register with probability, impact, and mitigation tracking
  - name: Compliance Records
    description: Compliance check completion records
  - name: Site FCI History
    description: Facility Condition Index history for sites
  - name: Dashboard Snapshots
    description: Monthly aggregate dashboard snapshots
  - name: Vendor Site Assignments
    description: Vendor-to-site service mappings
  - name: Contract Sites
    description: Contract-to-site coverage mappings
  - name: Custom Field Definitions
    description: Tenant-configured custom field schemas
  - name: Custom Field Values
    description: Custom field data values
  - name: Part Categories
    description: Part categorization
  - name: Upload URLs
    description: Generate signed upload URLs for file storage
  - name: Asset Documents
    description: Documents attached to assets (O&M manuals, warranties, specs, etc.)
  - name: Attachments
    description: File attachments linked to work orders, work requests, PM schedules, or PM templates
  - name: Project Documents
    description: Documents attached to projects
  - name: Contract Documents
    description: Documents attached to contracts
  - name: Change Orders
    description: Change order management for projects
  - name: Project Document Folder Templates
    description: Reusable folder structure templates for project documents
  - name: Service Areas
    description: Level of Service groupings that scope system classes and sites for measuring service delivery performance
  - name: Service Area System Classes
    description: Junction linking service areas to the system classes they cover
  - name: Service Area Sites
    description: Junction linking service areas to specific sites (optional scoping)
  - name: LoS Measures
    description: Level of Service measures — community or technical metrics tracked within a service area
  - name: LoS Measurements
    description: Time-series recorded values for LoS measures
  - name: LoS Targets History
    description: Audit trail of target value changes for LoS measures
  - name: Infrastructure Feature Classes
    description: Catalog of feature classes (water_main, sewer_gravity, pavement). Addressed by `code`, not UUID.
  - name: Infrastructure Networks
    description: Named groupings of features bound to one feature class
  - name: Infrastructure Assets
    description: Infrastructure features (segments and nodes) with GeoJSON geometry
  - name: Infrastructure Asset Inspections
    description: Inspections recorded against infrastructure features
  - name: Asset Condition Assessments
    description: Point-in-time condition assessments recorded against assets (condition history + CRV)

x-tagGroups:
  - name: Core
    tags:
      - Assets
      - Work Orders
      - Work Requests
      - PM Schedules
      - PM Templates
  - name: Facilities
    tags:
      - Sites
      - Buildings
      - Locations
  - name: Feature Classification
    tags:
      - Systems
      - System Groups
      - System Classes
      - Asset Types
      - Asset Type Groups
      - Manufacturers
  - name: Projects
    tags:
      - Projects
      - Project Sites
      - Project Buildings
      - Project Locations
      - Project System Classes
      - Project System Groups
      - Project Systems
      - Project Assets
      - Project Tasks
      - Project Milestones
      - Project Phases
      - Project Budget Items
      - Project Time Entries
      - Project Comments
      - Project Risks
      - Project Document Folder Templates
  - name: Financial
    tags:
      - Budgets
      - Invoices
      - Purchase Orders
      - Expenses
      - Change Orders
      - Cost Categories
      - Asset Costs
      - Asset Replacement Plans
  - name: Vendors & Contracts
    tags:
      - Vendors
      - Contracts
      - Vendor Site Assignments
      - Contract Sites
  - name: Compliance
    tags:
      - Compliance
      - Compliance Records
  - name: Parts & Inventory
    tags:
      - Parts
      - Part Categories
  - name: Comments & Documents
    tags:
      - Asset Comments
      - Work Order Comments
      - Asset Documents
      - Project Documents
      - Contract Documents
      - Attachments
      - Upload URLs
  - name: Level of Service
    tags:
      - Service Areas
      - Service Area System Classes
      - Service Area Sites
      - LoS Measures
      - LoS Measurements
      - LoS Targets History
  - name: Infrastructure
    tags:
      - Infrastructure Feature Classes
      - Infrastructure Networks
      - Infrastructure Assets
      - Infrastructure Asset Inspections
  - name: Dashboard & Analytics
    tags:
      - Dashboard
      - Dashboard Snapshots
      - Site FCI History
      - Asset Risk History
      - Asset Condition Assessments
  - name: Configuration
    tags:
      - Asset Statuses
      - Work Categories
      - Building Types
      - Location Types
      - Custom Field Definitions
      - Custom Field Values

# ──────────────────────────────────────────────────────────────
# Reusable components
# ──────────────────────────────────────────────────────────────

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: "API key from AssetLab Settings > API Keys"

  parameters:
    page:
      name: page
      in: query
      schema: { type: integer, default: 1, minimum: 1 }
      description: Page number
    per_page:
      name: per_page
      in: query
      schema: { type: integer, default: 25, minimum: 1, maximum: 1000 }
      description: Items per page
    search:
      name: search
      in: query
      schema: { type: string, maxLength: 200 }
      description: Case-insensitive name search
    id:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Resource UUID

  schemas:
    Pagination:
      type: object
      properties:
        page: { type: integer, example: 1 }
        per_page: { type: integer, example: 25 }
        total: { type: integer, example: 142 }
        total_pages: { type: integer, example: 6 }

    Error:
      type: object
      properties:
        error: { type: string, example: "Asset not found" }

    BulkItemResult:
      type: object
      properties:
        index: { type: integer, description: "Zero-based index in the request array" }
        success: { type: boolean }
        data: { type: object, description: "Created/updated record (on success)" }
        error: { type: string, description: "Error message (on failure)" }
        details:
          type: array
          description: "Validation error details (on failure)"
          items:
            type: object
            properties:
              field: { type: string }
              message: { type: string }

    BulkSummary:
      type: object
      properties:
        total: { type: integer, description: "Total items in request" }
        succeeded: { type: integer, description: "Number that succeeded" }
        failed: { type: integer, description: "Number that failed" }

    BulkResponse:
      type: object
      properties:
        summary: { $ref: "#/components/schemas/BulkSummary" }
        results:
          type: array
          items: { $ref: "#/components/schemas/BulkItemResult" }

    # ── Linked reference objects ──

    SiteRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    BuildingRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    SystemRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    VendorRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    SystemGroupRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        system_class_id: { type: string, format: uuid, nullable: true }

    SystemClassRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    ProjectRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    LocationRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    WorkOrderRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        title: { type: string }

    CostCategoryRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    AssetTypeGroupRef:
      type: object
      nullable: true
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    AssetTypeGroup:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        color: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    AssetTypeGroupCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        color: { type: string, nullable: true, description: "Hex color code (e.g., #6366f1)" }

    AssetTypeGroupUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        color: { type: string, nullable: true, description: "Hex color code (e.g., #6366f1)" }

    # ── Resources ──

    Asset:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        model: { type: string, nullable: true }
        serial_number: { type: string, nullable: true }
        purchase_cost: { type: number, nullable: true }
        purchase_date: { type: string, format: date, nullable: true }
        expected_lifetime_years: { type: number, nullable: true }
        condition_score: { type: number, nullable: true }
        risk_factor: { type: string, nullable: true, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
        status_id: { type: string, nullable: true }
        qr_code: { type: string, nullable: true, description: "Only in detail response" }
        image_url: { type: string, nullable: true, description: "Only in detail response" }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        system_id: { type: string, format: uuid, nullable: true }
        safety_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        service_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        environmental_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        regulatory_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        reputation_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        consequence_of_failure_score: { type: integer, nullable: true }
        likelihood_of_failure_score: { type: integer, nullable: true }
        sites: { $ref: "#/components/schemas/SiteRef" }
        buildings: { $ref: "#/components/schemas/BuildingRef" }
        systems: { $ref: "#/components/schemas/SystemRef" }
        current_meter_reading: { type: number, nullable: true, description: "Current meter/odometer reading" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    WorkOrder:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true, description: "Only in detail response" }
        priority: { type: string, enum: [LOW, MEDIUM, HIGH, URGENT] }
        status: { type: string, enum: [NEW, IN_PROGRESS, ON_HOLD, REJECTED, COMPLETED, CANCELLED] }
        type: { type: string, nullable: true, enum: [PM, REACTIVE] }
        work_order_number: { type: integer, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        image_url: { type: string, nullable: true, description: "Image URL for the work order" }
        sites: { $ref: "#/components/schemas/SiteRef" }
        buildings: { $ref: "#/components/schemas/BuildingRef" }
        start_date: { type: string, format: date, nullable: true }
        due_date: { type: string, format: date, nullable: true }
        estimated_time: { type: number, nullable: true, description: "Estimated hours" }
        estimated_cost: { type: number, nullable: true, description: "Only in detail response" }
        actual_hours: { type: number, nullable: true, description: "Only in detail response" }
        completed_at: { type: string, format: date-time, nullable: true, description: "Only in detail response" }
        completed_by: { type: string, nullable: true, description: "Clerk user ID. Only in detail response" }
        completion_notes: { type: string, nullable: true, description: "Only in detail response" }
        assignees: { type: array, items: { type: string }, nullable: true, description: "Array of assigned Clerk user IDs" }
        meter_reading: { type: number, nullable: true, description: "Meter/odometer reading at time of service" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }
        pm_schedule_id: { type: string, format: uuid, nullable: true }
        work_request_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Site:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        address: { type: string, nullable: true }
        city: { type: string, nullable: true }
        province: { type: string, nullable: true }
        postal code: { type: string, nullable: true }
        country: { type: string, nullable: true }
        Description: { type: string, nullable: true }
        square_footage: { type: number, nullable: true, description: "Square footage" }
        cost_per_sqft: { type: number, nullable: true, description: "Base cost per square foot" }
        additional_cost_per_sqft: { type: number, nullable: true, description: "Additional cost per square foot" }
        operational_cost_per_sqft: { type: number, nullable: true, description: "Operational cost per square foot" }
        year_built: { type: integer, nullable: true, description: "Year built" }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        owner_landlord: { type: string, nullable: true }
        ownership_type: { type: string, nullable: true, enum: [Owned, Leased, Managed] }
        lease_start_date: { type: string, format: date, nullable: true }
        lease_end_date: { type: string, format: date, nullable: true }
        renewal_option: { type: string, nullable: true }
        lease_details: { type: string, nullable: true }
        insurance_provider: { type: string, nullable: true }
        insurance_policy_number: { type: string, nullable: true }
        property_manager_company: { type: string, nullable: true }
        property_manager_contact_name: { type: string, nullable: true }
        property_manager_contact_email: { type: string, nullable: true }
        property_manager_contact_phone: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Building:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        floors: { type: integer, nullable: true }
        area_sqft: { type: number, nullable: true, description: "Area in square feet" }
        type: { type: string, nullable: true, description: "Building type label" }
        building_type_id: { type: string, format: uuid, nullable: true }
        year_built: { type: integer, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        sites: { $ref: "#/components/schemas/SiteRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Location:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        building_id: { type: string, format: uuid, nullable: true }
        floor: { type: string, nullable: true }
        area: { type: string, nullable: true }
        type: { type: string, nullable: true }
        location_type_id: { type: string, format: uuid, nullable: true }
        buildings: { $ref: "#/components/schemas/BuildingRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    System:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        system_group_id: { type: string, format: uuid, nullable: true }
        crv_multiplier: { type: number, nullable: true }
        system_groups: { $ref: "#/components/schemas/SystemGroupRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    SystemGroup:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        system_class_id: { type: string, format: uuid, nullable: true }
        system_classes: { $ref: "#/components/schemas/SystemClassRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    SystemClass:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    SystemClassCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    SystemClassUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    SystemGroupCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        system_class_id: { type: string, format: uuid, nullable: true }

    SystemGroupUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        system_class_id: { type: string, format: uuid, nullable: true }

    PmSchedule:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true, description: "Only in detail response" }
        frequency: { type: string, nullable: true }
        custom_interval_weeks: { type: integer, nullable: true, description: "Only in detail response" }
        next_due: { type: string, format: date, nullable: true }
        last_performed: { type: string, format: date, nullable: true }
        estimated_hours: { type: number, nullable: true }
        estimated_cost: { type: number, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        asset_ids: { type: array, items: { type: string }, nullable: true, description: "Only in detail response" }
        system_ids: { type: array, items: { type: string }, nullable: true, description: "Only in detail response" }
        location_ids: { type: array, items: { type: string }, nullable: true, description: "Only in detail response" }
        location_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        work_category: { type: string, nullable: true, description: "Only in detail response" }
        schedule_type: { type: string, nullable: true }
        lead_time_days: { type: integer, nullable: true, description: "Only in detail response" }
        grace_period_days: { type: integer, nullable: true, description: "Only in detail response" }
        safety_requirements: { type: string, nullable: true, description: "Only in detail response" }
        status: { type: string, nullable: true }
        auto_generate_wo: { type: boolean, nullable: true }
        floating: { type: boolean, nullable: true, description: "Only in detail response" }
        meter_based: { type: boolean, nullable: true, description: "Whether this is a meter-based PM schedule" }
        meter_interval: { type: number, nullable: true, description: "Trigger every N meter units" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }
        tasks: { type: array, nullable: true, items: { type: object, properties: { id: { type: string }, description: { type: string }, completed: { type: boolean } }, required: [id, description, completed] }, description: "Only in detail response" }
        resources: { type: array, nullable: true, items: { type: object }, description: "Only in detail response" }
        start_date: { type: string, format: date, nullable: true }
        next_generation_date: { type: string, format: date, nullable: true }
        last_generated_date: { type: string, format: date, nullable: true, description: "Only in detail response" }
        form_template_id: { type: string, format: uuid, nullable: true, description: "Published form attached to every work order this schedule generates" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    PmTemplate:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true }
        frequency: { type: string, nullable: true }
        custom_interval_weeks: { type: integer, nullable: true, description: "Only in detail response" }
        work_category: { type: string, nullable: true }
        estimated_hours: { type: number, nullable: true }
        estimated_cost: { type: number, nullable: true }
        safety_requirements: { type: string, nullable: true, description: "Only in detail response" }
        tasks: { type: array, nullable: true, items: { type: object, properties: { id: { type: string }, description: { type: string }, completed: { type: boolean } }, required: [id, description, completed] }, description: "Only in detail response" }
        resources: { type: array, nullable: true, items: { type: object }, description: "Only in detail response" }
        form_template_id: { type: string, format: uuid, nullable: true, description: "Published form inherited by schedules seeded from this template" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    FormTemplate:
      type: object
      properties:
        id: { type: string, format: uuid }
        lineage_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        version: { type: integer, nullable: true }
        is_current: { type: boolean, nullable: true }
        name: { type: string }
        description: { type: string, nullable: true }
        work_category_id: { type: string, format: uuid, nullable: true, description: "Work category (see /work-categories)" }
        status: { type: string, nullable: true, enum: [draft, published, archived] }
        created_by: { type: string, nullable: true, description: "Only in detail response" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    FormTemplateItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        template_id: { type: string, format: uuid }
        item_key: { type: string }
        sort_order: { type: integer }
        item_type: { type: string, enum: [section, checkbox, single_select, multi_select, number, text, photo] }
        label: { type: string }
        help_text: { type: string, nullable: true }
        required: { type: boolean, nullable: true }
        options: { type: array, nullable: true, items: { type: object, properties: { value: { type: string }, label: { type: string } }, required: [value, label] } }
        config: { type: object, nullable: true, description: "Per-type configuration (e.g. min, max, unit, multiline)" }
        visible_when: { type: object, nullable: true, properties: { itemKey: { type: string }, op: { type: string }, value: {} }, required: [itemKey, op], description: "Conditional-visibility rule referencing another item" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    FormResponse:
      type: object
      properties:
        id: { type: string, format: uuid }
        template_id: { type: string, format: uuid, nullable: true }
        template_version: { type: integer, nullable: true }
        lineage_id: { type: string, format: uuid, nullable: true }
        subject_type: { type: string, nullable: true }
        subject_id: { type: string, format: uuid, nullable: true }
        status: { type: string, nullable: true }
        snapshot: { type: object, nullable: true, description: "Frozen copy of the template at fill time. Only in detail response" }
        completed_by: { type: string, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    FormResponseCreate:
      type: object
      required: [template_id, subject_type, subject_id]
      properties:
        template_id:
          type: string
          format: uuid
          description: "Published form template to attach. A draft or archived template is rejected."
        subject_type:
          type: string
          enum: [work_order, pm_schedule, infrastructure_asset, compliance_record, site]
          description: "Kind of record the form is attached to"
        subject_id:
          type: string
          format: uuid
          description: "ID of the record, which must belong to the API key's tenant"

    FormResponseAnswer:
      type: object
      properties:
        id: { type: string, format: uuid }
        response_id: { type: string, format: uuid }
        item_key: { type: string }
        item_type: { type: string, nullable: true }
        value_text: { type: string, nullable: true }
        value_number: { type: number, nullable: true }
        value_boolean: { type: boolean, nullable: true }
        value_json: { type: object, nullable: true }
        answered_by: { type: string, nullable: true }
        answered_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Project:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true, description: "Only in detail response" }
        status: { type: string, nullable: true }
        project_code: { type: string, nullable: true }
        project_type: { type: string, nullable: true, enum: [capital, maintenance, repair, upgrade, new_construction, renovation, deferred_maintenance, other] }
        current_phase: { type: string, nullable: true, description: "Only in detail response" }
        budget_status: { type: string, nullable: true, description: "Only in detail response" }
        progress_status: { type: string, nullable: true, description: "Only in detail response" }
        image_url: { type: string, nullable: true, description: "Only in detail response" }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        budget: { type: number, nullable: true }
        committed_cost: { type: number, nullable: true, description: "Only in detail response" }
        total_invoiced: { type: number, nullable: true, description: "Only in detail response" }
        total_expenses: { type: number, nullable: true, description: "Only in detail response" }
        actual_cost: { type: number, nullable: true }
        project_manager: { type: string, nullable: true, description: "Clerk user ID" }
        progress_percentage: { type: number, nullable: true }
        health_status: { type: string, nullable: true }
        schedule_variance_days: { type: number, nullable: true, description: "Only in detail response" }
        cost_variance: { type: number, nullable: true, description: "Only in detail response" }
        project_sites:
          type: array
          nullable: true
          description: "Only in detail response"
          items:
            type: object
            properties:
              site_id: { type: string, format: uuid }
              sites: { $ref: "#/components/schemas/SiteRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Contract:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true, description: "Only in detail response" }
        company_id: { type: string, format: uuid, nullable: true }
        category: { type: string, nullable: true }
        purchase_order: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        extendable: { type: boolean, nullable: true }
        annual_cost: { type: number, nullable: true }
        quality_score: { type: number, nullable: true }
        vendors: { $ref: "#/components/schemas/VendorRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ComplianceItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        regulation_reference: { type: string, nullable: true }
        compliance_period_months: { type: integer, nullable: true }
        status: { type: string, nullable: true }
        system_id: { type: string, format: uuid, nullable: true }
        compliance_pm_schedules:
          type: array
          nullable: true
          description: "Only in detail response"
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              pm_schedule_id: { type: string, format: uuid }
              required_frequency_days: { type: integer, nullable: true }
              weight: { type: number, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    DashboardStats:
      type: object
      properties:
        total_assets: { type: integer }
        total_work_orders: { type: integer }
        work_orders_by_status:
          type: object
          additionalProperties: { type: integer }
          example: { NEW: 12, IN_PROGRESS: 8, COMPLETED: 45 }
        overdue_work_orders: { type: integer }
        active_pm_schedules: { type: integer }
        total_sites: { type: integer }
        total_buildings: { type: integer }
        generated_at: { type: string, format: date-time }

    Part:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        part_number: { type: string, nullable: true }
        category: { type: string, nullable: true }
        supplier: { type: string, nullable: true, description: "DEPRECATED — legacy free-text supplier name. Use supplier_id." }
        supplier_id: { type: string, format: uuid, nullable: true }
        cost: { type: number, nullable: true }
        quantity: { type: integer }
        desired_quantity: { type: integer }
        specific_location: { type: string, nullable: true, description: "Only in detail response" }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        sites: { $ref: "#/components/schemas/SiteRef" }
        buildings: { $ref: "#/components/schemas/BuildingRef" }
        locations: { $ref: "#/components/schemas/LocationRef" }
        vendors: { $ref: "#/components/schemas/VendorRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    AssetPart:
      type: object
      properties:
        id: { type: string, format: uuid }
        asset_id: { type: string, format: uuid }
        part_id: { type: string, format: uuid }
        quantity: { type: number, nullable: true }
        assets:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
        parts:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            part_number: { type: string, nullable: true }
            category: { type: string, nullable: true }
            cost: { type: number, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Vendor:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        address: { type: string, nullable: true, description: "Only in detail response" }
        city: { type: string, nullable: true }
        state: { type: string, nullable: true }
        country: { type: string, nullable: true }
        status: { type: string, nullable: true }
        categories: { type: array, items: { type: string }, nullable: true, description: "Vendor categories (e.g. HVAC, Plumbing)" }
        website: { type: string, nullable: true }
        description: { type: string, nullable: true, description: "Only in detail response" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    WorkRequest:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true, description: "Only in detail response" }
        status: { type: string, enum: [PENDING_REVIEW, APPROVED, REJECTED] }
        priority: { type: string, nullable: true }
        requester_id: { type: string, nullable: true, description: "Clerk user ID" }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        system_id: { type: string, format: uuid, nullable: true }
        work_category_id: { type: string, format: uuid, nullable: true }
        attachments: { type: array, nullable: true, items: { type: object }, description: "Only in detail response" }
        rejection_comment: { type: string, nullable: true, description: "Only in detail response" }
        work_order_id: { type: string, format: uuid, nullable: true }
        processed_by: { type: string, nullable: true, description: "Clerk user ID. Only in detail response" }
        processed_at: { type: string, format: date-time, nullable: true, description: "Only in detail response" }
        sites: { $ref: "#/components/schemas/SiteRef" }
        buildings: { $ref: "#/components/schemas/BuildingRef" }
        submitted_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Invoice:
      type: object
      properties:
        id: { type: string, format: uuid }
        invoice_number: { type: string }
        description: { type: string, nullable: true }
        amount: { type: number }
        tax_amount: { type: number, nullable: true }
        invoice_date: { type: string, format: date }
        due_date: { type: string, format: date, nullable: true }
        paid_date: { type: string, format: date, nullable: true }
        status: { type: string, enum: [pending, approved, paid, voided] }
        notes: { type: string, nullable: true, description: "Only in detail response" }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        purchase_order_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        vendor_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID. Only in detail response" }
        vendors: { $ref: "#/components/schemas/VendorRef" }
        projects: { $ref: "#/components/schemas/ProjectRef" }
        cost_categories: { $ref: "#/components/schemas/CostCategoryRef", description: "Only in detail response" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    PurchaseOrder:
      type: object
      properties:
        id: { type: string, format: uuid }
        po_number: { type: string }
        description: { type: string, nullable: true }
        amount: { type: number }
        status: { type: string, enum: [draft, issued, partially_received, received, closed, cancelled] }
        issued_date: { type: string, format: date, nullable: true }
        expected_date: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true, description: "Only in detail response" }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true, description: "Only in detail response" }
        vendor_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID. Only in detail response" }
        vendors: { $ref: "#/components/schemas/VendorRef" }
        projects: { $ref: "#/components/schemas/ProjectRef" }
        cost_categories: { $ref: "#/components/schemas/CostCategoryRef", description: "Only in detail response" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Expense:
      type: object
      properties:
        id: { type: string, format: uuid }
        description: { type: string }
        amount: { type: number }
        expense_date: { type: string, format: date }
        receipt_url: { type: string, nullable: true }
        notes: { type: string, nullable: true, description: "Only in detail response" }
        submitted_by: { type: string, nullable: true, description: "Clerk user ID. Only in detail response" }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }
        projects: { $ref: "#/components/schemas/ProjectRef" }
        work_orders: { $ref: "#/components/schemas/WorkOrderRef", description: "Only in detail response" }
        cost_categories: { $ref: "#/components/schemas/CostCategoryRef", description: "Only in detail response" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    AssetType:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        group_id: { type: string, format: uuid, nullable: true }
        asset_type_groups: { $ref: "#/components/schemas/AssetTypeGroupRef" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    WorkCategory:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Manufacturer:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        website: { type: string, nullable: true }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        notes: { type: string, nullable: true, description: "Only in detail response" }
        system_class_ids: { type: array, items: { type: string, format: uuid }, nullable: true, description: "Only in detail response" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    BuildingType:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    LocationType:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectPhaseCategory:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        sort_order: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    CostCategory:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        parent_id: { type: string, format: uuid, nullable: true }
        is_active: { type: boolean }
        sort_order: { type: integer }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    Budget:
      type: object
      properties:
        id: { type: string, format: uuid }
        year: { type: integer }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        funding_source: { type: integer, nullable: true }
        allocated_amount: { type: number, nullable: true }
        budgeted_amount: { type: number, nullable: true }
        remaining_amount: { type: number, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    AssetComment:
      type: object
      properties:
        id: { type: string, format: uuid }
        asset_id: { type: string, format: uuid }
        user_id: { type: string, nullable: true, description: "Clerk user ID" }
        comment: { type: string }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }
        asset:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            name: { type: string }

    AssetStatus:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    AssetStatusCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    AssetStatusUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    AssetCost:
      type: object
      properties:
        id: { type: string, format: uuid }
        asset_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        category: { type: string, nullable: true, enum: [Repair, PM, Operation, Replacement, Decommission, Other] }
        amount: { type: number }
        cost_date: { type: string, format: date, nullable: true }
        description: { type: string, nullable: true }
        invoice_number: { type: string, nullable: true }
        po_number: { type: string, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        work_order_number: { type: integer, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }
        asset:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
        sites: { $ref: "#/components/schemas/SiteRef" }
        buildings: { $ref: "#/components/schemas/BuildingRef" }

    AssetReplacementPlan:
      type: object
      properties:
        id: { type: string, format: uuid }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        planned_replacement_year: { type: integer, nullable: true }
        planned_replacement_date: { type: string, format: date, nullable: true }
        estimated_cost: { type: number, nullable: true }
        funding_source: { type: string, nullable: true }
        priority: { type: string, nullable: true, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
        status: { type: string, nullable: true, enum: [PLANNED, BUDGETED, APPROVED, COMPLETED, CANCELLED] }
        notes: { type: string, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    AssetRiskHistory:
      type: object
      properties:
        id: { type: string, format: uuid }
        asset_id: { type: string, format: uuid, nullable: true }
        risk_score: { type: integer, nullable: true }
        consequence_of_failure_score: { type: integer, nullable: true }
        likelihood_of_failure_score: { type: integer, nullable: true }
        condition_score: { type: integer, nullable: true }
        risk_factor: { type: string, nullable: true, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
        recorded_date: { type: string, format: date, nullable: true }
        trigger_event: { type: string, nullable: true, enum: [maintenance, inspection, manual_update, scheduled] }
        work_order_id: { type: string, format: uuid, nullable: true }
        notes: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        asset:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            name: { type: string }

    WorkOrderComment:
      type: object
      properties:
        id: { type: string, format: uuid }
        work_order_id: { type: string, format: uuid }
        comment: { type: string }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_by_name: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }

    WorkOrderSchedule:
      type: object
      properties:
        id: { type: string, format: uuid }
        work_order_id: { type: string, format: uuid }
        technician_id: { type: string, description: "Clerk user ID" }
        scheduled_date: { type: string, format: date }
        scheduled_start_time: { type: string, nullable: true, description: "HH:MM:SS" }
        scheduled_end_time: { type: string, nullable: true, description: "HH:MM:SS" }
        duration_minutes: { type: integer, nullable: true }
        break_time_minutes: { type: integer, nullable: true }
        travel_time_minutes: { type: integer, nullable: true, description: "Straight-line travel estimate from the previous day-plan stop" }
        stop_order: { type: integer, nullable: true, description: "1-based position in the technician's day plan; null for calendar entries outside a plan" }
        scheduling_notes: { type: string, nullable: true }
        auto_scheduled: { type: boolean, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectTask:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        phase_id: { type: string, format: uuid, nullable: true }
        parent_task_id: { type: string, format: uuid, nullable: true }
        title: { type: string }
        description: { type: string, nullable: true }
        assigned_to: { type: string, nullable: true, description: "Clerk user ID" }
        due_date: { type: string, format: date, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [todo, in_progress, completed, blocked, cancelled] }
        priority: { type: string, nullable: true, enum: [low, medium, high, critical] }
        estimated_hours: { type: number, nullable: true }
        actual_hours: { type: number, nullable: true }
        completion_percentage: { type: integer, nullable: true }
        estimated_cost: { type: number, nullable: true }
        actual_cost: { type: number, nullable: true }
        cost_to_complete: { type: number, nullable: true }
        sequence_order: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }

    ProjectMilestone:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        phase_id: { type: string, format: uuid, nullable: true }
        name: { type: string }
        description: { type: string, nullable: true }
        due_date: { type: string, format: date, nullable: true }
        completed_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [pending, completed, missed, at_risk] }
        is_critical: { type: boolean, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectRisk:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true }
        category: { type: string, nullable: true, enum: [technical, financial, schedule, resource, external] }
        probability: { type: string, nullable: true, enum: [low, medium, high] }
        impact: { type: string, nullable: true, enum: [low, medium, high, critical] }
        status: { type: string, nullable: true, enum: [identified, analyzing, mitigating, resolved, accepted] }
        mitigation_plan: { type: string, nullable: true }
        contingency_plan: { type: string, nullable: true }
        owner_id: { type: string, nullable: true, description: "Clerk user ID" }
        due_date: { type: string, format: date, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectPhase:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        sequence_order: { type: integer, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [pending, in_progress, completed, skipped] }
        completion_percentage: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectBudgetItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        category: { type: string, nullable: true, enum: [labor, materials, equipment, subcontractors, permits, contingency, other] }
        description: { type: string, nullable: true }
        planned_amount: { type: number, nullable: true }
        actual_amount: { type: number, nullable: true }
        notes: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectTimeEntry:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        task_id: { type: string, format: uuid, nullable: true }
        user_id: { type: string, nullable: true, description: "Clerk user ID" }
        user_name: { type: string, nullable: true }
        start_time: { type: string, format: date-time, nullable: true }
        end_time: { type: string, format: date-time, nullable: true }
        duration_minutes: { type: integer, nullable: true }
        description: { type: string, nullable: true }
        is_billable: { type: boolean, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectComment:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        parent_id: { type: string, format: uuid, nullable: true }
        author_id: { type: string, nullable: true, description: "Clerk user ID" }
        content: { type: string }
        is_edited: { type: boolean, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectTeamMember:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        user_id: { type: string, description: "Clerk user ID" }
        role: { type: string }
        responsibilities: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        is_active: { type: boolean, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectTaskDependency:
      type: object
      properties:
        id: { type: string, format: uuid }
        task_id: { type: string, format: uuid }
        depends_on_task_id: { type: string, format: uuid }
        dependency_type: { type: string, nullable: true, enum: [finish_to_start, start_to_start, finish_to_finish, start_to_finish] }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectUpdate:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        author_id: { type: string, description: "Clerk user ID" }
        timeframe: { type: string, enum: [monthly, quarterly, bi-annually, annually] }
        period_year: { type: integer }
        period_value: { type: string }
        title: { type: string, nullable: true }
        content: { type: string }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    ProjectCostSnapshot:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        snapshot_date: { type: string, format: date }
        total_budget: { type: number }
        actual_cost: { type: number }
        forecasted_cost: { type: number, nullable: true }
        percent_complete: { type: number, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectLocation:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        location_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectSite:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectSiteCreate:
      type: object
      required: [project_id, site_id]
      properties:
        project_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }

    ProjectBuilding:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        building_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectBuildingCreate:
      type: object
      required: [project_id, building_id]
      properties:
        project_id: { type: string, format: uuid }
        building_id: { type: string, format: uuid }

    ProjectSystem:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        system_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectSystemCreate:
      type: object
      required: [project_id, system_id]
      properties:
        project_id: { type: string, format: uuid }
        system_id: { type: string, format: uuid }

    ProjectSystemClass:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        system_class_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectSystemClassCreate:
      type: object
      required: [project_id, system_class_id]
      properties:
        project_id: { type: string, format: uuid }
        system_class_id: { type: string, format: uuid }

    ProjectSystemGroup:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        system_group_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectSystemGroupCreate:
      type: object
      required: [project_id, system_group_id]
      properties:
        project_id: { type: string, format: uuid }
        system_group_id: { type: string, format: uuid }

    ProjectAsset:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        asset_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time, nullable: true }

    ProjectAssetCreate:
      type: object
      required: [project_id, asset_id]
      properties:
        project_id: { type: string, format: uuid }
        asset_id: { type: string, format: uuid }

    ComplianceItemCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        regulation_reference: { type: string, nullable: true }
        compliance_period_months: { type: integer, nullable: true }
        status: { type: string, nullable: true, enum: [active, archived] }
        system_id: { type: string, format: uuid, nullable: true }

    ComplianceItemUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        regulation_reference: { type: string, nullable: true }
        compliance_period_months: { type: integer, nullable: true }
        status: { type: string, nullable: true, enum: [active, archived] }
        system_id: { type: string, format: uuid, nullable: true }

    ComplianceRecord:
      type: object
      properties:
        id: { type: string, format: uuid }
        compliance_item_id: { type: string, format: uuid }
        pm_schedule_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        completed_by: { type: string, nullable: true, description: "Clerk user ID" }
        required_frequency_days: { type: integer, nullable: true }
        days_since_last_completion: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }

    ComplianceRecordCreate:
      type: object
      required: [compliance_item_id, pm_schedule_id, work_order_id, completed_at, required_frequency_days]
      properties:
        compliance_item_id: { type: string, format: uuid }
        pm_schedule_id: { type: string, format: uuid }
        work_order_id: { type: string, format: uuid }
        completed_at: { type: string, format: date-time }
        completed_by: { type: string, nullable: true }
        required_frequency_days: { type: integer }
        days_since_last_completion: { type: integer, nullable: true }

    ComplianceRecordUpdate:
      type: object
      properties:
        completed_at: { type: string, format: date-time }
        completed_by: { type: string, nullable: true }
        required_frequency_days: { type: integer }
        days_since_last_completion: { type: integer, nullable: true }

    SiteFciHistory:
      type: object
      properties:
        id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        fci_value: { type: number, nullable: true }
        recorded_date: { type: string, format: date, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        site: { $ref: "#/components/schemas/SiteRef" }

    DashboardSnapshot:
      type: object
      properties:
        id: { type: string, format: uuid }
        snapshot_date: { type: string, format: date, nullable: true }
        snapshot_month: { type: integer, nullable: true }
        snapshot_year: { type: integer, nullable: true }
        total_assets: { type: integer, nullable: true }
        critical_assets: { type: integer, nullable: true }
        avg_condition: { type: number, nullable: true }
        end_of_life_assets: { type: integer, nullable: true }
        total_crv: { type: number, nullable: true }
        open_work_orders: { type: integer, nullable: true }
        high_priority_work_orders: { type: integer, nullable: true }
        overdue_work_orders: { type: integer, nullable: true }
        completed_work_orders: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }

    VendorSiteAssignment:
      type: object
      properties:
        id: { type: string, format: uuid }
        vendor_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        assigned_by: { type: string, nullable: true, description: "Clerk user ID" }
        created_at: { type: string, format: date-time, nullable: true }
        vendor: { $ref: "#/components/schemas/VendorRef" }
        site: { $ref: "#/components/schemas/SiteRef" }

    ContractSite:
      type: object
      properties:
        contract_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        contract:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
        site: { $ref: "#/components/schemas/SiteRef" }

    CustomFieldDefinition:
      type: object
      properties:
        id: { type: string, format: uuid }
        entity_type: { type: string }
        field_name: { type: string }
        field_label: { type: string }
        field_type: { type: string, enum: [text, number, date, boolean, select] }
        field_options: { type: object, nullable: true }
        is_required: { type: boolean, nullable: true }
        display_order: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    CustomFieldValue:
      type: object
      properties:
        id: { type: string, format: uuid }
        entity_id: { type: string, format: uuid }
        field_definition_id: { type: string, format: uuid }
        value_text: { type: string, nullable: true }
        value_number: { type: number, nullable: true }
        value_date: { type: string, format: date, nullable: true }
        value_boolean: { type: boolean, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    PartCategory:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    # ── Create / Update input schemas ──

    AssetCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        model: { type: string, nullable: true }
        serial_number: { type: string, nullable: true }
        purchase_cost: { type: number, nullable: true }
        purchase_date: { type: string, format: date, nullable: true }
        expected_lifetime_years: { type: number, nullable: true }
        condition_score: { type: number, nullable: true }
        risk_factor: { type: string, nullable: true, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
        status_id: { type: string, nullable: true }
        qr_code: { type: string, nullable: true }
        image_url: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        system_id: { type: string, format: uuid, nullable: true }
        safety_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        service_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        environmental_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        regulatory_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        reputation_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        consequence_of_failure_score: { type: integer, nullable: true }
        likelihood_of_failure_score: { type: integer, nullable: true }
        current_meter_reading: { type: number, nullable: true, description: "Current meter/odometer reading" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }

    AssetUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        model: { type: string, nullable: true }
        serial_number: { type: string, nullable: true }
        purchase_cost: { type: number, nullable: true }
        purchase_date: { type: string, format: date, nullable: true }
        expected_lifetime_years: { type: number, nullable: true }
        condition_score: { type: number, nullable: true }
        risk_factor: { type: string, nullable: true, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
        status_id: { type: string, nullable: true }
        qr_code: { type: string, nullable: true }
        image_url: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        system_id: { type: string, format: uuid, nullable: true }
        safety_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        service_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        environmental_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        regulatory_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        reputation_impact: { type: string, nullable: true, enum: [LOW, MEDIUM, HIGH, CRITICAL] }
        consequence_of_failure_score: { type: integer, nullable: true }
        likelihood_of_failure_score: { type: integer, nullable: true }
        current_meter_reading: { type: number, nullable: true, description: "Current meter/odometer reading" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }

    WorkOrderCreate:
      type: object
      required: [title]
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        priority: { type: string, enum: [LOW, MEDIUM, HIGH, URGENT] }
        status: { type: string, enum: [NEW, IN_PROGRESS, ON_HOLD, REJECTED, COMPLETED, CANCELLED] }
        type: { type: string, nullable: true, enum: [PM, REACTIVE] }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        start_date: { type: string, format: date, nullable: true }
        due_date: { type: string, format: date, nullable: true }
        estimated_time: { type: number, nullable: true, description: "Estimated hours" }
        estimated_cost: { type: number, nullable: true }
        assigned_to: { type: string, nullable: true, description: "Single user ID — mapped to assignees array" }
        assignees: { type: array, items: { type: string }, nullable: true, description: "Array of assigned Clerk user IDs (alternative to assigned_to)" }
        image_url: { type: string, nullable: true, description: "Image URL (upload via POST /upload-urls with bucket asset-images)" }
        meter_reading: { type: number, nullable: true, description: "Meter/odometer reading at time of service" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }

    WorkOrderUpdate:
      type: object
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        priority: { type: string, enum: [LOW, MEDIUM, HIGH, URGENT] }
        status: { type: string, enum: [NEW, IN_PROGRESS, ON_HOLD, REJECTED, COMPLETED, CANCELLED] }
        type: { type: string, nullable: true, enum: [PM, REACTIVE] }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        start_date: { type: string, format: date, nullable: true }
        due_date: { type: string, format: date, nullable: true }
        estimated_time: { type: number, nullable: true, description: "Estimated hours" }
        estimated_cost: { type: number, nullable: true }
        actual_hours: { type: number, nullable: true }
        completion_notes: { type: string, nullable: true }
        assigned_to: { type: string, nullable: true, description: "Single user ID — mapped to assignees array" }
        assignees: { type: array, items: { type: string }, nullable: true, description: "Array of assigned Clerk user IDs (alternative to assigned_to)" }
        image_url: { type: string, nullable: true, description: "Image URL" }
        meter_reading: { type: number, nullable: true, description: "Meter/odometer reading at time of service" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }

    SiteCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        address: { type: string, nullable: true }
        city: { type: string, nullable: true }
        province: { type: string, nullable: true }
        postal code: { type: string, nullable: true }
        country: { type: string, nullable: true }
        Description: { type: string, nullable: true }
        square_footage: { type: number, nullable: true }
        cost_per_sqft: { type: number, nullable: true }
        additional_cost_per_sqft: { type: number, nullable: true }
        operational_cost_per_sqft: { type: number, nullable: true }
        year_built: { type: integer, nullable: true }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        owner_landlord: { type: string, nullable: true }
        ownership_type: { type: string, nullable: true, enum: [Owned, Leased, Managed] }
        lease_start_date: { type: string, format: date, nullable: true }
        lease_end_date: { type: string, format: date, nullable: true }
        renewal_option: { type: string, nullable: true }
        lease_details: { type: string, nullable: true }
        insurance_provider: { type: string, nullable: true }
        insurance_policy_number: { type: string, nullable: true }
        property_manager_company: { type: string, nullable: true }
        property_manager_contact_name: { type: string, nullable: true }
        property_manager_contact_email: { type: string, nullable: true }
        property_manager_contact_phone: { type: string, nullable: true }

    SiteUpdate:
      type: object
      properties:
        name: { type: string }
        address: { type: string, nullable: true }
        city: { type: string, nullable: true }
        province: { type: string, nullable: true }
        postal code: { type: string, nullable: true }
        country: { type: string, nullable: true }
        Description: { type: string, nullable: true }
        square_footage: { type: number, nullable: true }
        cost_per_sqft: { type: number, nullable: true }
        additional_cost_per_sqft: { type: number, nullable: true }
        operational_cost_per_sqft: { type: number, nullable: true }
        year_built: { type: integer, nullable: true }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        owner_landlord: { type: string, nullable: true }
        ownership_type: { type: string, nullable: true, enum: [Owned, Leased, Managed] }
        lease_start_date: { type: string, format: date, nullable: true }
        lease_end_date: { type: string, format: date, nullable: true }
        renewal_option: { type: string, nullable: true }
        lease_details: { type: string, nullable: true }
        insurance_provider: { type: string, nullable: true }
        insurance_policy_number: { type: string, nullable: true }
        property_manager_company: { type: string, nullable: true }
        property_manager_contact_name: { type: string, nullable: true }
        property_manager_contact_email: { type: string, nullable: true }
        property_manager_contact_phone: { type: string, nullable: true }

    BuildingCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        floors: { type: integer, nullable: true }
        area_sqft: { type: number, nullable: true, description: "Area in square feet" }
        type: { type: string, nullable: true, description: "Building type label" }
        building_type_id: { type: string, format: uuid, nullable: true }
        year_built: { type: integer, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }

    BuildingUpdate:
      type: object
      properties:
        name: { type: string }
        floors: { type: integer, nullable: true }
        area_sqft: { type: number, nullable: true, description: "Area in square feet" }
        type: { type: string, nullable: true, description: "Building type label" }
        building_type_id: { type: string, format: uuid, nullable: true }
        year_built: { type: integer, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }

    LocationCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        building_id: { type: string, format: uuid, nullable: true }
        floor: { type: string, nullable: true }
        area: { type: string, nullable: true }
        type: { type: string, nullable: true }
        location_type_id: { type: string, format: uuid, nullable: true }

    LocationUpdate:
      type: object
      properties:
        name: { type: string }
        building_id: { type: string, format: uuid, nullable: true }
        floor: { type: string, nullable: true }
        area: { type: string, nullable: true }
        type: { type: string, nullable: true }
        location_type_id: { type: string, format: uuid, nullable: true }

    SystemCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        system_group_id: { type: string, format: uuid, nullable: true }
        crv_multiplier: { type: number, nullable: true }

    SystemUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        system_group_id: { type: string, format: uuid, nullable: true }
        crv_multiplier: { type: number, nullable: true }

    PmScheduleCreate:
      type: object
      required: [title]
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        frequency: { type: string, nullable: true }
        custom_interval_weeks: { type: integer, nullable: true }
        next_due: { type: string, format: date, nullable: true }
        estimated_hours: { type: number, nullable: true }
        estimated_cost: { type: number, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        asset_ids: { type: array, items: { type: string }, nullable: true }
        system_ids: { type: array, items: { type: string }, nullable: true }
        location_ids: { type: array, items: { type: string }, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        work_category: { type: string, nullable: true }
        schedule_type: { type: string, nullable: true }
        lead_time_days: { type: integer, nullable: true }
        grace_period_days: { type: integer, nullable: true }
        safety_requirements: { type: string, nullable: true }
        status: { type: string, nullable: true }
        auto_generate_wo: { type: boolean, nullable: true }
        floating: { type: boolean, nullable: true }
        meter_based: { type: boolean, nullable: true, description: "Whether this is a meter-based PM schedule" }
        meter_interval: { type: number, nullable: true, description: "Trigger every N meter units" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }
        tasks: { type: array, nullable: true, items: { type: object, properties: { id: { type: string }, description: { type: string }, completed: { type: boolean } }, required: [id, description, completed] } }
        resources: { type: array, nullable: true, items: { type: object } }
        start_date: { type: string, format: date, nullable: true }
        form_template_id: { type: string, format: uuid, nullable: true, description: "Published form template to attach to every work order this schedule generates. Must belong to your tenant; a draft attaches nothing until published." }

    PmScheduleUpdate:
      type: object
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        frequency: { type: string, nullable: true }
        custom_interval_weeks: { type: integer, nullable: true }
        next_due: { type: string, format: date, nullable: true }
        estimated_hours: { type: number, nullable: true }
        estimated_cost: { type: number, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        asset_ids: { type: array, items: { type: string }, nullable: true }
        system_ids: { type: array, items: { type: string }, nullable: true }
        location_ids: { type: array, items: { type: string }, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        work_category: { type: string, nullable: true }
        schedule_type: { type: string, nullable: true }
        lead_time_days: { type: integer, nullable: true }
        grace_period_days: { type: integer, nullable: true }
        safety_requirements: { type: string, nullable: true }
        status: { type: string, nullable: true }
        auto_generate_wo: { type: boolean, nullable: true }
        floating: { type: boolean, nullable: true }
        meter_based: { type: boolean, nullable: true, description: "Whether this is a meter-based PM schedule" }
        meter_interval: { type: number, nullable: true, description: "Trigger every N meter units" }
        meter_unit: { type: string, nullable: true, description: "Meter unit (km, miles, hours, cycles)" }
        tasks: { type: array, nullable: true, items: { type: object, properties: { id: { type: string }, description: { type: string }, completed: { type: boolean } }, required: [id, description, completed] } }
        resources: { type: array, nullable: true, items: { type: object } }
        start_date: { type: string, format: date, nullable: true }
        form_template_id: { type: string, format: uuid, nullable: true, description: "Published form template to attach to every work order this schedule generates. Must belong to your tenant; a draft attaches nothing until published." }

    PmTemplateCreate:
      type: object
      required: [title]
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        frequency: { type: string, nullable: true }
        custom_interval_weeks: { type: integer, nullable: true }
        work_category: { type: string, nullable: true }
        work_category_id: { type: string, format: uuid, nullable: true }
        estimated_hours: { type: number, nullable: true }
        estimated_cost: { type: number, nullable: true }
        safety_requirements: { type: string, nullable: true }
        tasks: { type: array, nullable: true, items: { type: object, properties: { id: { type: string }, description: { type: string }, completed: { type: boolean } }, required: [id, description, completed] } }
        resources: { type: array, nullable: true, items: { type: object } }
        documents: { type: array, nullable: true, items: { type: object } }
        asset_ids: { type: array, items: { type: string, format: uuid }, nullable: true }
        location_ids: { type: array, items: { type: string, format: uuid }, nullable: true }
        form_template_id: { type: string, format: uuid, nullable: true, description: "Published form template inherited by schedules seeded from this template" }

    PmTemplateUpdate:
      type: object
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        frequency: { type: string, nullable: true }
        custom_interval_weeks: { type: integer, nullable: true }
        work_category: { type: string, nullable: true }
        work_category_id: { type: string, format: uuid, nullable: true }
        estimated_hours: { type: number, nullable: true }
        estimated_cost: { type: number, nullable: true }
        safety_requirements: { type: string, nullable: true }
        tasks: { type: array, nullable: true, items: { type: object, properties: { id: { type: string }, description: { type: string }, completed: { type: boolean } }, required: [id, description, completed] } }
        resources: { type: array, nullable: true, items: { type: object } }
        documents: { type: array, nullable: true, items: { type: object } }
        asset_ids: { type: array, items: { type: string, format: uuid }, nullable: true }
        location_ids: { type: array, items: { type: string, format: uuid }, nullable: true }
        form_template_id: { type: string, format: uuid, nullable: true, description: "Published form template inherited by schedules seeded from this template" }

    FormTemplateCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 500 }
        description: { type: string, nullable: true, maxLength: 5000 }
        work_category_id: { type: string, format: uuid, nullable: true, description: "Work category (see /work-categories)" }
        status: { type: string, nullable: true, enum: [draft, published, archived] }

    FormTemplateUpdate:
      type: object
      properties:
        name: { type: string, maxLength: 500 }
        description: { type: string, nullable: true, maxLength: 5000 }
        work_category_id: { type: string, format: uuid, nullable: true, description: "Work category (see /work-categories)" }
        status: { type: string, nullable: true, enum: [draft, published, archived] }

    FormTemplateItemCreate:
      type: object
      required: [template_id, item_key, sort_order, item_type, label]
      properties:
        template_id: { type: string, format: uuid }
        item_key: { type: string, maxLength: 200 }
        sort_order: { type: integer, minimum: 0 }
        item_type: { type: string, enum: [section, checkbox, single_select, multi_select, number, text, photo] }
        label: { type: string, maxLength: 2000 }
        help_text: { type: string, nullable: true, maxLength: 5000 }
        required: { type: boolean, nullable: true }
        options: { type: array, nullable: true, items: { type: object, properties: { value: { type: string }, label: { type: string } }, required: [value, label] } }
        config: { type: object, nullable: true }
        visible_when: { type: object, nullable: true, properties: { itemKey: { type: string }, op: { type: string }, value: {} }, required: [itemKey, op] }

    FormTemplateItemUpdate:
      type: object
      description: "template_id and item_key are immutable and are silently stripped."
      properties:
        sort_order: { type: integer, minimum: 0 }
        item_type: { type: string, enum: [section, checkbox, single_select, multi_select, number, text, photo] }
        label: { type: string, maxLength: 2000 }
        help_text: { type: string, nullable: true, maxLength: 5000 }
        required: { type: boolean, nullable: true }
        options: { type: array, nullable: true, items: { type: object, properties: { value: { type: string }, label: { type: string } }, required: [value, label] } }
        config: { type: object, nullable: true }
        visible_when: { type: object, nullable: true, properties: { itemKey: { type: string }, op: { type: string }, value: {} }, required: [itemKey, op] }

    ProjectCreate:
      type: object
      # start_date has always been required by the gateway; the spec omitted it (F-160).
      required: [name, start_date]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        # Optional since F-160, defaults to "planning". The project lifecycle lives on
        # current_phase, whose meaning is configured per phase in Settings.
        status: { type: string, nullable: true }
        project_code: { type: string, nullable: true }
        project_type: { type: string, nullable: true, enum: [capital, maintenance, repair, upgrade, new_construction, renovation, deferred_maintenance, other] }
        current_phase: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        budget: { type: number, nullable: true }
        project_manager: { type: string, nullable: true, description: "Clerk user ID" }
        progress_percentage: { type: number, nullable: true }
        health_status: { type: string, nullable: true, enum: [on_track, at_risk, delayed, critical] }
        budget_status: { type: string, nullable: true, enum: [off_track, on_track, not_set, monitor] }
        progress_status: { type: string, nullable: true, enum: [off_track, on_track, monitor] }
        image_url: { type: string, nullable: true }

    ProjectInput:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        status: { type: string, nullable: true }
        project_code: { type: string, nullable: true }
        project_type: { type: string, nullable: true, enum: [capital, maintenance, repair, upgrade, new_construction, renovation, deferred_maintenance, other] }
        current_phase: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        budget: { type: number, nullable: true }
        project_manager: { type: string, nullable: true, description: "Clerk user ID" }
        progress_percentage: { type: number, nullable: true }
        health_status: { type: string, nullable: true, enum: [on_track, at_risk, delayed, critical] }
        budget_status: { type: string, nullable: true, enum: [off_track, on_track, not_set, monitor] }
        progress_status: { type: string, nullable: true, enum: [off_track, on_track, monitor] }
        image_url: { type: string, nullable: true }

    ContractCreate:
      type: object
      required: [title]
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        company_id: { type: string, format: uuid, nullable: true }
        category: { type: string, nullable: true }
        purchase_order: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        extendable: { type: boolean, nullable: true }
        annual_cost: { type: number, nullable: true }
        quality_score: { type: number, nullable: true }

    ContractUpdate:
      type: object
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        company_id: { type: string, format: uuid, nullable: true }
        category: { type: string, nullable: true }
        purchase_order: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        extendable: { type: boolean, nullable: true }
        annual_cost: { type: number, nullable: true }
        quality_score: { type: number, nullable: true }

    ContractSiteCreate:
      type: object
      required: [contract_id, site_id]
      properties:
        contract_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }

    VendorCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        address: { type: string, nullable: true }
        city: { type: string, nullable: true }
        state: { type: string, nullable: true }
        country: { type: string, nullable: true }
        status: { type: string, nullable: true }
        categories: { type: array, items: { type: string }, nullable: true, description: "Vendor categories (e.g. [\"HVAC\", \"Plumbing\"]). Also accepts singular 'category' string for backwards compatibility." }
        website: { type: string, nullable: true }
        description: { type: string, nullable: true }

    VendorUpdate:
      type: object
      properties:
        name: { type: string }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        address: { type: string, nullable: true }
        city: { type: string, nullable: true }
        state: { type: string, nullable: true }
        country: { type: string, nullable: true }
        status: { type: string, nullable: true }
        categories: { type: array, items: { type: string }, nullable: true, description: "Vendor categories (e.g. [\"HVAC\", \"Plumbing\"]). Also accepts singular 'category' string for backwards compatibility." }
        website: { type: string, nullable: true }
        description: { type: string, nullable: true }

    VendorSiteAssignmentCreate:
      type: object
      required: [vendor_id, site_id]
      properties:
        vendor_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }

    WorkRequestCreate:
      type: object
      required: [title]
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        priority: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        system_id: { type: string, format: uuid, nullable: true }
        work_category_id: { type: string, format: uuid, nullable: true }
        attachments: { type: array, nullable: true, items: { type: object } }

    WorkRequestUpdate:
      type: object
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        status: { type: string, enum: [PENDING_REVIEW, APPROVED, REJECTED] }
        priority: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        asset_id: { type: string, format: uuid, nullable: true }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        system_id: { type: string, format: uuid, nullable: true }
        work_category_id: { type: string, format: uuid, nullable: true }
        rejection_comment: { type: string, nullable: true }

    InvoiceCreate:
      type: object
      required: [invoice_number, amount, invoice_date, status]
      properties:
        invoice_number: { type: string }
        description: { type: string, nullable: true }
        amount: { type: number }
        tax_amount: { type: number, nullable: true }
        invoice_date: { type: string, format: date }
        due_date: { type: string, format: date, nullable: true }
        paid_date: { type: string, format: date, nullable: true }
        status: { type: string, enum: [pending, approved, paid, voided] }
        notes: { type: string, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        purchase_order_id: { type: string, format: uuid, nullable: true }
        vendor_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }

    InvoiceUpdate:
      type: object
      properties:
        invoice_number: { type: string }
        description: { type: string, nullable: true }
        amount: { type: number }
        tax_amount: { type: number, nullable: true }
        invoice_date: { type: string, format: date }
        due_date: { type: string, format: date, nullable: true }
        paid_date: { type: string, format: date, nullable: true }
        status: { type: string, enum: [pending, approved, paid, voided] }
        notes: { type: string, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        purchase_order_id: { type: string, format: uuid, nullable: true }
        vendor_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }

    PurchaseOrderCreate:
      type: object
      required: [po_number, amount, status]
      properties:
        po_number: { type: string }
        description: { type: string, nullable: true }
        amount: { type: number }
        status: { type: string, enum: [draft, issued, partially_received, received, closed, cancelled] }
        issued_date: { type: string, format: date, nullable: true }
        expected_date: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        vendor_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }

    PurchaseOrderUpdate:
      type: object
      properties:
        po_number: { type: string }
        description: { type: string, nullable: true }
        amount: { type: number }
        status: { type: string, enum: [draft, issued, partially_received, received, closed, cancelled] }
        issued_date: { type: string, format: date, nullable: true }
        expected_date: { type: string, format: date, nullable: true }
        notes: { type: string, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        vendor_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }

    ExpenseCreate:
      type: object
      required: [description, amount, expense_date]
      properties:
        description: { type: string }
        amount: { type: number }
        expense_date: { type: string, format: date }
        receipt_url: { type: string, nullable: true }
        notes: { type: string, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }

    ExpenseUpdate:
      type: object
      properties:
        description: { type: string }
        amount: { type: number }
        expense_date: { type: string, format: date }
        receipt_url: { type: string, nullable: true }
        notes: { type: string, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        category_id: { type: string, format: uuid, nullable: true }

    ChangeOrder:
      type: object
      properties:
        id: { type: string, format: uuid }
        co_number: { type: string }
        description: { type: string }
        reason: { type: string }
        amount: { type: number, description: "Positive for cost increases, negative for credits" }
        status: { type: string, enum: [draft, submitted, approved, rejected] }
        project_id: { type: string, format: uuid }
        vendor_id: { type: string, format: uuid }
        category_id: { type: string, format: uuid }
        approved_by: { type: string }
        approved_at: { type: string, format: date-time }
        notes: { type: string }
        created_by: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ChangeOrderCreate:
      type: object
      required: [co_number, description, amount]
      properties:
        co_number: { type: string, maxLength: 200 }
        description: { type: string }
        amount: { type: number }
        status: { type: string, enum: [draft, submitted, approved, rejected] }
        reason: { type: string }
        notes: { type: string }
        project_id: { type: string, format: uuid }
        vendor_id: { type: string, format: uuid }
        category_id: { type: string, format: uuid }

    ChangeOrderUpdate:
      type: object
      properties:
        co_number: { type: string, maxLength: 200 }
        description: { type: string }
        amount: { type: number }
        status: { type: string, enum: [draft, submitted, approved, rejected] }
        reason: { type: string }
        notes: { type: string }
        project_id: { type: string, format: uuid }
        vendor_id: { type: string, format: uuid }
        category_id: { type: string, format: uuid }

    ProjectDocumentFolderTemplate:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        structure:
          type: array
          items:
            type: object
          description: "Hierarchical folder structure as JSON array"
        is_default: { type: boolean }
        created_by: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ProjectDocumentFolderTemplateCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 500 }
        description: { type: string, maxLength: 2000 }
        structure:
          type: array
          items:
            type: object
        is_default: { type: boolean }

    ProjectDocumentFolderTemplateUpdate:
      type: object
      properties:
        name: { type: string, maxLength: 500 }
        description: { type: string, maxLength: 2000 }
        structure:
          type: array
          items:
            type: object
        is_default: { type: boolean }

    AssetTypeCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        group_id: { type: string, format: uuid, nullable: true }

    AssetTypeUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        group_id: { type: string, format: uuid, nullable: true }

    WorkCategoryCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    WorkCategoryUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    ManufacturerCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        website: { type: string, nullable: true }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        notes: { type: string, nullable: true }
        system_class_ids: { type: array, items: { type: string, format: uuid }, nullable: true }

    ManufacturerUpdate:
      type: object
      properties:
        name: { type: string }
        website: { type: string, nullable: true }
        contact_name: { type: string, nullable: true }
        contact_email: { type: string, nullable: true }
        contact_phone: { type: string, nullable: true }
        notes: { type: string, nullable: true }
        system_class_ids: { type: array, items: { type: string, format: uuid }, nullable: true }

    BuildingTypeCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    BuildingTypeUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    LocationTypeCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    LocationTypeUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    ProjectPhaseCategoryCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        sort_order: { type: integer, description: "Display order (lower = first)" }

    ProjectPhaseCategoryUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        sort_order: { type: integer, description: "Display order (lower = first)" }

    CostCategoryCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        parent_id: { type: string, format: uuid, nullable: true }
        is_active: { type: boolean }
        sort_order: { type: integer }

    CostCategoryUpdate:
      type: object
      properties:
        name: { type: string }
        parent_id: { type: string, format: uuid, nullable: true }
        is_active: { type: boolean }
        sort_order: { type: integer }

    BudgetCreate:
      type: object
      required: [year]
      properties:
        year: { type: integer }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        funding_source: { type: integer, nullable: true }
        allocated_amount: { type: number, nullable: true }
        budgeted_amount: { type: number, nullable: true }

    BudgetUpdate:
      type: object
      properties:
        year: { type: integer }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        funding_source: { type: integer, nullable: true }
        allocated_amount: { type: number, nullable: true }
        budgeted_amount: { type: number, nullable: true }

    AssetCommentCreate:
      type: object
      required: [asset_id, comment]
      properties:
        asset_id: { type: string, format: uuid }
        comment: { type: string }

    AssetCommentUpdate:
      type: object
      properties:
        comment: { type: string }

    AssetCostCreate:
      type: object
      required: [asset_id, amount]
      properties:
        asset_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        category: { type: string, nullable: true, enum: [Repair, PM, Operation, Replacement, Decommission, Other] }
        amount: { type: number }
        cost_date: { type: string, format: date, nullable: true }
        description: { type: string, nullable: true }
        invoice_number: { type: string, nullable: true }
        po_number: { type: string, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        work_order_number: { type: integer, nullable: true }

    AssetCostUpdate:
      type: object
      properties:
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        category: { type: string, nullable: true, enum: [Repair, PM, Operation, Replacement, Decommission, Other] }
        amount: { type: number }
        cost_date: { type: string, format: date, nullable: true }
        description: { type: string, nullable: true }
        invoice_number: { type: string, nullable: true }
        po_number: { type: string, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        work_order_number: { type: integer, nullable: true }

    AssetReplacementPlanCreate:
      type: object
      description: "Exactly one of asset_id or infrastructure_asset_id must be provided."
      properties:
        asset_id: { type: string, format: uuid, nullable: true, description: "Required unless infrastructure_asset_id is set" }
        infrastructure_asset_id: { type: string, format: uuid, nullable: true, description: "Infrastructure feature target (mutually exclusive with asset_id)" }
        planned_replacement_year: { type: integer, nullable: true }
        planned_replacement_date: { type: string, format: date, nullable: true }
        estimated_cost: { type: number, nullable: true }
        funding_source: { type: string, nullable: true }
        priority: { type: string, nullable: true, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
        status: { type: string, nullable: true, enum: [PLANNED, BUDGETED, APPROVED, COMPLETED, CANCELLED] }
        notes: { type: string, nullable: true }

    AssetReplacementPlanUpdate:
      type: object
      properties:
        planned_replacement_year: { type: integer, nullable: true }
        planned_replacement_date: { type: string, format: date, nullable: true }
        estimated_cost: { type: number, nullable: true }
        funding_source: { type: string, nullable: true }
        priority: { type: string, nullable: true, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
        status: { type: string, nullable: true, enum: [PLANNED, BUDGETED, APPROVED, COMPLETED, CANCELLED] }
        notes: { type: string, nullable: true }

    WorkOrderCommentCreate:
      type: object
      required: [work_order_id, comment]
      properties:
        work_order_id: { type: string, format: uuid }
        comment: { type: string }

    WorkOrderCommentUpdate:
      type: object
      properties:
        comment: { type: string }

    WorkOrderScheduleCreate:
      type: object
      required: [work_order_id, technician_id, scheduled_date]
      properties:
        work_order_id: { type: string, format: uuid, description: "Must reference a work order in the key's tenant" }
        technician_id: { type: string, description: "Clerk user ID" }
        scheduled_date: { type: string, format: date }
        scheduled_start_time: { type: string, nullable: true }
        scheduled_end_time: { type: string, nullable: true }
        duration_minutes: { type: integer, nullable: true }
        break_time_minutes: { type: integer, nullable: true }
        travel_time_minutes: { type: integer, nullable: true }
        stop_order: { type: integer, nullable: true }
        scheduling_notes: { type: string, nullable: true }

    WorkOrderScheduleUpdate:
      type: object
      description: work_order_id is immutable.
      properties:
        technician_id: { type: string, description: "Clerk user ID" }
        scheduled_date: { type: string, format: date }
        scheduled_start_time: { type: string, nullable: true }
        scheduled_end_time: { type: string, nullable: true }
        duration_minutes: { type: integer, nullable: true }
        break_time_minutes: { type: integer, nullable: true }
        travel_time_minutes: { type: integer, nullable: true }
        stop_order: { type: integer, nullable: true }
        scheduling_notes: { type: string, nullable: true }

    ProjectTaskCreate:
      type: object
      required: [project_id, title]
      properties:
        project_id: { type: string, format: uuid }
        phase_id: { type: string, format: uuid, nullable: true }
        parent_task_id: { type: string, format: uuid, nullable: true }
        title: { type: string }
        description: { type: string, nullable: true }
        assigned_to: { type: string, nullable: true, description: "Clerk user ID" }
        due_date: { type: string, format: date, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [todo, in_progress, completed, blocked, cancelled] }
        priority: { type: string, nullable: true, enum: [low, medium, high, critical] }
        estimated_hours: { type: number, nullable: true }
        estimated_cost: { type: number, nullable: true }
        sequence_order: { type: integer, nullable: true }

    ProjectTaskUpdate:
      type: object
      properties:
        phase_id: { type: string, format: uuid, nullable: true }
        parent_task_id: { type: string, format: uuid, nullable: true }
        title: { type: string }
        description: { type: string, nullable: true }
        assigned_to: { type: string, nullable: true, description: "Clerk user ID" }
        due_date: { type: string, format: date, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [todo, in_progress, completed, blocked, cancelled] }
        priority: { type: string, nullable: true, enum: [low, medium, high, critical] }
        estimated_hours: { type: number, nullable: true }
        actual_hours: { type: number, nullable: true }
        completion_percentage: { type: integer, nullable: true }
        estimated_cost: { type: number, nullable: true }
        actual_cost: { type: number, nullable: true }
        sequence_order: { type: integer, nullable: true }

    ProjectMilestoneCreate:
      type: object
      required: [project_id, name]
      properties:
        project_id: { type: string, format: uuid }
        phase_id: { type: string, format: uuid, nullable: true }
        name: { type: string }
        description: { type: string, nullable: true }
        due_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [pending, completed, missed, at_risk] }
        is_critical: { type: boolean, nullable: true }

    ProjectMilestoneUpdate:
      type: object
      properties:
        phase_id: { type: string, format: uuid, nullable: true }
        name: { type: string }
        description: { type: string, nullable: true }
        due_date: { type: string, format: date, nullable: true }
        completed_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [pending, completed, missed, at_risk] }
        is_critical: { type: boolean, nullable: true }

    ProjectRiskCreate:
      type: object
      required: [project_id, title]
      properties:
        project_id: { type: string, format: uuid }
        title: { type: string }
        description: { type: string, nullable: true }
        category: { type: string, nullable: true, enum: [technical, financial, schedule, resource, external] }
        probability: { type: string, nullable: true, enum: [low, medium, high] }
        impact: { type: string, nullable: true, enum: [low, medium, high, critical] }
        status: { type: string, nullable: true, enum: [identified, analyzing, mitigating, resolved, accepted] }
        mitigation_plan: { type: string, nullable: true }
        contingency_plan: { type: string, nullable: true }
        owner_id: { type: string, nullable: true, description: "Clerk user ID" }
        due_date: { type: string, format: date, nullable: true }
        created_by: { type: string, nullable: true, description: "Clerk user ID" }

    ProjectRiskUpdate:
      type: object
      properties:
        title: { type: string }
        description: { type: string, nullable: true }
        category: { type: string, nullable: true, enum: [technical, financial, schedule, resource, external] }
        probability: { type: string, nullable: true, enum: [low, medium, high] }
        impact: { type: string, nullable: true, enum: [low, medium, high, critical] }
        status: { type: string, nullable: true, enum: [identified, analyzing, mitigating, resolved, accepted] }
        mitigation_plan: { type: string, nullable: true }
        contingency_plan: { type: string, nullable: true }
        owner_id: { type: string, nullable: true, description: "Clerk user ID" }
        due_date: { type: string, format: date, nullable: true }

    ProjectPhaseCreate:
      type: object
      required: [project_id, name]
      properties:
        project_id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        sequence_order: { type: integer, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [pending, in_progress, completed, skipped] }

    ProjectPhaseUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        sequence_order: { type: integer, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        status: { type: string, nullable: true, enum: [pending, in_progress, completed, skipped] }
        completion_percentage: { type: integer, nullable: true }

    ProjectBudgetItemCreate:
      type: object
      required: [project_id]
      properties:
        project_id: { type: string, format: uuid }
        category: { type: string, nullable: true, enum: [labor, materials, equipment, subcontractors, permits, contingency, other] }
        description: { type: string, nullable: true }
        planned_amount: { type: number, nullable: true }
        actual_amount: { type: number, nullable: true }
        notes: { type: string, nullable: true }

    ProjectBudgetItemUpdate:
      type: object
      properties:
        category: { type: string, nullable: true, enum: [labor, materials, equipment, subcontractors, permits, contingency, other] }
        description: { type: string, nullable: true }
        planned_amount: { type: number, nullable: true }
        actual_amount: { type: number, nullable: true }
        notes: { type: string, nullable: true }

    ProjectTimeEntryCreate:
      type: object
      # task_id, user_id and start_time are NOT NULL on the table and required by the
      # handler; the spec listed only project_id, so an integrator following it got a 400.
      required: [project_id, task_id, user_id, start_time]
      properties:
        project_id: { type: string, format: uuid }
        task_id: { type: string, format: uuid }
        user_id: { type: string, description: "Clerk user ID" }
        user_name: { type: string, nullable: true }
        start_time: { type: string, format: date-time }
        end_time: { type: string, format: date-time, nullable: true }
        duration_minutes: { type: integer, nullable: true }
        description: { type: string, nullable: true }
        is_billable: { type: boolean, nullable: true }

    ProjectTimeEntryUpdate:
      type: object
      properties:
        project_id: { type: string, format: uuid }
        task_id: { type: string, format: uuid }
        user_id: { type: string, description: "Clerk user ID" }
        user_name: { type: string, nullable: true }
        start_time: { type: string, format: date-time }
        end_time: { type: string, format: date-time, nullable: true }
        duration_minutes: { type: integer, nullable: true }
        description: { type: string, nullable: true }
        is_billable: { type: boolean, nullable: true }

    ProjectCommentCreate:
      type: object
      required: [project_id, content]
      properties:
        project_id: { type: string, format: uuid }
        parent_id: { type: string, format: uuid, nullable: true }
        content: { type: string }

    ProjectCommentUpdate:
      type: object
      properties:
        content: { type: string }

    ProjectTeamMemberCreate:
      type: object
      required: [project_id, user_id, role]
      properties:
        project_id: { type: string, format: uuid }
        user_id: { type: string, description: "Clerk user ID" }
        role: { type: string, maxLength: 100 }
        responsibilities: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        is_active: { type: boolean, nullable: true }

    ProjectTeamMemberUpdate:
      type: object
      properties:
        user_id: { type: string, description: "Clerk user ID" }
        role: { type: string, maxLength: 100 }
        responsibilities: { type: string, nullable: true }
        start_date: { type: string, format: date, nullable: true }
        end_date: { type: string, format: date, nullable: true }
        is_active: { type: boolean, nullable: true }

    ProjectTaskDependencyCreate:
      type: object
      required: [task_id, depends_on_task_id]
      properties:
        task_id: { type: string, format: uuid }
        depends_on_task_id: { type: string, format: uuid }
        dependency_type: { type: string, nullable: true, enum: [finish_to_start, start_to_start, finish_to_finish, start_to_finish] }

    ProjectUpdateCreate:
      type: object
      required: [project_id, author_id, timeframe, period_year, period_value, content]
      properties:
        project_id: { type: string, format: uuid }
        author_id: { type: string, description: "Clerk user ID" }
        timeframe: { type: string, enum: [monthly, quarterly, bi-annually, annually] }
        period_year: { type: integer }
        period_value: { type: string }
        title: { type: string, nullable: true }
        content: { type: string }

    ProjectUpdateUpdate:
      type: object
      properties:
        timeframe: { type: string, enum: [monthly, quarterly, bi-annually, annually] }
        period_year: { type: integer }
        period_value: { type: string }
        title: { type: string, nullable: true }
        content: { type: string }

    ProjectCostSnapshotCreate:
      type: object
      required: [project_id, snapshot_date, total_budget, actual_cost]
      properties:
        project_id: { type: string, format: uuid }
        snapshot_date: { type: string, format: date }
        total_budget: { type: number }
        actual_cost: { type: number }
        forecasted_cost: { type: number, nullable: true }
        percent_complete: { type: number, nullable: true }

    ProjectLocationCreate:
      type: object
      required: [project_id, location_id]
      properties:
        project_id: { type: string, format: uuid }
        location_id: { type: string, format: uuid }

    CustomFieldDefinitionCreate:
      type: object
      required: [entity_type, field_name, field_label, field_type]
      properties:
        entity_type: { type: string }
        field_name: { type: string }
        field_label: { type: string }
        field_type: { type: string, enum: [text, number, date, boolean, select] }
        field_options: { type: object, nullable: true }
        is_required: { type: boolean, nullable: true }
        display_order: { type: integer, nullable: true }

    CustomFieldDefinitionUpdate:
      type: object
      properties:
        field_label: { type: string }
        field_options: { type: object, nullable: true }
        is_required: { type: boolean, nullable: true }
        display_order: { type: integer, nullable: true }

    CustomFieldValueCreate:
      type: object
      required: [entity_id, field_definition_id]
      properties:
        entity_id: { type: string, format: uuid }
        field_definition_id: { type: string, format: uuid }
        value_text: { type: string, nullable: true }
        value_number: { type: number, nullable: true }
        value_date: { type: string, format: date, nullable: true }
        value_boolean: { type: boolean, nullable: true }

    CustomFieldValueUpdate:
      type: object
      properties:
        value_text: { type: string, nullable: true }
        value_number: { type: number, nullable: true }
        value_date: { type: string, format: date, nullable: true }
        value_boolean: { type: boolean, nullable: true }

    PartCreate:
      type: object
      required: [name, quantity, desired_quantity]
      properties:
        name: { type: string }
        part_number: { type: string, nullable: true }
        category: { type: string, nullable: true }
        supplier: { type: string, nullable: true, description: "DEPRECATED — legacy free-text supplier name. Use supplier_id." }
        supplier_id: { type: string, format: uuid, nullable: true }
        cost: { type: number, nullable: true }
        quantity: { type: integer }
        desired_quantity: { type: integer }
        specific_location: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }

    PartUpdate:
      type: object
      properties:
        name: { type: string }
        part_number: { type: string, nullable: true }
        category: { type: string, nullable: true }
        supplier: { type: string, nullable: true, description: "DEPRECATED — legacy free-text supplier name. Use supplier_id." }
        supplier_id: { type: string, format: uuid, nullable: true }
        cost: { type: number, nullable: true }
        quantity: { type: integer }
        desired_quantity: { type: integer }
        specific_location: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }

    PartCategoryCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    PartCategoryUpdate:
      type: object
      properties:
        name: { type: string }
        description: { type: string, nullable: true }

    # ── Upload URL ──

    UploadUrlRequest:
      type: object
      required: [bucket, file_name]
      properties:
        bucket: { type: string, enum: [documents, attachments, project-documents, contract-documents, asset-images] }
        file_name: { type: string }

    UploadUrlResponse:
      type: object
      properties:
        signed_url: { type: string }
        path: { type: string }
        token: { type: string }
        bucket: { type: string }
        expires_in: { type: integer }

    UploadFileRequest:
      type: object
      required: [bucket, file_name, content_base64]
      properties:
        bucket: { type: string, enum: [documents, attachments, project-documents, contract-documents, asset-images] }
        file_name: { type: string, description: "File name including extension" }
        content_base64: { type: string, description: "File bytes base64-encoded. Data URI prefixes (e.g. 'data:image/png;base64,') are stripped automatically." }
        content_type: { type: string, description: "MIME type (e.g. image/jpeg, application/pdf). Defaults to application/octet-stream.", nullable: true }

    UploadFileResponse:
      type: object
      properties:
        path: { type: string }
        bucket: { type: string }
        file_size: { type: integer, description: "Decoded file size in bytes" }
        content_type: { type: string }

    # ── Asset Documents ──

    AssetDocument:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        file_path: { type: string }
        file_type: { type: string, nullable: true }
        file_size: { type: integer, nullable: true }
        asset_id: { type: string, format: uuid }
        category: { type: string, nullable: true, enum: [om, commissioning, warranty, installation, specification, other] }
        user_id: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AssetDocumentCreate:
      type: object
      required: [name, file_path, asset_id]
      properties:
        name: { type: string }
        file_path: { type: string }
        asset_id: { type: string, format: uuid }
        category: { type: string, nullable: true, enum: [om, commissioning, warranty, installation, specification, other] }
        description: { type: string, nullable: true }
        file_type: { type: string, nullable: true }
        file_size: { type: integer, nullable: true }
        user_id: { type: string, nullable: true }

    AssetDocumentUpdate:
      type: object
      properties:
        name: { type: string }
        file_path: { type: string }
        asset_id: { type: string, format: uuid }
        category: { type: string, nullable: true, enum: [om, commissioning, warranty, installation, specification, other] }
        description: { type: string, nullable: true }
        file_type: { type: string, nullable: true }
        file_size: { type: integer, nullable: true }
        user_id: { type: string, nullable: true }

    # ── Attachments ──

    Attachment:
      type: object
      properties:
        id: { type: string, format: uuid }
        file_url: { type: string }
        file_name: { type: string }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }
        uploaded_at: { type: string, format: date-time }
        work_request_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        pm_schedule_id: { type: string, format: uuid, nullable: true }
        pm_template_id: { type: string, format: uuid, nullable: true }
        description: { type: string, nullable: true }

    AttachmentCreate:
      type: object
      required: [file_url, file_name]
      properties:
        file_url: { type: string }
        file_name: { type: string }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }
        description: { type: string, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        work_request_id: { type: string, format: uuid, nullable: true }
        pm_schedule_id: { type: string, format: uuid, nullable: true }
        pm_template_id: { type: string, format: uuid, nullable: true }

    AttachmentUpdate:
      type: object
      properties:
        file_url: { type: string }
        file_name: { type: string }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }
        description: { type: string, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        work_request_id: { type: string, format: uuid, nullable: true }
        pm_schedule_id: { type: string, format: uuid, nullable: true }
        pm_template_id: { type: string, format: uuid, nullable: true }

    # ── Project Documents ──

    ProjectDocument:
      type: object
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        folder_id: { type: string, format: uuid, nullable: true }
        name: { type: string }
        description: { type: string, nullable: true }
        file_path: { type: string }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ProjectDocumentCreate:
      type: object
      required: [project_id, name, file_path, uploaded_by]
      properties:
        project_id: { type: string, format: uuid }
        name: { type: string }
        file_path: { type: string }
        uploaded_by: { type: string }
        folder_id: { type: string, format: uuid, nullable: true }
        description: { type: string, nullable: true }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }

    ProjectDocumentUpdate:
      type: object
      properties:
        project_id: { type: string, format: uuid }
        name: { type: string }
        file_path: { type: string }
        uploaded_by: { type: string }
        folder_id: { type: string, format: uuid, nullable: true }
        description: { type: string, nullable: true }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }

    # ── Contract Documents ──

    ContractDocument:
      type: object
      properties:
        id: { type: string, format: uuid }
        contract_id: { type: string, format: uuid }
        file_name: { type: string }
        file_path: { type: string }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }
        uploaded_at: { type: string, format: date-time }

    ContractDocumentCreate:
      type: object
      required: [contract_id, file_name, file_path]
      properties:
        contract_id: { type: string, format: uuid }
        file_name: { type: string }
        file_path: { type: string }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }

    ContractDocumentUpdate:
      type: object
      properties:
        contract_id: { type: string, format: uuid }
        file_name: { type: string }
        file_path: { type: string }
        file_size: { type: integer, nullable: true }
        file_type: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }

    # ── Level of Service ──

    ServiceArea:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        icon: { type: string, nullable: true, description: "Icon name (e.g. droplets)" }
        color: { type: string, nullable: true, description: "Hex color code" }
        sort_order: { type: integer, nullable: true }
        is_active: { type: boolean }
        created_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ServiceAreaCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, maxLength: 500 }
        description: { type: string, maxLength: 2000, nullable: true }
        icon: { type: string, maxLength: 200, nullable: true }
        color: { type: string, maxLength: 50, nullable: true }
        sort_order: { type: integer, minimum: 0, nullable: true }
        is_active: { type: boolean }

    ServiceAreaUpdate:
      type: object
      properties:
        name: { type: string, maxLength: 500 }
        description: { type: string, maxLength: 2000, nullable: true }
        icon: { type: string, maxLength: 200, nullable: true }
        color: { type: string, maxLength: 50, nullable: true }
        sort_order: { type: integer, minimum: 0, nullable: true }
        is_active: { type: boolean }

    ServiceAreaSystemClass:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        service_area_id: { type: string, format: uuid }
        system_class_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }

    ServiceAreaSystemClassCreate:
      type: object
      required: [service_area_id, system_class_id]
      properties:
        service_area_id: { type: string, format: uuid }
        system_class_id: { type: string, format: uuid }

    ServiceAreaSite:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        service_area_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        created_at: { type: string, format: date-time }

    ServiceAreaSiteCreate:
      type: object
      required: [service_area_id, site_id]
      properties:
        service_area_id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }

    LosMeasure:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        service_area_id: { type: string, format: uuid }
        category: { type: string, enum: [quality, reliability, responsiveness, safety, sustainability, cost_efficiency, capacity] }
        type: { type: string, enum: [community, technical] }
        name: { type: string }
        description: { type: string, nullable: true }
        community_statement: { type: string, nullable: true }
        unit: { type: string, nullable: true }
        data_source: { type: string, enum: [manual, custom_formula, asset_condition_avg, asset_condition_pct_above, asset_condition_pct_below, risk_score_avg, risk_pct_critical, wo_response_time_avg, wo_completion_time_avg, wo_backlog_count, wo_overdue_count, pm_compliance_rate, compliance_score, fci, deferred_maintenance_ratio, asset_past_useful_life_pct] }
        data_source_config: { type: object, nullable: true, description: "JSONB config for data source (e.g. threshold, days_back)" }
        trend_direction: { type: string, enum: [higher_is_better, lower_is_better, target_is_optimal], nullable: true }
        target_value: { type: number, nullable: true }
        minimum_acceptable: { type: number, nullable: true }
        stretch_goal: { type: number, nullable: true }
        weight: { type: number, nullable: true, description: "Weight for composite score (default 1.0)" }
        is_active: { type: boolean }
        sort_order: { type: integer, nullable: true }
        created_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    LosMeasureCreate:
      type: object
      required: [service_area_id, name, category, type, data_source]
      properties:
        service_area_id: { type: string, format: uuid }
        name: { type: string, maxLength: 500 }
        category: { type: string, enum: [quality, reliability, responsiveness, safety, sustainability, cost_efficiency, capacity] }
        type: { type: string, enum: [community, technical] }
        data_source: { type: string, enum: [manual, custom_formula, asset_condition_avg, asset_condition_pct_above, asset_condition_pct_below, risk_score_avg, risk_pct_critical, wo_response_time_avg, wo_completion_time_avg, wo_backlog_count, wo_overdue_count, pm_compliance_rate, compliance_score, fci, deferred_maintenance_ratio, asset_past_useful_life_pct] }
        description: { type: string, maxLength: 2000, nullable: true }
        community_statement: { type: string, maxLength: 2000, nullable: true }
        unit: { type: string, maxLength: 100, nullable: true }
        data_source_config: { type: object, nullable: true }
        trend_direction: { type: string, enum: [higher_is_better, lower_is_better, target_is_optimal], nullable: true }
        target_value: { type: number, nullable: true }
        minimum_acceptable: { type: number, nullable: true }
        stretch_goal: { type: number, nullable: true }
        weight: { type: number, minimum: 0, nullable: true }
        is_active: { type: boolean }
        sort_order: { type: integer, minimum: 0, nullable: true }

    LosMeasureUpdate:
      type: object
      properties:
        name: { type: string, maxLength: 500 }
        category: { type: string, enum: [quality, reliability, responsiveness, safety, sustainability, cost_efficiency, capacity] }
        type: { type: string, enum: [community, technical] }
        data_source: { type: string, enum: [manual, custom_formula, asset_condition_avg, asset_condition_pct_above, asset_condition_pct_below, risk_score_avg, risk_pct_critical, wo_response_time_avg, wo_completion_time_avg, wo_backlog_count, wo_overdue_count, pm_compliance_rate, compliance_score, fci, deferred_maintenance_ratio, asset_past_useful_life_pct] }
        description: { type: string, maxLength: 2000, nullable: true }
        community_statement: { type: string, maxLength: 2000, nullable: true }
        unit: { type: string, maxLength: 100, nullable: true }
        data_source_config: { type: object, nullable: true }
        trend_direction: { type: string, enum: [higher_is_better, lower_is_better, target_is_optimal], nullable: true }
        target_value: { type: number, nullable: true }
        minimum_acceptable: { type: number, nullable: true }
        stretch_goal: { type: number, nullable: true }
        weight: { type: number, minimum: 0, nullable: true }
        is_active: { type: boolean }
        sort_order: { type: integer, minimum: 0, nullable: true }

    LosMeasurement:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        los_measure_id: { type: string, format: uuid }
        period_type: { type: string, enum: [monthly, quarterly, semi_annual, annual] }
        period_start: { type: string, format: date }
        period_end: { type: string, format: date }
        actual_value: { type: number }
        notes: { type: string, nullable: true }
        is_auto: { type: boolean, description: "true if auto-calculated, false if manually entered" }
        source_snapshot: { type: object, nullable: true, description: "Calculation metadata for auto values" }
        recorded_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    LosMeasurementCreate:
      type: object
      required: [los_measure_id, period_type, period_start, period_end, actual_value]
      properties:
        los_measure_id: { type: string, format: uuid }
        period_type: { type: string, enum: [monthly, quarterly, semi_annual, annual] }
        period_start: { type: string, format: date }
        period_end: { type: string, format: date }
        actual_value: { type: number }
        notes: { type: string, maxLength: 2000, nullable: true }
        is_auto: { type: boolean }

    LosMeasurementUpdate:
      type: object
      properties:
        actual_value: { type: number }
        notes: { type: string, maxLength: 2000, nullable: true }
        period_type: { type: string, enum: [monthly, quarterly, semi_annual, annual] }
        period_start: { type: string, format: date }
        period_end: { type: string, format: date }
        is_auto: { type: boolean }

    LosTargetsHistory:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        los_measure_id: { type: string, format: uuid }
        target_value: { type: number, nullable: true }
        minimum_acceptable: { type: number, nullable: true }
        stretch_goal: { type: number, nullable: true }
        effective_date: { type: string, format: date }
        reason: { type: string, nullable: true }
        changed_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    Floorplan:
      type: object
      description: |
        One floor (or sheet) sourced from one page of an uploaded PDF. Each floorplan
        belongs to **exactly one** of `building_id` (per-building floor) or `site_id`
        (site-level / campus plan).
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        building_id: { type: string, format: uuid, nullable: true, description: "Set for building-scoped floorplans; null for site-scoped" }
        site_id: { type: string, format: uuid, nullable: true, description: "Set for site-scoped floorplans (campus / outdoor); null for building-scoped" }
        floor_label: { type: string }
        floor_order: { type: integer }
        pdf_storage_path: { type: string, description: "Supabase Storage path to the PDF" }
        pdf_filename: { type: string }
        page_number: { type: integer, description: "1-indexed page within the PDF" }
        page_width_pt: { type: number, nullable: true }
        page_height_pt: { type: number, nullable: true }
        rotation_deg: { type: integer, enum: [0, 90, 180, 270], description: "Display rotation, clockwise" }
        status: { type: string, enum: [pending, detecting, ready, failed] }
        detection_error: { type: string, nullable: true }
        uploaded_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    FloorplanCreate:
      type: object
      required: [floor_label, pdf_storage_path, pdf_filename]
      description: |
        Provide **exactly one** of `building_id` or `site_id`. Returns 400 if both
        or neither are supplied.
      properties:
        building_id: { type: string, format: uuid, description: "Building this floor belongs to (omit for site-scoped)" }
        site_id: { type: string, format: uuid, description: "Site this plan belongs to (use for campus / outdoor plans; omit for building-scoped)" }
        floor_label: { type: string, maxLength: 200 }
        floor_order: { type: integer, minimum: 0 }
        pdf_storage_path: { type: string, maxLength: 1000 }
        pdf_filename: { type: string, maxLength: 500 }
        page_number: { type: integer, minimum: 1 }
        page_width_pt: { type: number, minimum: 0 }
        page_height_pt: { type: number, minimum: 0 }
        status: { type: string, enum: [pending, detecting, ready, failed] }

    FloorplanUpdate:
      type: object
      properties:
        floor_label: { type: string, maxLength: 200 }
        floor_order: { type: integer, minimum: 0 }
        status: { type: string, enum: [pending, detecting, ready, failed] }
        detection_error: { type: string, maxLength: 2000, nullable: true }

    FloorplanPolygon:
      type: array
      description: "Polygon outline as an array of [x, y] points in normalized 0-1 coordinates (origin top-left). At least 3 points."
      minItems: 3
      items:
        type: array
        minItems: 2
        maxItems: 2
        items: { type: number, minimum: 0, maximum: 1 }

    FloorplanRegion:
      type: object
      description: A labeled room or zone on a floorplan, optionally linked to a Location.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        floorplan_id: { type: string, format: uuid }
        location_id: { type: string, format: uuid, nullable: true }
        label: { type: string }
        polygon: { $ref: "#/components/schemas/FloorplanPolygon" }
        bbox:
          type: object
          nullable: true
          description: "Axis-aligned bounding box in normalized 0-1 coords"
          properties:
            x: { type: number }
            y: { type: number }
            w: { type: number }
            h: { type: number }
        source: { type: string, enum: [manual, ai] }
        confidence: { type: number, minimum: 0, maximum: 1, nullable: true }
        reviewed: { type: boolean }
        created_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    FloorplanRegionCreate:
      type: object
      required: [floorplan_id, label, polygon]
      properties:
        floorplan_id: { type: string, format: uuid }
        label: { type: string, maxLength: 500 }
        polygon: { $ref: "#/components/schemas/FloorplanPolygon" }
        location_id: { type: string, format: uuid, nullable: true }
        source: { type: string, enum: [manual, ai] }
        confidence: { type: number, minimum: 0, maximum: 1 }
        reviewed: { type: boolean }

    FloorplanRegionUpdate:
      type: object
      properties:
        label: { type: string, maxLength: 500 }
        polygon: { $ref: "#/components/schemas/FloorplanPolygon" }
        location_id: { type: string, format: uuid, nullable: true }
        confidence: { type: number, minimum: 0, maximum: 1 }
        reviewed: { type: boolean }

    AssetPlacement:
      type: object
      description: A single asset pin on a floorplan. Unique per asset_id (one placement per asset globally).
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        asset_id: { type: string, format: uuid }
        floorplan_id: { type: string, format: uuid }
        region_id: { type: string, format: uuid, nullable: true }
        x: { type: number, minimum: 0, maximum: 1, description: "Normalized x (0=left, 1=right)" }
        y: { type: number, minimum: 0, maximum: 1, description: "Normalized y (0=top, 1=bottom)" }
        source: { type: string, enum: [manual, ai] }
        placed_by: { type: string, nullable: true }
        placed_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AssetPlacementCreate:
      type: object
      required: [asset_id, floorplan_id, x, y]
      properties:
        asset_id: { type: string, format: uuid }
        floorplan_id: { type: string, format: uuid }
        x: { type: number, minimum: 0, maximum: 1 }
        y: { type: number, minimum: 0, maximum: 1 }
        region_id: { type: string, format: uuid, nullable: true }
        source: { type: string, enum: [manual, ai] }

    AssetPlacementUpdate:
      type: object
      properties:
        x: { type: number, minimum: 0, maximum: 1 }
        y: { type: number, minimum: 0, maximum: 1 }
        region_id: { type: string, format: uuid, nullable: true }
        floorplan_id: { type: string, format: uuid }

    # ── Infrastructure ──

    GeoJsonPoint:
      type: object
      required: [type, coordinates]
      description: GeoJSON Point in EPSG:4326. Coordinates are [longitude, latitude].
      properties:
        type: { type: string, enum: [Point] }
        coordinates:
          type: array
          minItems: 2
          maxItems: 2
          items: { type: number }

    GeoJsonLineString:
      type: object
      required: [type, coordinates]
      description: GeoJSON LineString in EPSG:4326. Each coordinate is [longitude, latitude]; minimum 2 points.
      properties:
        type: { type: string, enum: [LineString] }
        coordinates:
          type: array
          minItems: 2
          items:
            type: array
            minItems: 2
            maxItems: 2
            items: { type: number }

    GeoJsonGeometry:
      oneOf:
        - { $ref: "#/components/schemas/GeoJsonPoint" }
        - { $ref: "#/components/schemas/GeoJsonLineString" }
      discriminator:
        propertyName: type
        mapping:
          Point: "#/components/schemas/GeoJsonPoint"
          LineString: "#/components/schemas/GeoJsonLineString"

    GeoJsonPolygon:
      type: object
      required: [type, coordinates]
      description: >
        GeoJSON Polygon in EPSG:4326. An array of linear rings; the first is the
        exterior ring, the rest are holes. Each ring is closed (first and last
        positions identical) with at least 4 positions. MultiPolygon is not supported.
      properties:
        type: { type: string, enum: [Polygon] }
        coordinates:
          type: array
          minItems: 1
          items:
            type: array
            minItems: 4
            items:
              type: array
              minItems: 2
              maxItems: 2
              items: { type: number }

    InfrastructureFeatureClass:
      type: object
      description: |
        Catalog of infrastructure feature classes (e.g. `water_main`, `sewer_gravity`, `pavement`).
        Addressed by `code`, not UUID. Builtin classes are managed by AssetLab; tenant-defined
        classes have `is_builtin: false`.
      properties:
        tenant_id: { type: string }
        code: { type: string, description: "Natural key (lowercase snake_case)" }
        name: { type: string }
        category:
          type: string
          enum: [transportation, water, wastewater, stormwater, structures, electrical, telecom, gas, roadside, other]
        description: { type: string, nullable: true }
        icon: { type: string, nullable: true }
        color: { type: string, nullable: true }
        sort_order: { type: integer }
        is_builtin: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InfrastructureFeatureClassCreate:
      type: object
      required: [code, name, category]
      description: Tenant-defined classes only. `is_builtin` is forced to false.
      properties:
        code: { type: string, pattern: "^[a-z][a-z0-9_]{0,49}$" }
        name: { type: string, maxLength: 200 }
        category:
          type: string
          enum: [transportation, water, wastewater, stormwater, structures, electrical, telecom, gas, roadside, other]
        description: { type: string, nullable: true }
        icon: { type: string, maxLength: 50, nullable: true }
        color: { type: string, maxLength: 20, nullable: true }
        sort_order: { type: integer }

    InfrastructureFeatureClassUpdate:
      type: object
      description: |
        `code`, `category`, and `is_builtin` are immutable. Attempts to update builtin
        classes return 409.
      properties:
        name: { type: string, maxLength: 200 }
        description: { type: string, nullable: true }
        icon: { type: string, maxLength: 50, nullable: true }
        color: { type: string, maxLength: 20, nullable: true }
        sort_order: { type: integer }

    InfrastructureNetwork:
      type: object
      description: Named grouping of features bound to one feature class.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        name: { type: string }
        feature_class: { type: string, description: "Asset class code (FK)" }
        description: { type: string, nullable: true }
        color_scheme: { type: string, nullable: true }
        metadata: { type: object, additionalProperties: true, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InfrastructureNetworkCreate:
      type: object
      required: [name, feature_class]
      properties:
        name: { type: string, maxLength: 200 }
        feature_class: { type: string, pattern: "^[a-z][a-z0-9_]{0,49}$" }
        description: { type: string, nullable: true }
        color_scheme: { type: string, maxLength: 50, nullable: true }
        metadata: { type: object, additionalProperties: true }

    InfrastructureNetworkUpdate:
      type: object
      properties:
        name: { type: string, maxLength: 200 }
        feature_class: { type: string, pattern: "^[a-z][a-z0-9_]{0,49}$" }
        description: { type: string, nullable: true }
        color_scheme: { type: string, maxLength: 50, nullable: true }
        metadata: { type: object, additionalProperties: true }

    InfrastructureAsset:
      type: object
      description: |
        An infrastructure feature — segment (LineString) or node (Point). Geometry is
        returned as GeoJSON in EPSG:4326. `length_m`, `slope_pct`, and `risk_score`
        are computed server-side.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        network_id: { type: string, format: uuid }
        feature_type: { type: string, enum: [segment, node] }
        geometry: { $ref: "#/components/schemas/GeoJsonGeometry" }
        length_m: { type: number, nullable: true, description: "Server-computed for segments" }
        name: { type: string, nullable: true }
        feature_code: { type: string, nullable: true, description: "Human-readable Feature ID, unique per tenant" }
        description: { type: string, nullable: true }
        external_ids: { type: object, additionalProperties: true, nullable: true }
        from_street: { type: string, nullable: true }
        to_street: { type: string, nullable: true }
        image_url: { type: string, nullable: true }
        qr_code: { type: string, nullable: true }
        from_feature_id: { type: string, format: uuid, nullable: true }
        to_feature_id: { type: string, format: uuid, nullable: true }
        flow_direction: { type: string, enum: [with_geometry, against_geometry], nullable: true }
        split_from_feature_id: { type: string, format: uuid, nullable: true }
        asset_type_id: { type: string, format: uuid, nullable: true }
        manufacturer_id: { type: string, format: uuid, nullable: true }
        system_class_id: { type: string, format: uuid, nullable: true }
        system_group_id: { type: string, format: uuid, nullable: true }
        system_id: { type: string, format: uuid, nullable: true }
        model: { type: string, nullable: true }
        serial_number: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        status_id: { type: string, nullable: true }
        condition_score: { type: number, nullable: true }
        last_maintenance_date: { type: string, format: date, nullable: true }
        risk_factor: { type: string, nullable: true }
        consequence_of_failure_score: { type: number, nullable: true }
        likelihood_of_failure_score: { type: number, nullable: true }
        risk_score: { type: number, nullable: true, description: "Server-computed" }
        safety_impact: { type: string, nullable: true }
        service_impact: { type: string, nullable: true }
        environmental_impact: { type: string, nullable: true }
        financial_impact: { type: string, nullable: true }
        regulatory_impact: { type: string, nullable: true }
        reputation_impact: { type: string, nullable: true }
        purchase_cost: { type: number, nullable: true }
        purchase_date: { type: string, format: date, nullable: true }
        install_date: { type: string, format: date, nullable: true }
        expected_lifetime_years: { type: number, nullable: true }
        salvage_value: { type: number, nullable: true }
        salvage_value_percentage: { type: number, nullable: true }
        quantity: { type: number, nullable: true }
        unit_of_measure: { type: string, nullable: true }
        unit_replacement_value: { type: number, nullable: true }
        purchase_cost_calculation_method:
          type: string
          enum: [manual, calculated_sqft, calculated_unit]
          nullable: true
        material: { type: string, nullable: true }
        diameter_mm: { type: number, nullable: true }
        width_m: { type: number, nullable: true }
        lanes: { type: integer, nullable: true }
        depth_m: { type: number, nullable: true }
        from_invert_m: { type: number, nullable: true }
        to_invert_m: { type: number, nullable: true }
        slope_pct: { type: number, nullable: true, description: "Server-computed for segments" }
        positional_accuracy_class:
          type: string
          enum: [survey_grade, mapping_grade, sketch, unknown]
          nullable: true
        data_source: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        deleted_at: { type: string, format: date-time, nullable: true }

    InfrastructureAssetCreate:
      type: object
      required: [network_id, feature_type, geometry]
      description: |
        `geometry` must match `feature_type` (segment ↔ LineString, node ↔ Point).
        Coordinates are [longitude, latitude] in EPSG:4326.
        Computed columns (`length_m`, `slope_pct`, `risk_score`) cannot be set.
      properties:
        network_id: { type: string, format: uuid }
        feature_type: { type: string, enum: [segment, node] }
        geometry: { $ref: "#/components/schemas/GeoJsonGeometry" }
        name: { type: string, maxLength: 500, nullable: true }
        feature_code: { type: string, maxLength: 100, nullable: true, description: "Human-readable Feature ID, unique per tenant" }
        description: { type: string, nullable: true }
        external_ids: { type: object, additionalProperties: true }
        from_street: { type: string, maxLength: 200, nullable: true }
        to_street: { type: string, maxLength: 200, nullable: true }
        image_url: { type: string, nullable: true }
        qr_code: { type: string, maxLength: 200, nullable: true }
        from_feature_id: { type: string, format: uuid, nullable: true }
        to_feature_id: { type: string, format: uuid, nullable: true }
        flow_direction: { type: string, enum: [with_geometry, against_geometry], nullable: true }
        asset_type_id: { type: string, format: uuid, nullable: true }
        manufacturer_id: { type: string, format: uuid, nullable: true }
        system_class_id: { type: string, format: uuid, nullable: true }
        system_group_id: { type: string, format: uuid, nullable: true }
        system_id: { type: string, format: uuid, nullable: true }
        model: { type: string, maxLength: 200, nullable: true }
        serial_number: { type: string, maxLength: 200, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        status_id: { type: string, nullable: true }
        condition_score: { type: number, minimum: 0, maximum: 100, nullable: true }
        last_maintenance_date: { type: string, format: date, nullable: true }
        risk_factor: { type: string, nullable: true }
        consequence_of_failure_score: { type: number, minimum: 0, maximum: 100, nullable: true }
        likelihood_of_failure_score: { type: number, minimum: 0, maximum: 100, nullable: true }
        safety_impact: { type: string, nullable: true }
        service_impact: { type: string, nullable: true }
        environmental_impact: { type: string, nullable: true }
        financial_impact: { type: string, nullable: true }
        regulatory_impact: { type: string, nullable: true }
        reputation_impact: { type: string, nullable: true }
        purchase_cost: { type: number, minimum: 0, nullable: true }
        purchase_date: { type: string, format: date, nullable: true }
        install_date: { type: string, format: date, nullable: true }
        expected_lifetime_years: { type: number, minimum: 0, nullable: true }
        salvage_value: { type: number, minimum: 0, nullable: true }
        salvage_value_percentage: { type: number, minimum: 0, maximum: 100, nullable: true }
        quantity: { type: number, minimum: 0, nullable: true }
        unit_of_measure: { type: string, maxLength: 50, nullable: true }
        unit_replacement_value: { type: number, minimum: 0, nullable: true }
        purchase_cost_calculation_method:
          type: string
          enum: [manual, calculated_sqft, calculated_unit]
          nullable: true
        material: { type: string, maxLength: 200, nullable: true }
        diameter_mm: { type: number, minimum: 0, nullable: true }
        width_m: { type: number, minimum: 0, nullable: true }
        lanes: { type: integer, minimum: 0, nullable: true }
        depth_m: { type: number, minimum: 0, nullable: true }
        from_invert_m: { type: number, nullable: true }
        to_invert_m: { type: number, nullable: true }
        positional_accuracy_class:
          type: string
          enum: [survey_grade, mapping_grade, sketch, unknown]
          nullable: true
        data_source: { type: string, maxLength: 200, nullable: true }

    InfrastructureAssetUpdate:
      type: object
      description: |
        Replace geometry by providing a new GeoJSON Point/LineString matching the existing
        `feature_type`. Computed columns cannot be set.
      properties:
        geometry: { $ref: "#/components/schemas/GeoJsonGeometry" }
        name: { type: string, maxLength: 500, nullable: true }
        feature_code: { type: string, maxLength: 100, nullable: true, description: "Human-readable Feature ID, unique per tenant" }
        description: { type: string, nullable: true }
        external_ids: { type: object, additionalProperties: true }
        from_street: { type: string, nullable: true }
        to_street: { type: string, nullable: true }
        image_url: { type: string, nullable: true }
        qr_code: { type: string, nullable: true }
        from_feature_id: { type: string, format: uuid, nullable: true }
        to_feature_id: { type: string, format: uuid, nullable: true }
        flow_direction: { type: string, enum: [with_geometry, against_geometry], nullable: true }
        asset_type_id: { type: string, format: uuid, nullable: true }
        manufacturer_id: { type: string, format: uuid, nullable: true }
        system_class_id: { type: string, format: uuid, nullable: true }
        system_group_id: { type: string, format: uuid, nullable: true }
        system_id: { type: string, format: uuid, nullable: true }
        model: { type: string, nullable: true }
        serial_number: { type: string, nullable: true }
        site_id: { type: string, format: uuid, nullable: true }
        building_id: { type: string, format: uuid, nullable: true }
        location_id: { type: string, format: uuid, nullable: true }
        status_id: { type: string, nullable: true }
        condition_score: { type: number, minimum: 0, maximum: 100, nullable: true }
        last_maintenance_date: { type: string, format: date, nullable: true }
        risk_factor: { type: string, nullable: true }
        consequence_of_failure_score: { type: number, nullable: true }
        likelihood_of_failure_score: { type: number, nullable: true }
        safety_impact: { type: string, nullable: true }
        service_impact: { type: string, nullable: true }
        environmental_impact: { type: string, nullable: true }
        financial_impact: { type: string, nullable: true }
        regulatory_impact: { type: string, nullable: true }
        reputation_impact: { type: string, nullable: true }
        purchase_cost: { type: number, nullable: true }
        purchase_date: { type: string, format: date, nullable: true }
        install_date: { type: string, format: date, nullable: true }
        expected_lifetime_years: { type: number, nullable: true }
        salvage_value: { type: number, nullable: true }
        salvage_value_percentage: { type: number, nullable: true }
        quantity: { type: number, nullable: true }
        unit_of_measure: { type: string, nullable: true }
        unit_replacement_value: { type: number, nullable: true }
        purchase_cost_calculation_method:
          type: string
          enum: [manual, calculated_sqft, calculated_unit]
          nullable: true
        material: { type: string, nullable: true }
        diameter_mm: { type: number, nullable: true }
        width_m: { type: number, nullable: true }
        lanes: { type: integer, nullable: true }
        depth_m: { type: number, nullable: true }
        from_invert_m: { type: number, nullable: true }
        to_invert_m: { type: number, nullable: true }
        positional_accuracy_class:
          type: string
          enum: [survey_grade, mapping_grade, sketch, unknown]
          nullable: true
        data_source: { type: string, nullable: true }

    InfrastructureAssetInspection:
      type: object
      description: A point-in-time condition observation against an infrastructure feature.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        feature_id: { type: string, format: uuid }
        inspection_date: { type: string, format: date }
        inspector_id: { type: string, nullable: true }
        method: { type: string, nullable: true }
        condition_score: { type: number, nullable: true }
        defects: { type: object, additionalProperties: true, nullable: true }
        notes: { type: string, nullable: true }
        attachments:
          type: array
          items: { type: string }
          nullable: true
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        deleted_at: { type: string, format: date-time, nullable: true }

    InfrastructureAssetInspectionCreate:
      type: object
      required: [feature_id, inspection_date]
      properties:
        feature_id: { type: string, format: uuid }
        inspection_date: { type: string, format: date }
        inspector_id: { type: string, nullable: true }
        method: { type: string, maxLength: 100, nullable: true }
        condition_score: { type: number, minimum: 0, maximum: 100, nullable: true }
        defects: { type: object, additionalProperties: true }
        notes: { type: string, nullable: true }
        attachments:
          type: array
          items: { type: string }

    InfrastructureAssetInspectionUpdate:
      type: object
      properties:
        inspection_date: { type: string, format: date }
        inspector_id: { type: string, nullable: true }
        method: { type: string, maxLength: 100, nullable: true }
        condition_score: { type: number, minimum: 0, maximum: 100, nullable: true }
        defects: { type: object, additionalProperties: true }
        notes: { type: string, nullable: true }
        attachments:
          type: array
          items: { type: string }

    AssetConditionAssessment:
      type: object
      description: A point-in-time condition assessment recorded against an asset.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string }
        asset_id: { type: string, format: uuid }
        assessed_on: { type: string, format: date }
        condition_score: { type: integer, minimum: 0, maximum: 100, nullable: true }
        replacement_cost: { type: number, nullable: true, description: Current replacement value (CRV) at assessment time }
        assessor_id: { type: string, nullable: true }
        method: { type: string, enum: [visual, detailed, vendor], nullable: true }
        notes: { type: string, nullable: true }
        defects: { type: object, additionalProperties: true, nullable: true }
        update_purchase_cost: { type: boolean, description: Whether this assessment overwrote the asset's purchase_cost with the CRV (create-time only) }
        previous_purchase_cost: { type: number, nullable: true, description: "Read-only: the asset's purchase_cost before this assessment overwrote it" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        deleted_at: { type: string, format: date-time, nullable: true }

    AssetConditionAssessmentCreate:
      type: object
      required: [asset_id, assessed_on]
      properties:
        asset_id: { type: string, format: uuid }
        assessed_on: { type: string, format: date }
        condition_score: { type: integer, minimum: 0, maximum: 100, nullable: true }
        replacement_cost: { type: number, minimum: 0, nullable: true }
        assessor_id: { type: string, nullable: true }
        method: { type: string, enum: [visual, detailed, vendor], nullable: true }
        notes: { type: string, nullable: true }
        defects: { type: object, additionalProperties: true }
        update_purchase_cost:
          type: boolean
          description: When true, overwrites the asset's purchase_cost with replacement_cost (CRV); the prior value is preserved on this assessment as previous_purchase_cost. Honored on create only.

    AssetConditionAssessmentUpdate:
      type: object
      properties:
        asset_id: { type: string, format: uuid }
        assessed_on: { type: string, format: date }
        condition_score: { type: integer, minimum: 0, maximum: 100, nullable: true }
        replacement_cost: { type: number, minimum: 0, nullable: true }
        assessor_id: { type: string, nullable: true }
        method: { type: string, enum: [visual, detailed, vendor], nullable: true }
        notes: { type: string, nullable: true }
        defects: { type: object, additionalProperties: true }

    # ── Phase 4.5b — Infrastructure attached records ──

    InfrastructureAssetCost:
      type: object
      description: A cost row attached to an infrastructure feature.
      properties:
        id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        category: { type: string, enum: [Repair, PM, Operation, Replacement, Decommission, Other] }
        amount: { type: number }
        cost_date: { type: string, format: date }
        invoice_number: { type: string, nullable: true }
        po_number: { type: string, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        work_order_number: { type: integer, nullable: true, description: "Server-stamped from work_order_id; read-only." }
        created_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InfrastructureAssetCostCreate:
      type: object
      required: [feature_id, category, amount, cost_date]
      properties:
        feature_id: { type: string, format: uuid }
        category: { type: string, enum: [Repair, PM, Operation, Replacement, Decommission, Other] }
        amount: { type: number, minimum: 0 }
        cost_date: { type: string, format: date }
        invoice_number: { type: string, maxLength: 200, nullable: true }
        po_number: { type: string, maxLength: 200, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }

    InfrastructureAssetCostUpdate:
      type: object
      properties:
        category: { type: string, enum: [Repair, PM, Operation, Replacement, Decommission, Other] }
        amount: { type: number, minimum: 0 }
        cost_date: { type: string, format: date }
        invoice_number: { type: string, maxLength: 200, nullable: true }
        po_number: { type: string, maxLength: 200, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }

    InfrastructureAssetPart:
      type: object
      description: A part associated with an infrastructure feature.
      properties:
        id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        part_id: { type: string, format: uuid }
        quantity: { type: number }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InfrastructureAssetPartCreate:
      type: object
      required: [feature_id, part_id]
      properties:
        feature_id: { type: string, format: uuid }
        part_id: { type: string, format: uuid }
        quantity: { type: number, minimum: 0, description: "Design/installed quantity (default 1)" }

    InfrastructureAssetPartUpdate:
      type: object
      properties:
        feature_id: { type: string, format: uuid }
        part_id: { type: string, format: uuid }
        quantity: { type: number, minimum: 0 }

    InfrastructureAssetDocument:
      type: object
      description: Document metadata attached to an infrastructure feature.
      properties:
        id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string, nullable: true }
        file_path: { type: string }
        file_type: { type: string, nullable: true }
        file_size: { type: integer, nullable: true }
        category: { type: string, nullable: true }
        user_id: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InfrastructureAssetDocumentCreate:
      type: object
      required: [feature_id, name, file_path]
      properties:
        feature_id: { type: string, format: uuid }
        name: { type: string, maxLength: 500 }
        file_path: { type: string, maxLength: 2000, description: "Storage path from the upload-url endpoint." }
        description: { type: string, nullable: true }
        file_type: { type: string, maxLength: 200, nullable: true }
        file_size: { type: integer, minimum: 0, nullable: true }
        category: { type: string, maxLength: 100, nullable: true }

    InfrastructureAssetDocumentUpdate:
      type: object
      properties:
        feature_id: { type: string, format: uuid }
        name: { type: string, maxLength: 500 }
        file_path: { type: string, maxLength: 2000 }
        description: { type: string, nullable: true }
        file_type: { type: string, maxLength: 200, nullable: true }
        file_size: { type: integer, minimum: 0, nullable: true }
        category: { type: string, maxLength: 100, nullable: true }

    InfrastructureAssetComment:
      type: object
      description: A comment on an infrastructure feature. user_id is assigned from the API key context, never client input.
      properties:
        id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        user_id: { type: string, description: "Author, attributed to the API key (e.g. api_key:<id>)." }
        comment: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InfrastructureAssetCommentCreate:
      type: object
      required: [feature_id, comment]
      properties:
        feature_id: { type: string, format: uuid }
        comment: { type: string, maxLength: 10000 }

    InfrastructureAssetCommentUpdate:
      type: object
      properties:
        comment: { type: string, maxLength: 10000 }

    InfrastructureZone:
      type: object
      description: An operational hydraulic boundary attached to an infrastructure network. Boundary is rendered as a GeoJSON Polygon.
      properties:
        id: { type: string, format: uuid }
        network_id: { type: string, format: uuid }
        kind: { type: string, enum: [pressure_zone, dma, sewershed, storm_catchment, maintenance_district] }
        name: { type: string }
        code: { type: string, nullable: true }
        boundary: { $ref: "#/components/schemas/GeoJsonPolygon" }
        notes: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    InfrastructureZoneCreate:
      type: object
      required: [network_id, kind, name, boundary]
      properties:
        network_id: { type: string, format: uuid }
        kind: { type: string, enum: [pressure_zone, dma, sewershed, storm_catchment, maintenance_district] }
        name: { type: string, maxLength: 200 }
        boundary: { $ref: "#/components/schemas/GeoJsonPolygon" }
        code: { type: string, maxLength: 50, nullable: true }
        notes: { type: string, nullable: true }

    InfrastructureZoneUpdate:
      type: object
      properties:
        network_id: { type: string, format: uuid }
        kind: { type: string, enum: [pressure_zone, dma, sewershed, storm_catchment, maintenance_district] }
        name: { type: string, maxLength: 200 }
        boundary: { $ref: "#/components/schemas/GeoJsonPolygon" }
        code: { type: string, maxLength: 50, nullable: true }
        notes: { type: string, nullable: true }

    ProjectInfrastructureAsset:
      type: object
      description: A link between a project and an infrastructure feature.
      properties:
        id: { type: string, format: uuid }
        project_id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        notes: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ProjectInfrastructureAssetCreate:
      type: object
      required: [project_id, feature_id]
      properties:
        project_id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        notes: { type: string, nullable: true }

    ProjectInfrastructureAssetUpdate:
      type: object
      properties:
        project_id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        notes: { type: string, nullable: true }

    InfrastructureAssetRiskHistoryEntry:
      type: object
      description: A trigger-populated risk + condition snapshot for an infrastructure feature. Read-only.
      properties:
        id: { type: string, format: uuid }
        feature_id: { type: string, format: uuid }
        captured_at: { type: string, format: date-time }
        consequence_of_failure_score: { type: integer, nullable: true }
        likelihood_of_failure_score: { type: integer, nullable: true }
        risk_score: { type: integer, nullable: true }
        condition_score: { type: integer, nullable: true }
        risk_factor: { type: string, nullable: true }
        source: { type: string, nullable: true }
        notes: { type: string, nullable: true }

    User:
      type: object
      description: Organization member (read-only, sourced from identity provider)
      properties:
        id: { type: string, description: "User ID" }
        first_name: { type: string }
        last_name: { type: string }
        email: { type: string, format: email }
        role: { type: string, description: "Organization role (e.g. org:org_administrator, org:org_manager)" }
        created_at: { type: integer, description: "Membership creation timestamp (epoch ms)" }

    # ── Webhook delivery envelope ──

    WebhookDeliveryPayload:
      type: object
      properties:
        event: { type: string, description: "Event name, e.g. work_order.created" }
        timestamp: { type: string, format: date-time }
        tenant_id: { type: string, format: uuid }
        data:
          type: object
          description: "The full resource object that triggered the event"
          additionalProperties: true

  responses:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadRequest:
      description: Invalid request (validation error or malformed input)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Insufficient permissions (missing scope)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PayloadTooLarge:
      description: Request body exceeds size limit
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    MultiStatus:
      description: Partial success — some items succeeded, others failed
      content:
        application/json:
          schema: { $ref: "#/components/schemas/BulkResponse" }
    Deleted:
      description: Resource deleted
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean }
              message: { type: string }
    Conflict:
      description: Conflict (e.g. a uniqueness constraint was violated)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

# ──────────────────────────────────────────────────────────────
# Paths
# ──────────────────────────────────────────────────────────────

paths:
  # ── Assets ──
  /assets:
    get:
      operationId: listAssets
      summary: List assets
      tags: [Assets]
      description: |
        Returns a paginated list of assets. Requires `assets:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
        - name: building_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by building
        - name: system_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system
      responses:
        "200":
          description: Paginated asset list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Asset" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAsset
      summary: Create asset
      tags: [Assets]
      description: Requires `assets:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Asset" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /assets/{id}:
    get:
      operationId: getAsset
      summary: Get asset by ID
      tags: [Assets]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Asset" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAsset
      summary: Update asset
      tags: [Assets]
      description: Requires `assets:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Asset" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAsset
      summary: Delete asset
      tags: [Assets]
      description: Requires `assets:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Work Orders ──
  /work-orders:
    get:
      operationId: listWorkOrders
      summary: List work orders
      tags: [Work Orders]
      description: |
        Returns a paginated list of work orders. Requires `work_orders:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string, enum: [NEW, IN_PROGRESS, ON_HOLD, REJECTED, COMPLETED, CANCELLED] }
          description: Filter by status
        - name: priority
          in: query
          schema: { type: string, enum: [LOW, MEDIUM, HIGH, URGENT] }
          description: Filter by priority
        - name: type
          in: query
          schema: { type: string, enum: [PM, REACTIVE] }
          description: Filter by type
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
        - name: infrastructure_asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by infrastructure feature (mutually exclusive target with asset_id)
      responses:
        "200":
          description: Paginated work order list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WorkOrder" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWorkOrder
      summary: Create work order
      tags: [Work Orders]
      description: Requires `work_orders:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkOrderCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrder" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /work-orders/{id}:
    get:
      operationId: getWorkOrder
      summary: Get work order by ID
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Work order detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrder" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateWorkOrder
      summary: Update work order
      tags: [Work Orders]
      description: Requires `work_orders:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkOrderUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrder" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkOrder
      summary: Delete work order
      tags: [Work Orders]
      description: Requires `work_orders:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Sites ──
  /sites:
    get:
      operationId: listSites
      summary: List sites
      tags: [Sites]
      description: |
        Returns a paginated list of sites. Requires `sites:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: city
          in: query
          schema: { type: string }
          description: Filter by city (partial match)
      responses:
        "200":
          description: Paginated site list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Site" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createSite
      summary: Create site
      tags: [Sites]
      description: Requires `sites:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SiteCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Site" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /sites/{id}:
    get:
      operationId: getSite
      summary: Get site by ID
      tags: [Sites]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Site detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Site" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateSite
      summary: Update site
      tags: [Sites]
      description: Requires `sites:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SiteUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Site" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteSite
      summary: Delete site
      tags: [Sites]
      description: Requires `sites:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Buildings ──
  /buildings:
    get:
      operationId: listBuildings
      summary: List buildings
      tags: [Buildings]
      description: |
        Returns a paginated list of buildings. Requires `buildings:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
      responses:
        "200":
          description: Paginated building list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Building" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createBuilding
      summary: Create building
      tags: [Buildings]
      description: Requires `buildings:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BuildingCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Building" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /buildings/{id}:
    get:
      operationId: getBuilding
      summary: Get building by ID
      tags: [Buildings]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Building detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Building" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateBuilding
      summary: Update building
      tags: [Buildings]
      description: Requires `buildings:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BuildingUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Building" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteBuilding
      summary: Delete building
      tags: [Buildings]
      description: Requires `buildings:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Locations ──
  /locations:
    get:
      operationId: listLocations
      summary: List locations
      tags: [Locations]
      description: |
        Returns a paginated list of locations within buildings. Requires `locations:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: building_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by building
        - name: location_type_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by location type
      responses:
        "200":
          description: Paginated location list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Location" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createLocation
      summary: Create location
      tags: [Locations]
      description: Requires `locations:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LocationCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Location" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /locations/{id}:
    get:
      operationId: getLocation
      summary: Get location by ID
      tags: [Locations]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Location detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Location" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateLocation
      summary: Update location
      tags: [Locations]
      description: Requires `locations:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LocationUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Location" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteLocation
      summary: Delete location
      tags: [Locations]
      description: Requires `locations:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Systems ──
  /systems:
    get:
      operationId: listSystems
      summary: List systems
      tags: [Systems]
      description: |
        Returns a paginated list of systems. Requires `systems:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: system_group_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system group
      responses:
        "200":
          description: Paginated system list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/System" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createSystem
      summary: Create system
      tags: [Systems]
      description: Requires `systems:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SystemCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/System" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /systems/{id}:
    get:
      operationId: getSystem
      summary: Get system by ID
      tags: [Systems]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: System detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/System" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateSystem
      summary: Update system
      tags: [Systems]
      description: Requires `systems:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SystemUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/System" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteSystem
      summary: Delete system
      tags: [Systems]
      description: Requires `systems:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── System Groups ──
  /system-groups:
    get:
      operationId: listSystemGroups
      summary: List system groups
      tags: [System Groups]
      description: |
        Returns a paginated list of system groups. Requires `systems:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: system_class_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system class
      responses:
        "200":
          description: Paginated system group list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/SystemGroup" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createSystemGroup
      summary: Create system group
      tags: [System Groups]
      description: Requires `systems:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SystemGroupCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SystemGroup" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /system-groups/{id}:
    get:
      operationId: getSystemGroup
      summary: Get system group by ID
      tags: [System Groups]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: System group detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SystemGroup" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateSystemGroup
      summary: Update system group
      tags: [System Groups]
      description: Requires `systems:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SystemGroupUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SystemGroup" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteSystemGroup
      summary: Delete system group
      tags: [System Groups]
      description: Requires `systems:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── System Classes ──
  /system-classes:
    get:
      operationId: listSystemClasses
      summary: List system classes
      tags: [System Classes]
      description: |
        Returns a paginated list of system classes (top-level classification). Requires `systems:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated system class list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/SystemClass" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createSystemClass
      summary: Create system class
      tags: [System Classes]
      description: Requires `systems:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SystemClassCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SystemClass" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /system-classes/{id}:
    get:
      operationId: getSystemClass
      summary: Get system class by ID
      tags: [System Classes]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: System class detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SystemClass" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateSystemClass
      summary: Update system class
      tags: [System Classes]
      description: Requires `systems:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SystemClassUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SystemClass" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteSystemClass
      summary: Delete system class
      tags: [System Classes]
      description: Requires `systems:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── PM Schedules ──
  /pm-schedules:
    get:
      operationId: listPmSchedules
      summary: List PM schedules
      tags: [PM Schedules]
      description: |
        Returns a paginated list of preventive maintenance schedules. Requires `pm_schedules:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string }
          description: Filter by status (e.g. active, paused)
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
        - name: frequency
          in: query
          schema: { type: string }
          description: Filter by frequency (e.g. WEEKLY, MONTHLY)
        - name: infrastructure_asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by infrastructure feature (mutually exclusive target with asset_id)
      responses:
        "200":
          description: Paginated PM schedule list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PmSchedule" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createPmSchedule
      summary: Create PM schedule
      tags: [PM Schedules]
      description: Requires `pm_schedules:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PmScheduleCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PmSchedule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /pm-schedules/{id}:
    get:
      operationId: getPmSchedule
      summary: Get PM schedule by ID
      tags: [PM Schedules]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: PM schedule detail (includes tasks, resources, additional IDs)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PmSchedule" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updatePmSchedule
      summary: Update PM schedule
      tags: [PM Schedules]
      description: Requires `pm_schedules:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PmScheduleUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PmSchedule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deletePmSchedule
      summary: Delete PM schedule
      tags: [PM Schedules]
      description: Requires `pm_schedules:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── PM Templates ──
  /pm-templates:
    get:
      operationId: listPmTemplates
      summary: List PM templates
      tags: [PM Templates]
      description: |
        Returns a paginated list of preventive maintenance templates. Requires `pm_templates:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated PM template list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PmTemplate" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createPmTemplate
      summary: Create PM template
      tags: [PM Templates]
      description: Requires `pm_templates:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PmTemplateCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PmTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /pm-templates/{id}:
    get:
      operationId: getPmTemplate
      summary: Get PM template by ID
      tags: [PM Templates]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: PM template detail (includes tasks, resources)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PmTemplate" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updatePmTemplate
      summary: Update PM template
      tags: [PM Templates]
      description: Requires `pm_templates:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PmTemplateUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PmTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deletePmTemplate
      summary: Delete PM template
      tags: [PM Templates]
      description: Requires `pm_templates:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Forms ──
  /form-templates:
    get:
      operationId: listFormTemplates
      summary: List form templates
      tags: [Forms]
      description: |
        Returns a paginated list of form templates. Requires `form_templates:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string, enum: [draft, published, archived] }
          description: Filter by status
        - name: work_category_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work category
      responses:
        "200":
          description: Paginated form template list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/FormTemplate" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createFormTemplate
      summary: Create form template
      tags: [Forms]
      description: Requires `form_templates:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FormTemplateCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /form-templates/{id}:
    get:
      operationId: getFormTemplate
      summary: Get form template by ID
      tags: [Forms]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Form template detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormTemplate" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateFormTemplate
      summary: Update form template
      tags: [Forms]
      description: Requires `form_templates:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FormTemplateUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteFormTemplate
      summary: Delete form template
      tags: [Forms]
      description: Requires `form_templates:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /form-template-items:
    get:
      operationId: listFormTemplateItems
      summary: List form template items
      tags: [Forms]
      description: |
        Returns a paginated list of form template items, ordered by sort order. Requires `form_template_items:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: template_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by form template ID
      responses:
        "200":
          description: Paginated form template item list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/FormTemplateItem" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createFormTemplateItem
      summary: Create form template item
      tags: [Forms]
      description: Requires `form_template_items:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FormTemplateItemCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormTemplateItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /form-template-items/{id}:
    get:
      operationId: getFormTemplateItem
      summary: Get form template item by ID
      tags: [Forms]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Form template item detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormTemplateItem" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateFormTemplateItem
      summary: Update form template item
      tags: [Forms]
      description: Requires `form_template_items:write` scope. Immutable fields (template_id, item_key) are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FormTemplateItemUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormTemplateItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteFormTemplateItem
      summary: Delete form template item
      tags: [Forms]
      description: Requires `form_template_items:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /form-responses:
    get:
      operationId: listFormResponses
      summary: List form responses
      tags: [Forms]
      description: |
        Returns a paginated list of form responses. Requires `form_responses:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: subject_type
          in: query
          schema: { type: string }
          description: Filter by subject type
        - name: subject_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by subject ID
        - name: template_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by form template ID
        - name: status
          in: query
          schema: { type: string }
          description: Filter by status
      responses:
        "200":
          description: Paginated form response list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/FormResponse" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createFormResponse
      summary: Attach a form to a record
      tags: [Forms]
      description: |
        Attaches a published form to one record so it can be filled in. The template's questions are
        snapshotted at attach time, so later edits to the template never change a form already in
        progress — which is why `snapshot`, `template_version`, `lineage_id` and `status` cannot be
        supplied by the caller.

        A record holds at most one form; attaching a second returns 409. To put a form on every work
        order a PM schedule generates, set `form_template_id` on the PM schedule instead of calling
        this per work order.

        Answering and completing a form happen in the AssetLab app, not through this API.
        Requires `form_responses:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FormResponseCreate" }
      responses:
        "201":
          description: Attached
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: A form is already attached to this record
        "429": { $ref: "#/components/responses/RateLimited" }

  /form-responses/{id}:
    get:
      operationId: getFormResponse
      summary: Get form response by ID
      tags: [Forms]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Form response detail (includes snapshot)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormResponse" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteFormResponse
      summary: Detach a form from its record
      tags: [Forms]
      description: |
        Removes the form from the record it is attached to, together with any answers already given.
        Requires `form_responses:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /form-response-answers:
    get:
      operationId: listFormResponseAnswers
      summary: List form response answers
      tags: [Forms]
      description: |
        Returns a paginated list of per-question answer values (read-only). Requires `form_response_answers:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: response_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by form response ID
        - name: item_key
          in: query
          schema: { type: string }
          description: Filter by item key
      responses:
        "200":
          description: Paginated form response answer list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/FormResponseAnswer" }
                  pagination: { $ref: "#/components/schemas/Pagination" }

  /form-response-answers/{id}:
    get:
      operationId: getFormResponseAnswer
      summary: Get form response answer by ID
      tags: [Forms]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Form response answer detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FormResponseAnswer" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Projects ──
  /projects:
    get:
      operationId: listProjects
      summary: List projects
      tags: [Projects]
      description: |
        Returns a paginated list of capital projects. Requires `projects:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string }
          description: Filter by project status
        - name: health_status
          in: query
          schema: { type: string }
          description: Filter by health status
      responses:
        "200":
          description: Paginated project list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Project" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createProject
      summary: Create project
      tags: [Projects]
      description: Requires `projects:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Project" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /projects/{id}:
    get:
      operationId: getProject
      summary: Get project by ID
      tags: [Projects]
      description: Detail response includes related sites, financial breakdown, and phase info.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Project" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateProject
      summary: Update project
      tags: [Projects]
      description: Requires `projects:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectInput" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Project" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProject
      summary: Delete project
      tags: [Projects]
      description: Requires `projects:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Contracts ──
  /contracts:
    get:
      operationId: listContracts
      summary: List contracts
      tags: [Contracts]
      description: |
        Returns a paginated list of vendor contracts. Requires `contracts:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: category
          in: query
          schema: { type: string }
          description: Filter by contract category
      responses:
        "200":
          description: Paginated contract list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Contract" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createContract
      summary: Create contract
      tags: [Contracts]
      description: Requires `contracts:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContractCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Contract" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /contracts/{id}:
    get:
      operationId: getContract
      summary: Get contract by ID
      tags: [Contracts]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Contract detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Contract" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateContract
      summary: Update contract
      tags: [Contracts]
      description: Requires `contracts:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContractUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Contract" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteContract
      summary: Delete contract
      tags: [Contracts]
      description: Requires `contracts:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Compliance ──
  /compliance:
    get:
      operationId: listComplianceItems
      summary: List compliance items
      tags: [Compliance]
      description: |
        Returns a paginated list of regulatory compliance items. Requires `compliance:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string }
          description: Filter by status
        - name: system_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system
      responses:
        "200":
          description: Paginated compliance item list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ComplianceItem" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createComplianceItem
      summary: Create compliance item
      tags: [Compliance]
      description: Requires `compliance:write` scope. Manager+ role required.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ComplianceItemCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ComplianceItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /compliance/{id}:
    get:
      operationId: getComplianceItem
      summary: Get compliance item by ID
      tags: [Compliance]
      description: Detail response includes linked PM schedules.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Compliance item detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ComplianceItem" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateComplianceItem
      summary: Update compliance item
      tags: [Compliance]
      description: Requires `compliance:write` scope. Manager+ role required.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ComplianceItemUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ComplianceItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteComplianceItem
      summary: Delete compliance item
      tags: [Compliance]
      description: Requires `compliance:write` scope. Manager+ role required.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Dashboard ──
  /dashboard:
    get:
      operationId: getDashboardStats
      summary: Get dashboard statistics
      tags: [Dashboard]
      description: |
        Returns aggregated counts and statistics across all resources. Requires `dashboard:read` scope.
        No pagination — returns a single stats object.
      responses:
        "200":
          description: Dashboard statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/DashboardStats" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Parts ──
  /parts:
    get:
      operationId: listParts
      summary: List parts
      tags: [Parts]
      description: |
        Returns a paginated list of spare parts and inventory items. Requires `parts:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
        - name: category
          in: query
          schema: { type: string }
          description: Filter by category (partial match)
      responses:
        "200":
          description: Paginated parts list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Part" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createPart
      summary: Create part
      tags: [Parts]
      description: Requires `parts:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PartCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Part" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /parts/{id}:
    get:
      operationId: getPart
      summary: Get part by ID
      tags: [Parts]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Part detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Part" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updatePart
      summary: Update part
      tags: [Parts]
      description: Requires `parts:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PartUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Part" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deletePart
      summary: Delete part
      tags: [Parts]
      description: Requires `parts:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Parts ──
  /asset-parts:
    get:
      operationId: listAssetParts
      summary: List asset-part associations
      tags: [Parts]
      description: |
        Returns part-to-asset associations. Filter by `asset_id` to list all parts on one asset,
        or by `part_id` to see all assets using a specific part. Requires `asset_parts:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset ID
        - name: part_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by part ID
      responses:
        "200":
          description: Paginated asset-parts list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetPart" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetPart
      summary: Link a part to an asset
      tags: [Parts]
      description: Requires `asset_parts:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [asset_id, part_id]
              properties:
                asset_id: { type: string, format: uuid }
                part_id: { type: string, format: uuid }
                quantity: { type: number, minimum: 0 }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetPart" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-parts/{id}:
    get:
      operationId: getAssetPart
      summary: Get asset-part association by ID
      tags: [Parts]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset-part detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetPart" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetPart
      summary: Update asset-part quantity
      tags: [Parts]
      description: Requires `asset_parts:write` scope. Only `quantity` is updatable.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                quantity: { type: number, minimum: 0 }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetPart" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetPart
      summary: Remove a part from an asset
      tags: [Parts]
      description: Requires `asset_parts:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Vendors ──
  /vendors:
    get:
      operationId: listVendors
      summary: List vendors
      tags: [Vendors]
      description: |
        Returns a paginated list of vendor and supplier records. Requires `vendors:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string }
          description: "Filter by status (e.g. active, inactive)"
        - name: category
          in: query
          schema: { type: string }
          description: Filter by category (matches vendors whose categories array contains this value)
        - name: city
          in: query
          schema: { type: string }
          description: Filter by city (partial match)
      responses:
        "200":
          description: Paginated vendor list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Vendor" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createVendor
      summary: Create vendor
      tags: [Vendors]
      description: Requires `vendors:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VendorCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Vendor" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /vendors/{id}:
    get:
      operationId: getVendor
      summary: Get vendor by ID
      tags: [Vendors]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Vendor detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Vendor" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateVendor
      summary: Update vendor
      tags: [Vendors]
      description: Requires `vendors:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VendorUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Vendor" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteVendor
      summary: Delete vendor
      tags: [Vendors]
      description: Requires `vendors:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Work Requests ──
  /work-requests:
    get:
      operationId: listWorkRequests
      summary: List work requests
      tags: [Work Requests]
      description: |
        Returns a paginated list of work requests submitted by requesters. Requires `work_requests:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string, enum: [PENDING_REVIEW, APPROVED, REJECTED] }
          description: Filter by status
        - name: priority
          in: query
          schema: { type: string }
          description: Filter by priority
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
        - name: infrastructure_asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by infrastructure feature (mutually exclusive target with asset_id)
      responses:
        "200":
          description: Paginated work request list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WorkRequest" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWorkRequest
      summary: Create work request
      tags: [Work Requests]
      description: Requires `work_requests:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkRequestCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkRequest" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /work-requests/{id}:
    get:
      operationId: getWorkRequest
      summary: Get work request by ID
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Work request detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateWorkRequest
      summary: Update work request
      tags: [Work Requests]
      description: Requires `work_requests:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkRequestUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkRequest" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkRequest
      summary: Delete work request
      tags: [Work Requests]
      description: Requires `work_requests:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Invoices ──
  /invoices:
    get:
      operationId: listInvoices
      summary: List invoices
      tags: [Invoices]
      description: |
        Returns a paginated list of invoices. Requires `invoices:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string, enum: [pending, approved, paid, voided] }
          description: Filter by status
        - name: vendor_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by vendor
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
      responses:
        "200":
          description: Paginated invoice list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Invoice" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createInvoice
      summary: Create invoice
      tags: [Invoices]
      description: Requires `invoices:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InvoiceCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Invoice" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /invoices/{id}:
    get:
      operationId: getInvoice
      summary: Get invoice by ID
      tags: [Invoices]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Invoice detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Invoice" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInvoice
      summary: Update invoice
      tags: [Invoices]
      description: Requires `invoices:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InvoiceUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Invoice" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteInvoice
      summary: Delete invoice
      tags: [Invoices]
      description: Requires `invoices:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Purchase Orders ──
  /purchase-orders:
    get:
      operationId: listPurchaseOrders
      summary: List purchase orders
      tags: [Purchase Orders]
      description: |
        Returns a paginated list of purchase orders. Requires `purchase_orders:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string, enum: [draft, issued, partially_received, received, closed, cancelled] }
          description: Filter by status
        - name: vendor_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by vendor
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
      responses:
        "200":
          description: Paginated purchase order list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PurchaseOrder" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createPurchaseOrder
      summary: Create purchase order
      tags: [Purchase Orders]
      description: Requires `purchase_orders:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PurchaseOrderCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PurchaseOrder" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /purchase-orders/{id}:
    get:
      operationId: getPurchaseOrder
      summary: Get purchase order by ID
      tags: [Purchase Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Purchase order detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PurchaseOrder" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updatePurchaseOrder
      summary: Update purchase order
      tags: [Purchase Orders]
      description: Requires `purchase_orders:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PurchaseOrderUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PurchaseOrder" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deletePurchaseOrder
      summary: Delete purchase order
      tags: [Purchase Orders]
      description: Requires `purchase_orders:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Expenses ──
  /expenses:
    get:
      operationId: listExpenses
      summary: List expenses
      tags: [Expenses]
      description: |
        Returns a paginated list of expenses. Requires `expenses:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: work_order_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work order
        - name: category_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by cost category
      responses:
        "200":
          description: Paginated expense list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Expense" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createExpense
      summary: Create expense
      tags: [Expenses]
      description: Requires `expenses:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ExpenseCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Expense" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /expenses/{id}:
    get:
      operationId: getExpense
      summary: Get expense by ID
      tags: [Expenses]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Expense detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Expense" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateExpense
      summary: Update expense
      tags: [Expenses]
      description: Requires `expenses:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ExpenseUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Expense" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteExpense
      summary: Delete expense
      tags: [Expenses]
      description: Requires `expenses:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Change Orders ──
  /change-orders:
    get:
      operationId: listChangeOrders
      summary: List change orders
      tags: [Change Orders]
      description: |
        Returns a paginated list of change orders. Requires `change_orders:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: status
          in: query
          schema: { type: string, enum: [draft, submitted, approved, rejected] }
          description: Filter by status
        - name: vendor_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by vendor
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
      responses:
        "200":
          description: Paginated change order list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ChangeOrder" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createChangeOrder
      summary: Create change order
      tags: [Change Orders]
      description: Requires `change_orders:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChangeOrderCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ChangeOrder" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /change-orders/{id}:
    get:
      operationId: getChangeOrder
      summary: Get change order by ID
      tags: [Change Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Change order detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ChangeOrder" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateChangeOrder
      summary: Update change order
      tags: [Change Orders]
      description: Requires `change_orders:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChangeOrderUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ChangeOrder" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteChangeOrder
      summary: Delete change order
      tags: [Change Orders]
      description: Requires `change_orders:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Document Folder Templates ──
  /project-document-folder-templates:
    get:
      operationId: listProjectDocumentFolderTemplates
      summary: List project document folder templates
      tags: [Project Document Folder Templates]
      description: |
        Returns a paginated list of project document folder templates. Requires `project_document_folder_templates:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated template list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectDocumentFolderTemplate" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createProjectDocumentFolderTemplate
      summary: Create project document folder template
      tags: [Project Document Folder Templates]
      description: Requires `project_document_folder_templates:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectDocumentFolderTemplateCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectDocumentFolderTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-document-folder-templates/{id}:
    get:
      operationId: getProjectDocumentFolderTemplate
      summary: Get project document folder template by ID
      tags: [Project Document Folder Templates]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Template detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectDocumentFolderTemplate" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateProjectDocumentFolderTemplate
      summary: Update project document folder template
      tags: [Project Document Folder Templates]
      description: Requires `project_document_folder_templates:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectDocumentFolderTemplateUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectDocumentFolderTemplate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectDocumentFolderTemplate
      summary: Delete project document folder template
      tags: [Project Document Folder Templates]
      description: Requires `project_document_folder_templates:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Types ──
  /asset-types:
    get:
      operationId: listAssetTypes
      summary: List asset types
      tags: [Asset Types]
      description: |
        Returns a paginated list of asset type classifications. Requires `asset_types:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: group_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset type group
      responses:
        "200":
          description: Paginated asset type list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetType" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetType
      summary: Create asset type
      tags: [Asset Types]
      description: Requires `asset_types:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetTypeCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetType" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-types/{id}:
    get:
      operationId: getAssetType
      summary: Get asset type by ID
      tags: [Asset Types]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset type detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetType" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetType
      summary: Update asset type
      tags: [Asset Types]
      description: Requires `asset_types:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetTypeUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetType" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetType
      summary: Delete asset type
      tags: [Asset Types]
      description: Requires `asset_types:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Statuses ──
  /asset-statuses:
    get:
      operationId: listAssetStatuses
      summary: List asset statuses
      tags: [Asset Statuses]
      description: |
        Returns a paginated list of asset lifecycle statuses. Requires `asset_statuses:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated asset status list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetStatus" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetStatus
      summary: Create asset status
      tags: [Asset Statuses]
      description: Requires `asset_statuses:write` scope. Supervisor+ role required.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetStatusCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-statuses/{id}:
    get:
      operationId: getAssetStatus
      summary: Get asset status by ID
      tags: [Asset Statuses]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset status detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetStatus" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetStatus
      summary: Update asset status
      tags: [Asset Statuses]
      description: Requires `asset_statuses:write` scope. Supervisor+ role required.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetStatusUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetStatus
      summary: Delete asset status
      tags: [Asset Statuses]
      description: Requires `asset_statuses:write` scope. Admin role required.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Work Categories ──
  /work-categories:
    get:
      operationId: listWorkCategories
      summary: List work categories
      tags: [Work Categories]
      description: |
        Returns a paginated list of work order categories. Requires `work_categories:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated work category list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WorkCategory" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWorkCategory
      summary: Create work category
      tags: [Work Categories]
      description: Requires `work_categories:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkCategoryCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /work-categories/{id}:
    get:
      operationId: getWorkCategory
      summary: Get work category by ID
      tags: [Work Categories]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Work category detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkCategory" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateWorkCategory
      summary: Update work category
      tags: [Work Categories]
      description: Requires `work_categories:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkCategoryUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkCategory
      summary: Delete work category
      tags: [Work Categories]
      description: Requires `work_categories:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Manufacturers ──
  /manufacturers:
    get:
      operationId: listManufacturers
      summary: List manufacturers
      tags: [Manufacturers]
      description: |
        Returns a paginated list of equipment manufacturers. Requires `manufacturers:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated manufacturer list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Manufacturer" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createManufacturer
      summary: Create manufacturer
      tags: [Manufacturers]
      description: Requires `manufacturers:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ManufacturerCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Manufacturer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /manufacturers/{id}:
    get:
      operationId: getManufacturer
      summary: Get manufacturer by ID
      tags: [Manufacturers]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Manufacturer detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Manufacturer" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateManufacturer
      summary: Update manufacturer
      tags: [Manufacturers]
      description: Requires `manufacturers:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ManufacturerUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Manufacturer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteManufacturer
      summary: Delete manufacturer
      tags: [Manufacturers]
      description: Requires `manufacturers:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Type Groups ──
  /asset-type-groups:
    get:
      operationId: listAssetTypeGroups
      summary: List asset type groups
      tags: [Asset Type Groups]
      description: |
        Returns a paginated list of asset type groups. Groups organize asset types into logical categories. Requires `asset_type_groups:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated asset type group list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetTypeGroup" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetTypeGroup
      summary: Create asset type group
      tags: [Asset Type Groups]
      description: Requires `asset_type_groups:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetTypeGroupCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetTypeGroup" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-type-groups/{id}:
    get:
      operationId: getAssetTypeGroup
      summary: Get asset type group by ID
      tags: [Asset Type Groups]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset type group detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetTypeGroup" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetTypeGroup
      summary: Update asset type group
      tags: [Asset Type Groups]
      description: Requires `asset_type_groups:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetTypeGroupUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetTypeGroup" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetTypeGroup
      summary: Delete asset type group
      tags: [Asset Type Groups]
      description: Requires `asset_type_groups:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Building Types ──
  /building-types:
    get:
      operationId: listBuildingTypes
      summary: List building types
      tags: [Building Types]
      description: |
        Returns a paginated list of building type classifications. Requires `building_types:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated building type list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/BuildingType" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createBuildingType
      summary: Create building type
      tags: [Building Types]
      description: Requires `building_types:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BuildingTypeCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/BuildingType" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /building-types/{id}:
    get:
      operationId: getBuildingType
      summary: Get building type by ID
      tags: [Building Types]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Building type detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/BuildingType" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateBuildingType
      summary: Update building type
      tags: [Building Types]
      description: Requires `building_types:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BuildingTypeUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/BuildingType" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteBuildingType
      summary: Delete building type
      tags: [Building Types]
      description: Requires `building_types:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Location Types ──
  /location-types:
    get:
      operationId: listLocationTypes
      summary: List location types
      tags: [Location Types]
      description: |
        Returns a paginated list of location type classifications. Requires `location_types:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated location type list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LocationType" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createLocationType
      summary: Create location type
      tags: [Location Types]
      description: Requires `location_types:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LocationTypeCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LocationType" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /location-types/{id}:
    get:
      operationId: getLocationType
      summary: Get location type by ID
      tags: [Location Types]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Location type detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LocationType" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateLocationType
      summary: Update location type
      tags: [Location Types]
      description: Requires `location_types:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LocationTypeUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LocationType" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteLocationType
      summary: Delete location type
      tags: [Location Types]
      description: Requires `location_types:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Phase Categories ──
  /project-phase-categories:
    get:
      operationId: listProjectPhaseCategories
      summary: List project phase categories
      tags: [Project Phase Categories]
      description: |
        Returns a paginated list of project phase categories sorted by sort_order. Requires `project_phase_categories:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated project phase category list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectPhaseCategory" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createProjectPhaseCategory
      summary: Create project phase category
      tags: [Project Phase Categories]
      description: Requires `project_phase_categories:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectPhaseCategoryCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectPhaseCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-phase-categories/{id}:
    get:
      operationId: getProjectPhaseCategory
      summary: Get project phase category by ID
      tags: [Project Phase Categories]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project phase category detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectPhaseCategory" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateProjectPhaseCategory
      summary: Update project phase category
      tags: [Project Phase Categories]
      description: Requires `project_phase_categories:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectPhaseCategoryUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectPhaseCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectPhaseCategory
      summary: Delete project phase category
      tags: [Project Phase Categories]
      description: Requires `project_phase_categories:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Cost Categories ──
  /cost-categories:
    get:
      operationId: listCostCategories
      summary: List cost categories
      tags: [Cost Categories]
      description: |
        Returns a paginated list of cost categories for financial tracking. Requires `cost_categories:read` scope.
        Categories can be hierarchical (parent_id references another cost category).
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: is_active
          in: query
          schema: { type: string, enum: ["true", "false"] }
          description: Filter by active status
        - name: parent_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by parent category
      responses:
        "200":
          description: Paginated cost category list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CostCategory" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createCostCategory
      summary: Create cost category
      tags: [Cost Categories]
      description: Requires `cost_categories:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CostCategoryCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CostCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /cost-categories/{id}:
    get:
      operationId: getCostCategory
      summary: Get cost category by ID
      tags: [Cost Categories]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Cost category detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CostCategory" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateCostCategory
      summary: Update cost category
      tags: [Cost Categories]
      description: Requires `cost_categories:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CostCategoryUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CostCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteCostCategory
      summary: Delete cost category
      tags: [Cost Categories]
      description: Requires `cost_categories:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Budgets ──
  /budgets:
    get:
      operationId: listBudgets
      summary: List budgets
      tags: [Budgets]
      description: |
        Returns a paginated list of annual funding budgets. Requires `budgets:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: year
          in: query
          schema: { type: integer }
          description: Filter by budget year
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
        - name: building_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by building
        - name: funding_source
          in: query
          schema: { type: integer }
          description: Filter by funding source
      responses:
        "200":
          description: Paginated budget list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Budget" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createBudget
      summary: Create budget
      tags: [Budgets]
      description: Requires `budgets:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BudgetCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Budget" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /budgets/{id}:
    get:
      operationId: getBudget
      summary: Get budget by ID
      tags: [Budgets]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Budget detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Budget" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateBudget
      summary: Update budget
      tags: [Budgets]
      description: Requires `budgets:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BudgetUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Budget" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteBudget
      summary: Delete budget
      tags: [Budgets]
      description: Requires `budgets:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Comments ──
  /asset-comments:
    get:
      operationId: listAssetComments
      summary: List asset comments
      tags: [Asset Comments]
      description: |
        Returns a paginated list of comments on assets. Requires `asset_comments:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset
      responses:
        "200":
          description: Paginated asset comment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetComment" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetComment
      summary: Create asset comment
      tags: [Asset Comments]
      description: Requires `asset_comments:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetCommentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-comments/{id}:
    get:
      operationId: getAssetComment
      summary: Get asset comment by ID
      tags: [Asset Comments]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset comment detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetComment" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetComment
      summary: Update asset comment
      tags: [Asset Comments]
      description: Requires `asset_comments:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetCommentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetComment
      summary: Delete asset comment
      tags: [Asset Comments]
      description: Requires `asset_comments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Costs ──
  /asset-costs:
    get:
      operationId: listAssetCosts
      summary: List asset costs
      tags: [Asset Costs]
      description: |
        Returns a paginated list of cost records for assets. Requires `asset_costs:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
        - name: category
          in: query
          schema: { type: string }
          description: Filter by cost category (e.g. Repair, PM, Operation)
        - name: work_order_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work order
      responses:
        "200":
          description: Paginated asset cost list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetCost" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetCost
      summary: Create asset cost
      tags: [Asset Costs]
      description: Requires `asset_costs:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetCostCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetCost" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-costs/{id}:
    get:
      operationId: getAssetCost
      summary: Get asset cost by ID
      tags: [Asset Costs]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset cost detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetCost" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetCost
      summary: Update asset cost
      tags: [Asset Costs]
      description: Requires `asset_costs:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetCostUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetCost" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetCost
      summary: Delete asset cost
      tags: [Asset Costs]
      description: Requires `asset_costs:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Replacement Plans ──
  /asset-replacement-plans:
    get:
      operationId: listAssetReplacementPlans
      summary: List asset replacement plans
      tags: [Asset Replacement Plans]
      description: |
        Returns a paginated list of asset replacement plans. Requires `asset_replacement_plans:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset
        - name: status
          in: query
          schema: { type: string, enum: [PLANNED, BUDGETED, APPROVED, COMPLETED, CANCELLED] }
          description: Filter by status
        - name: priority
          in: query
          schema: { type: string, enum: [CRITICAL, HIGH, MEDIUM, LOW] }
          description: Filter by priority
        - name: year
          in: query
          schema: { type: integer }
          description: Filter by planned replacement year
        - name: infrastructure_asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by infrastructure feature (mutually exclusive target with asset_id)
      responses:
        "200":
          description: Paginated asset replacement plan list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetReplacementPlan" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetReplacementPlan
      summary: Create asset replacement plan
      tags: [Asset Replacement Plans]
      description: Requires `asset_replacement_plans:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetReplacementPlanCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetReplacementPlan" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-replacement-plans/{id}:
    get:
      operationId: getAssetReplacementPlan
      summary: Get asset replacement plan by ID
      tags: [Asset Replacement Plans]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset replacement plan detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetReplacementPlan" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetReplacementPlan
      summary: Update asset replacement plan
      tags: [Asset Replacement Plans]
      description: Requires `asset_replacement_plans:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetReplacementPlanUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetReplacementPlan" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetReplacementPlan
      summary: Delete asset replacement plan
      tags: [Asset Replacement Plans]
      description: Requires `asset_replacement_plans:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Risk History ──
  /asset-risk-history:
    get:
      operationId: listAssetRiskHistory
      summary: List asset risk history
      tags: [Asset Risk History]
      description: |
        Returns a paginated list of historical risk score entries for assets. Requires `asset_risk_history:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset
        - name: trigger_event
          in: query
          schema: { type: string, enum: [maintenance, inspection, manual_update, scheduled] }
          description: Filter by trigger event type
      responses:
        "200":
          description: Paginated asset risk history list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetRiskHistory" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-risk-history/{id}:
    get:
      operationId: getAssetRiskHistoryEntry
      summary: Get asset risk history entry by ID
      tags: [Asset Risk History]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset risk history entry detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetRiskHistory" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Work Order Comments ──
  /work-order-comments:
    get:
      operationId: listWorkOrderComments
      summary: List work order comments
      tags: [Work Order Comments]
      description: |
        Returns a paginated list of comments on work orders. Requires `work_order_comments:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: work_order_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work order
      responses:
        "200":
          description: Paginated work order comment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WorkOrderComment" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWorkOrderComment
      summary: Create work order comment
      tags: [Work Order Comments]
      description: Requires `work_order_comments:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkOrderCommentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrderComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /work-order-comments/{id}:
    get:
      operationId: getWorkOrderComment
      summary: Get work order comment by ID
      tags: [Work Order Comments]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Work order comment detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrderComment" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateWorkOrderComment
      summary: Update work order comment
      tags: [Work Order Comments]
      description: Requires `work_order_comments:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkOrderCommentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrderComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkOrderComment
      summary: Delete work order comment
      tags: [Work Order Comments]
      description: Requires `work_order_comments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Work Order Schedules (technician day plans) ──
  /work-order-schedules:
    get:
      operationId: listWorkOrderSchedules
      summary: List work order schedules
      tags: [Work Order Schedules]
      description: |
        Returns a paginated list of work order schedule entries (a work order on a technician's
        calendar for a date). Entries carrying a `stop_order` form that technician's ordered day
        plan; rows are returned by date, then `stop_order` (nulls last), then start time — so
        filtering by `technician_id` + `scheduled_date` yields the technician's day in driving
        order. Requires `work_order_schedules:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: work_order_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work order
        - name: technician_id
          in: query
          schema: { type: string }
          description: Filter by technician (Clerk user ID)
        - name: scheduled_date
          in: query
          schema: { type: string, format: date }
          description: Filter by exact date
        - name: date_from
          in: query
          schema: { type: string, format: date }
          description: Scheduled on or after this date
        - name: date_to
          in: query
          schema: { type: string, format: date }
          description: Scheduled on or before this date
      responses:
        "200":
          description: Paginated work order schedule list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WorkOrderSchedule" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWorkOrderSchedule
      summary: Create work order schedule
      tags: [Work Order Schedules]
      description: Requires `work_order_schedules:write` scope. `work_order_id` must reference a work order in the key's tenant.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkOrderScheduleCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrderSchedule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /work-order-schedules/{id}:
    get:
      operationId: getWorkOrderSchedule
      summary: Get work order schedule by ID
      tags: [Work Order Schedules]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Work order schedule detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrderSchedule" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateWorkOrderSchedule
      summary: Update work order schedule
      tags: [Work Order Schedules]
      description: Requires `work_order_schedules:write` scope. Immutable fields (including `work_order_id`) are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WorkOrderScheduleUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WorkOrderSchedule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkOrderSchedule
      summary: Delete work order schedule
      tags: [Work Order Schedules]
      description: Requires `work_order_schedules:write` scope. Removes the stop from the technician's day.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Tasks ──
  /project-tasks:
    get:
      operationId: listProjectTasks
      summary: List project tasks
      tags: [Project Tasks]
      description: |
        Returns a paginated list of tasks within projects. Requires `project_tasks:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: phase_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project phase
        - name: status
          in: query
          schema: { type: string, enum: [todo, in_progress, completed, blocked, cancelled] }
          description: Filter by status
        - name: priority
          in: query
          schema: { type: string, enum: [low, medium, high, critical] }
          description: Filter by priority
      responses:
        "200":
          description: Paginated project task list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectTask" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectTask
      summary: Create project task
      tags: [Project Tasks]
      description: Requires `project_tasks:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectTaskCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTask" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-tasks/{id}:
    get:
      operationId: getProjectTask
      summary: Get project task by ID
      tags: [Project Tasks]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project task detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTask" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectTask
      summary: Update project task
      tags: [Project Tasks]
      description: Requires `project_tasks:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectTaskUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTask" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectTask
      summary: Delete project task
      tags: [Project Tasks]
      description: Requires `project_tasks:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Milestones ──
  /project-milestones:
    get:
      operationId: listProjectMilestones
      summary: List project milestones
      tags: [Project Milestones]
      description: |
        Returns a paginated list of milestones within projects. Requires `project_milestones:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: status
          in: query
          schema: { type: string, enum: [pending, completed, missed, at_risk] }
          description: Filter by status
      responses:
        "200":
          description: Paginated project milestone list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectMilestone" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectMilestone
      summary: Create project milestone
      tags: [Project Milestones]
      description: Requires `project_milestones:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectMilestoneCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectMilestone" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-milestones/{id}:
    get:
      operationId: getProjectMilestone
      summary: Get project milestone by ID
      tags: [Project Milestones]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project milestone detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectMilestone" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectMilestone
      summary: Update project milestone
      tags: [Project Milestones]
      description: Requires `project_milestones:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectMilestoneUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectMilestone" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectMilestone
      summary: Delete project milestone
      tags: [Project Milestones]
      description: Requires `project_milestones:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Phases ──
  /project-phases:
    get:
      operationId: listProjectPhases
      summary: List project phases
      tags: [Project Phases]
      description: |
        Returns a paginated list of phases within projects. Requires `project_phases:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: status
          in: query
          schema: { type: string, enum: [pending, in_progress, completed, skipped] }
          description: Filter by status
      responses:
        "200":
          description: Paginated project phase list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectPhase" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectPhase
      summary: Create project phase
      tags: [Project Phases]
      description: Requires `project_phases:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectPhaseCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectPhase" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-phases/{id}:
    get:
      operationId: getProjectPhase
      summary: Get project phase by ID
      tags: [Project Phases]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project phase detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectPhase" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectPhase
      summary: Update project phase
      tags: [Project Phases]
      description: Requires `project_phases:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectPhaseUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectPhase" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectPhase
      summary: Delete project phase
      tags: [Project Phases]
      description: Requires `project_phases:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Budget Items ──
  /project-budget-items:
    get:
      operationId: listProjectBudgetItems
      summary: List project budget items
      tags: [Project Budget Items]
      description: |
        Returns a paginated list of budget line items for projects. Requires `project_budget_items:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: category
          in: query
          schema: { type: string, enum: [labor, materials, equipment, subcontractors, permits, contingency, other] }
          description: Filter by budget category
      responses:
        "200":
          description: Paginated project budget item list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectBudgetItem" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectBudgetItem
      summary: Create project budget item
      tags: [Project Budget Items]
      description: Requires `project_budget_items:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectBudgetItemCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectBudgetItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-budget-items/{id}:
    get:
      operationId: getProjectBudgetItem
      summary: Get project budget item by ID
      tags: [Project Budget Items]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project budget item detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectBudgetItem" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectBudgetItem
      summary: Update project budget item
      tags: [Project Budget Items]
      description: Requires `project_budget_items:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectBudgetItemUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectBudgetItem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectBudgetItem
      summary: Delete project budget item
      tags: [Project Budget Items]
      description: Requires `project_budget_items:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Time Entries ──
  /project-time-entries:
    get:
      operationId: listProjectTimeEntries
      summary: List project time entries
      tags: [Project Time Entries]
      description: |
        Returns a paginated list of time tracking entries for projects. Requires `project_time_entries:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: task_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by task
        - name: user_id
          in: query
          schema: { type: string }
          description: Filter by Clerk user ID
      responses:
        "200":
          description: Paginated project time entry list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectTimeEntry" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectTimeEntry
      summary: Create project time entry
      tags: [Project Time Entries]
      description: Requires `project_time_entries:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectTimeEntryCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTimeEntry" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-time-entries/{id}:
    get:
      operationId: getProjectTimeEntry
      summary: Get project time entry by ID
      tags: [Project Time Entries]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project time entry detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTimeEntry" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectTimeEntry
      summary: Update project time entry
      tags: [Project Time Entries]
      description: Requires `project_time_entries:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectTimeEntryUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTimeEntry" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectTimeEntry
      summary: Delete project time entry
      tags: [Project Time Entries]
      description: Requires `project_time_entries:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Comments ──
  /project-comments:
    get:
      operationId: listProjectComments
      summary: List project comments
      tags: [Project Comments]
      description: |
        Returns a paginated list of comments on projects. Requires `project_comments:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
      responses:
        "200":
          description: Paginated project comment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectComment" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectComment
      summary: Create project comment
      tags: [Project Comments]
      description: Requires `project_comments:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectCommentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-comments/{id}:
    get:
      operationId: getProjectComment
      summary: Get project comment by ID
      tags: [Project Comments]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project comment detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectComment" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectComment
      summary: Update project comment
      tags: [Project Comments]
      description: Requires `project_comments:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectCommentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectComment
      summary: Delete project comment
      tags: [Project Comments]
      description: Requires `project_comments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Team Members ──
  /project-team-members:
    get:
      operationId: listProjectTeamMembers
      summary: List project team members
      tags: [Project Team Members]
      description: |
        Returns a paginated list of project team members. Requires `project_team_members:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: user_id
          in: query
          schema: { type: string }
          description: Filter by Clerk user ID
        - name: is_active
          in: query
          schema: { type: boolean }
          description: Filter by active status
      responses:
        "200":
          description: Paginated project team member list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectTeamMember" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectTeamMember
      summary: Add team member to project
      tags: [Project Team Members]
      description: Requires `project_team_members:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectTeamMemberCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTeamMember" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-team-members/{id}:
    get:
      operationId: getProjectTeamMember
      summary: Get project team member by ID
      tags: [Project Team Members]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project team member detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTeamMember" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectTeamMember
      summary: Update project team member
      tags: [Project Team Members]
      description: Requires `project_team_members:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectTeamMemberUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTeamMember" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

    delete:
      operationId: deleteProjectTeamMember
      summary: Remove team member from project
      tags: [Project Team Members]
      description: Requires `project_team_members:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Task Dependencies ──
  /project-task-dependencies:
    get:
      operationId: listProjectTaskDependencies
      summary: List project task dependencies
      tags: [Project Task Dependencies]
      description: |
        Returns a paginated list of task dependencies. Requires `project_task_dependencies:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: task_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by task
        - name: depends_on_task_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by depended-on task
        - name: dependency_type
          in: query
          schema: { type: string, enum: [finish_to_start, start_to_start, finish_to_finish, start_to_finish] }
          description: Filter by dependency type
      responses:
        "200":
          description: Paginated task dependency list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectTaskDependency" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectTaskDependency
      summary: Create task dependency
      tags: [Project Task Dependencies]
      description: Requires `project_task_dependencies:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectTaskDependencyCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTaskDependency" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-task-dependencies/{id}:
    get:
      operationId: getProjectTaskDependency
      summary: Get task dependency by ID
      tags: [Project Task Dependencies]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Task dependency detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectTaskDependency" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectTaskDependency
      summary: Delete task dependency
      tags: [Project Task Dependencies]
      description: Requires `project_task_dependencies:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Updates ──
  /project-updates:
    get:
      operationId: listProjectUpdates
      summary: List project updates
      tags: [Project Updates]
      description: |
        Returns a paginated list of periodic project status updates. Requires `project_updates:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: timeframe
          in: query
          schema: { type: string, enum: [monthly, quarterly, bi-annually, annually] }
          description: Filter by update timeframe
        - name: period_year
          in: query
          schema: { type: integer }
          description: Filter by year
      responses:
        "200":
          description: Paginated project update list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectUpdate" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectUpdate
      summary: Create project update
      tags: [Project Updates]
      description: Requires `project_updates:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectUpdateCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectUpdate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-updates/{id}:
    get:
      operationId: getProjectUpdate
      summary: Get project update by ID
      tags: [Project Updates]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project update detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectUpdate" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectUpdate
      summary: Update project update
      tags: [Project Updates]
      description: Requires `project_updates:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectUpdateUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectUpdate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

    delete:
      operationId: deleteProjectUpdate
      summary: Delete project update
      tags: [Project Updates]
      description: Requires `project_updates:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Cost Snapshots ──
  /project-cost-snapshots:
    get:
      operationId: listProjectCostSnapshots
      summary: List project cost snapshots
      tags: [Project Cost Snapshots]
      description: |
        Returns a paginated list of historical cost snapshots for projects. Requires `project_cost_snapshots:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
      responses:
        "200":
          description: Paginated cost snapshot list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectCostSnapshot" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectCostSnapshot
      summary: Create cost snapshot
      tags: [Project Cost Snapshots]
      description: Requires `project_cost_snapshots:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectCostSnapshotCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectCostSnapshot" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-cost-snapshots/{id}:
    get:
      operationId: getProjectCostSnapshot
      summary: Get cost snapshot by ID
      tags: [Project Cost Snapshots]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Cost snapshot detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectCostSnapshot" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectCostSnapshot
      summary: Delete cost snapshot
      tags: [Project Cost Snapshots]
      description: Requires `project_cost_snapshots:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Locations ──
  /project-locations:
    get:
      operationId: listProjectLocations
      summary: List project location assignments
      tags: [Project Locations]
      description: |
        Returns a paginated list of project-location links. Requires `project_locations:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: location_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by location
      responses:
        "200":
          description: Paginated project location list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectLocation" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectLocation
      summary: Link location to project
      tags: [Project Locations]
      description: Requires `project_locations:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectLocationCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectLocation" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-locations/{id}:
    get:
      operationId: getProjectLocation
      summary: Get project location by ID
      tags: [Project Locations]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project location detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectLocation" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectLocation
      summary: Remove location from project
      tags: [Project Locations]
      description: Requires `project_locations:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Sites ──
  /project-sites:
    get:
      operationId: listProjectSites
      summary: List project site assignments
      tags: [Project Sites]
      description: |
        Returns a paginated list of project-site links. Requires `project_sites:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
      responses:
        "200":
          description: Paginated project site list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectSite" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectSite
      summary: Link site to project
      tags: [Project Sites]
      description: Requires `project_sites:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectSiteCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSite" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-sites/{id}:
    get:
      operationId: getProjectSite
      summary: Get project site by ID
      tags: [Project Sites]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project site detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSite" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectSite
      summary: Remove site from project
      tags: [Project Sites]
      description: Requires `project_sites:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Buildings ──
  /project-buildings:
    get:
      operationId: listProjectBuildings
      summary: List project building assignments
      tags: [Project Buildings]
      description: |
        Returns a paginated list of project-building links. Requires `project_buildings:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: building_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by building
      responses:
        "200":
          description: Paginated project building list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectBuilding" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectBuilding
      summary: Link building to project
      tags: [Project Buildings]
      description: Requires `project_buildings:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectBuildingCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectBuilding" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-buildings/{id}:
    get:
      operationId: getProjectBuilding
      summary: Get project building by ID
      tags: [Project Buildings]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project building detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectBuilding" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectBuilding
      summary: Remove building from project
      tags: [Project Buildings]
      description: Requires `project_buildings:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Systems ──
  /project-systems:
    get:
      operationId: listProjectSystems
      summary: List project system assignments
      tags: [Project Systems]
      description: |
        Returns a paginated list of project-system links. Requires `project_systems:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: system_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system
      responses:
        "200":
          description: Paginated project system list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectSystem" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectSystem
      summary: Link system to project
      tags: [Project Systems]
      description: Requires `project_systems:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectSystemCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSystem" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-systems/{id}:
    get:
      operationId: getProjectSystem
      summary: Get project system by ID
      tags: [Project Systems]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project system detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSystem" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectSystem
      summary: Remove system from project
      tags: [Project Systems]
      description: Requires `project_systems:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project System Classes ──
  /project-system-classes:
    get:
      operationId: listProjectSystemClasses
      summary: List project system class assignments
      tags: [Project System Classes]
      description: |
        Returns a paginated list of project-system-class links. Requires `project_system_classes:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: system_class_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system class
      responses:
        "200":
          description: Paginated project system class list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectSystemClass" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectSystemClass
      summary: Link system class to project
      tags: [Project System Classes]
      description: Requires `project_system_classes:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectSystemClassCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSystemClass" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-system-classes/{id}:
    get:
      operationId: getProjectSystemClass
      summary: Get project system class by ID
      tags: [Project System Classes]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project system class detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSystemClass" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectSystemClass
      summary: Remove system class from project
      tags: [Project System Classes]
      description: Requires `project_system_classes:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project System Groups ──
  /project-system-groups:
    get:
      operationId: listProjectSystemGroups
      summary: List project system group assignments
      tags: [Project System Groups]
      description: |
        Returns a paginated list of project-system-group links. Requires `project_system_groups:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: system_group_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system group
      responses:
        "200":
          description: Paginated project system group list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectSystemGroup" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectSystemGroup
      summary: Link system group to project
      tags: [Project System Groups]
      description: Requires `project_system_groups:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectSystemGroupCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSystemGroup" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-system-groups/{id}:
    get:
      operationId: getProjectSystemGroup
      summary: Get project system group by ID
      tags: [Project System Groups]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project system group detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectSystemGroup" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectSystemGroup
      summary: Remove system group from project
      tags: [Project System Groups]
      description: Requires `project_system_groups:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Assets ──
  /project-assets:
    get:
      operationId: listProjectAssets
      summary: List project asset assignments
      tags: [Project Assets]
      description: |
        Returns a paginated list of project-asset links. Requires `project_assets:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset
      responses:
        "200":
          description: Paginated project asset list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectAsset" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectAsset
      summary: Link asset to project
      tags: [Project Assets]
      description: Requires `project_assets:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectAssetCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectAsset" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-assets/{id}:
    get:
      operationId: getProjectAsset
      summary: Get project asset by ID
      tags: [Project Assets]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project asset detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectAsset" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteProjectAsset
      summary: Remove asset from project
      tags: [Project Assets]
      description: Requires `project_assets:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Risks ──
  /project-risks:
    get:
      operationId: listProjectRisks
      summary: List project risks
      tags: [Project Risks]
      description: |
        Returns a paginated list of risks in the project risk register. Requires `project_risks:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project
        - name: status
          in: query
          schema: { type: string, enum: [identified, analyzing, mitigating, resolved, accepted] }
          description: Filter by risk status
        - name: category
          in: query
          schema: { type: string, enum: [technical, financial, schedule, resource, external] }
          description: Filter by risk category
        - name: probability
          in: query
          schema: { type: string, enum: [low, medium, high] }
          description: Filter by probability
        - name: impact
          in: query
          schema: { type: string, enum: [low, medium, high, critical] }
          description: Filter by impact level
      responses:
        "200":
          description: Paginated project risk list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectRisk" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

    post:
      operationId: createProjectRisk
      summary: Create project risk
      tags: [Project Risks]
      description: Requires `project_risks:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectRiskCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectRisk" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-risks/{id}:
    get:
      operationId: getProjectRisk
      summary: Get project risk by ID
      tags: [Project Risks]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project risk detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectRisk" }
        "404": { $ref: "#/components/responses/NotFound" }

    patch:
      operationId: updateProjectRisk
      summary: Update project risk
      tags: [Project Risks]
      description: Requires `project_risks:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectRiskUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectRisk" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectRisk
      summary: Delete project risk
      tags: [Project Risks]
      description: Requires `project_risks:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Compliance Records ──
  /compliance-records:
    get:
      operationId: listComplianceRecords
      summary: List compliance records
      tags: [Compliance Records]
      description: |
        Returns a paginated list of compliance completion records. Requires `compliance_records:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: compliance_item_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by compliance item
        - name: work_order_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work order
      responses:
        "200":
          description: Paginated compliance record list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ComplianceRecord" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createComplianceRecord
      summary: Create compliance record
      tags: [Compliance Records]
      description: Requires `compliance_records:write` scope. Records are also auto-created when work orders linked to compliance items are completed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ComplianceRecordCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ComplianceRecord" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /compliance-records/{id}:
    get:
      operationId: getComplianceRecord
      summary: Get compliance record by ID
      tags: [Compliance Records]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Compliance record detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ComplianceRecord" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateComplianceRecord
      summary: Update compliance record
      tags: [Compliance Records]
      description: Requires `compliance_records:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ComplianceRecordUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ComplianceRecord" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteComplianceRecord
      summary: Delete compliance record
      tags: [Compliance Records]
      description: Requires `compliance_records:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Site FCI History ──
  /site-fci-history:
    get:
      operationId: listSiteFciHistory
      summary: List site FCI history
      tags: [Site FCI History]
      description: |
        Returns a paginated list of Facility Condition Index (FCI) trend history entries per site. Requires `site_fci_history:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
      responses:
        "200":
          description: Paginated site FCI history list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/SiteFciHistory" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /site-fci-history/{id}:
    get:
      operationId: getSiteFciHistoryEntry
      summary: Get site FCI history entry by ID
      tags: [Site FCI History]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Site FCI history entry detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/SiteFciHistory" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Dashboard Snapshots ──
  /dashboard-snapshots:
    get:
      operationId: listDashboardSnapshots
      summary: List dashboard snapshots
      tags: [Dashboard Snapshots]
      description: |
        Returns a paginated list of historical dashboard metric snapshots. Requires `dashboard_snapshots:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: year
          in: query
          schema: { type: integer }
          description: Filter by snapshot year
        - name: month
          in: query
          schema: { type: integer, minimum: 1, maximum: 12 }
          description: Filter by snapshot month (1-12)
      responses:
        "200":
          description: Paginated dashboard snapshot list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/DashboardSnapshot" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /dashboard-snapshots/{id}:
    get:
      operationId: getDashboardSnapshot
      summary: Get dashboard snapshot by ID
      tags: [Dashboard Snapshots]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Dashboard snapshot detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/DashboardSnapshot" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Vendor Site Assignments ──
  /vendor-site-assignments:
    get:
      operationId: listVendorSiteAssignments
      summary: List vendor site assignments
      tags: [Vendor Site Assignments]
      description: |
        Returns a paginated list of vendor-to-site assignments. Requires `vendor_site_assignments:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: vendor_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by vendor
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
      responses:
        "200":
          description: Paginated vendor site assignment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/VendorSiteAssignment" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createVendorSiteAssignment
      summary: Create vendor site assignment
      tags: [Vendor Site Assignments]
      description: Requires `vendor_site_assignments:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VendorSiteAssignmentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/VendorSiteAssignment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /vendor-site-assignments/{id}:
    get:
      operationId: getVendorSiteAssignment
      summary: Get vendor site assignment by ID
      tags: [Vendor Site Assignments]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Vendor site assignment detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/VendorSiteAssignment" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteVendorSiteAssignment
      summary: Delete vendor site assignment
      tags: [Vendor Site Assignments]
      description: Requires `vendor_site_assignments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Contract Sites ──
  /contract-sites:
    get:
      operationId: listContractSites
      summary: List contract sites
      tags: [Contract Sites]
      description: |
        Returns a paginated list of contract-to-site mappings. Requires `contract_sites:read` scope.
        This resource uses a composite key (contract_id + site_id) — no single-item `/{id}` endpoint is available.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: contract_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by contract
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
      responses:
        "200":
          description: Paginated contract site list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ContractSite" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createContractSite
      summary: Create contract-site mapping
      tags: [Contract Sites]
      description: Requires `contract_sites:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContractSiteCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ContractSite" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteContractSite
      summary: Remove contract-site mapping
      tags: [Contract Sites]
      description: Requires `contract_sites:write` scope.
      parameters:
        - name: contract_id
          in: query
          required: true
          schema: { type: string, format: uuid }
          description: Contract UUID
        - name: site_id
          in: query
          required: true
          schema: { type: string, format: uuid }
          description: Site UUID
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Custom Field Definitions ──
  /custom-field-definitions:
    get:
      operationId: listCustomFieldDefinitions
      summary: List custom field definitions
      tags: [Custom Fields]
      description: |
        Returns a paginated list of custom field schema definitions. Requires `custom_fields:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: entity_type
          in: query
          schema: { type: string }
          description: Filter by entity type (e.g. asset, work_order)
        - name: field_type
          in: query
          schema: { type: string, enum: [text, number, date, boolean, select] }
          description: Filter by field type
      responses:
        "200":
          description: Paginated custom field definition list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CustomFieldDefinition" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createCustomFieldDefinition
      summary: Create custom field definition
      tags: [Custom Fields]
      description: Requires `custom_fields:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomFieldDefinitionCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /custom-field-definitions/{id}:
    get:
      operationId: getCustomFieldDefinition
      summary: Get custom field definition by ID
      tags: [Custom Fields]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Custom field definition detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateCustomFieldDefinition
      summary: Update custom field definition
      tags: [Custom Fields]
      description: Requires `custom_fields:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomFieldDefinitionUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteCustomFieldDefinition
      summary: Delete custom field definition
      tags: [Custom Fields]
      description: Requires `custom_fields:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Custom Field Values ──
  /custom-field-values:
    get:
      operationId: listCustomFieldValues
      summary: List custom field values
      tags: [Custom Fields]
      description: |
        Returns a paginated list of custom field values for entities. Requires `custom_fields:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: entity_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by entity (e.g. a specific asset or work order ID)
        - name: field_definition_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by field definition
      responses:
        "200":
          description: Paginated custom field value list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CustomFieldValue" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createCustomFieldValue
      summary: Create custom field value
      tags: [Custom Fields]
      description: Requires `custom_fields:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomFieldValueCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CustomFieldValue" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /custom-field-values/{id}:
    get:
      operationId: getCustomFieldValue
      summary: Get custom field value by ID
      tags: [Custom Fields]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Custom field value detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CustomFieldValue" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateCustomFieldValue
      summary: Update custom field value
      tags: [Custom Fields]
      description: Requires `custom_fields:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomFieldValueUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/CustomFieldValue" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteCustomFieldValue
      summary: Delete custom field value
      tags: [Custom Fields]
      description: Requires `custom_fields:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Part Categories ──
  /part-categories:
    get:
      operationId: listPartCategories
      summary: List part categories
      tags: [Part Categories]
      description: |
        Returns a paginated list of inventory part categories. Requires `part_categories:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
      responses:
        "200":
          description: Paginated part category list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PartCategory" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createPartCategory
      summary: Create part category
      tags: [Part Categories]
      description: Requires `part_categories:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PartCategoryCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PartCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /part-categories/{id}:
    get:
      operationId: getPartCategory
      summary: Get part category by ID
      tags: [Part Categories]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Part category detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PartCategory" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updatePartCategory
      summary: Update part category
      tags: [Part Categories]
      description: Requires `part_categories:write` scope. Immutable fields are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PartCategoryUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PartCategory" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deletePartCategory
      summary: Delete part category
      tags: [Part Categories]
      description: Requires `part_categories:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Upload URLs ──

  /upload-urls:
    post:
      operationId: createUploadUrl
      summary: Create signed upload URL
      tags: [Upload URLs]
      description: |
        Generate a signed upload URL for uploading a file directly to AssetLab storage.

        **Upload flow:**
        1. Call this endpoint to get a `signed_url`
        2. PUT the file binary directly to the `signed_url`
        3. Store the returned `path` on the target record:
           - **Asset image:** bucket `asset-images` → PATCH `/assets/{id}` with `image_url` = `path`
           - **Asset document:** bucket `documents` → POST `/asset-documents` with `file_path` = `path`
           - **Work order image:** bucket `attachments` → PATCH `/work-orders/{id}` with `image_url` = `path`
           - **WO/WR/PM attachment:** bucket `attachments` → POST `/attachments` with `file_url` = `path`
           - **Project document:** bucket `project-documents` → POST `/project-documents` with `file_path` = `path`
           - **Contract document:** bucket `contract-documents` → POST `/contract-documents` with `file_path` = `path`

        AssetLab storage buckets are private. Reads are served as short-lived signed
        URLs, so store the `path` on the record and never a storage URL.

        Requires `upload_urls:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UploadUrlRequest" }
      responses:
        "201":
          description: Signed upload URL created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/UploadUrlResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /upload-files:
    post:
      operationId: uploadFile
      summary: Upload a file inline (base64)
      tags: [Upload URLs]
      description: |
        Upload a file to AssetLab storage by sending its bytes inline as base64.
        The AssetLab backend performs the Supabase Storage upload server-side.

        Use this endpoint when the client cannot (or should not) perform a direct
        HTTP PUT to a signed Supabase Storage URL — for example, MCP integrations
        where outbound network is restricted by host allowlist.

        Limits:
        - Decoded file size ≤ 10 MB (hard server limit)
        - Clients on arg-size-constrained channels (e.g. MCP tool calls) should
          prefer smaller files and fall back to `/upload-urls` for large uploads.

        After a successful upload, store the returned `path` on the target record
        (assets, work orders, attachments, project / contract / asset documents) —
        same mapping as `/upload-urls`.

        Requires `upload_urls:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UploadFileRequest" }
      responses:
        "201":
          description: File uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/UploadFileResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413":
          description: File exceeds 10 MB server limit
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Asset Documents ──

  /asset-documents:
    get:
      operationId: listAssetDocuments
      summary: List asset documents
      tags: [Asset Documents]
      description: |
        Returns a paginated list of asset documents. Requires `asset_documents:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by asset ID
        - name: category
          in: query
          schema: { type: string, enum: [om, commissioning, warranty, installation, specification, other] }
          description: Filter by document category
      responses:
        "200":
          description: Paginated asset document list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetDocument" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAssetDocument
      summary: Create asset document
      tags: [Asset Documents]
      description: Requires `asset_documents:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetDocumentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /asset-documents/{id}:
    get:
      operationId: getAssetDocument
      summary: Get asset document by ID
      tags: [Asset Documents]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset document detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetDocument" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetDocument
      summary: Update asset document
      tags: [Asset Documents]
      description: Requires `asset_documents:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetDocumentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAssetDocument
      summary: Delete asset document
      tags: [Asset Documents]
      description: Requires `asset_documents:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Attachments ──

  /attachments:
    get:
      operationId: listAttachments
      summary: List attachments
      tags: [Attachments]
      description: |
        Returns a paginated list of file attachments. Requires `attachments:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: work_order_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work order ID
        - name: work_request_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by work request ID
        - name: pm_schedule_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by PM schedule ID
        - name: pm_template_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by PM template ID
      responses:
        "200":
          description: Paginated attachment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Attachment" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAttachment
      summary: Create attachment
      tags: [Attachments]
      description: |
        Create a file attachment. Exactly one of `work_order_id`, `work_request_id`, `pm_schedule_id`, or `pm_template_id` must be provided.
        Requires `attachments:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AttachmentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Attachment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /attachments/{id}:
    get:
      operationId: getAttachment
      summary: Get attachment by ID
      tags: [Attachments]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Attachment detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Attachment" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAttachment
      summary: Update attachment
      tags: [Attachments]
      description: Requires `attachments:write` scope. Immutable fields (uploaded_at) are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AttachmentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Attachment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteAttachment
      summary: Delete attachment
      tags: [Attachments]
      description: Requires `attachments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Project Documents ──

  /project-documents:
    get:
      operationId: listProjectDocuments
      summary: List project documents
      tags: [Project Documents]
      description: |
        Returns a paginated list of project documents. Requires `project_documents:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: project_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by project ID
        - name: folder_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by folder ID
      responses:
        "200":
          description: Paginated project document list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectDocument" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createProjectDocument
      summary: Create project document
      tags: [Project Documents]
      description: Requires `project_documents:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectDocumentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /project-documents/{id}:
    get:
      operationId: getProjectDocument
      summary: Get project document by ID
      tags: [Project Documents]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Project document detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectDocument" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateProjectDocument
      summary: Update project document
      tags: [Project Documents]
      description: Requires `project_documents:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectDocumentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteProjectDocument
      summary: Delete project document
      tags: [Project Documents]
      description: Requires `project_documents:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Contract Documents ──

  /contract-documents:
    get:
      operationId: listContractDocuments
      summary: List contract documents
      tags: [Contract Documents]
      description: |
        Returns a paginated list of contract documents. Requires `contract_documents:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: contract_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by contract ID
      responses:
        "200":
          description: Paginated contract document list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ContractDocument" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createContractDocument
      summary: Create contract document
      tags: [Contract Documents]
      description: Requires `contract_documents:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContractDocumentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ContractDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /contract-documents/{id}:
    get:
      operationId: getContractDocument
      summary: Get contract document by ID
      tags: [Contract Documents]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Contract document detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ContractDocument" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateContractDocument
      summary: Update contract document
      tags: [Contract Documents]
      description: Requires `contract_documents:write` scope. Immutable fields (uploaded_at) are silently stripped.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContractDocumentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ContractDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteContractDocument
      summary: Delete contract document
      tags: [Contract Documents]
      description: Requires `contract_documents:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Level of Service: Service Areas ──
  /service-areas:
    get:
      operationId: listServiceAreas
      summary: List service areas
      tags: [Service Areas]
      description: |
        Returns a paginated list of service areas. Requires `service_areas:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: is_active
          in: query
          schema: { type: boolean }
          description: Filter by active status
      responses:
        "200":
          description: Paginated service area list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ServiceArea" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createServiceArea
      summary: Create service area
      tags: [Service Areas]
      description: Requires `service_areas:write` scope. After creating, link system classes and sites via their respective endpoints.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServiceAreaCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ServiceArea" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { description: "Service area with this name already exists" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /service-areas/{id}:
    get:
      operationId: getServiceArea
      summary: Get service area by ID
      tags: [Service Areas]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Service area detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ServiceArea" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateServiceArea
      summary: Update service area
      tags: [Service Areas]
      description: Requires `service_areas:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServiceAreaUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ServiceArea" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: "Service area with this name already exists" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteServiceArea
      summary: Delete service area
      tags: [Service Areas]
      description: |
        Requires `service_areas:write` scope. WARNING: Cascades to all linked measures, measurements, and junction records.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Level of Service: Service Area System Classes ──
  /service-area-system-classes:
    get:
      operationId: listServiceAreaSystemClasses
      summary: List service area system class links
      tags: [Service Area System Classes]
      description: |
        Returns links between service areas and system classes. Requires `service_areas:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: service_area_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by service area
        - name: system_class_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by system class
      responses:
        "200":
          description: Paginated list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ServiceAreaSystemClass" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createServiceAreaSystemClass
      summary: Link system class to service area
      tags: [Service Area System Classes]
      description: Requires `service_areas:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServiceAreaSystemClassCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ServiceAreaSystemClass" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { description: "System class already linked to this service area" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /service-area-system-classes/{id}:
    get:
      operationId: getServiceAreaSystemClass
      summary: Get service area system class link by ID
      tags: [Service Area System Classes]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Link detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ServiceAreaSystemClass" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteServiceAreaSystemClass
      summary: Unlink system class from service area
      tags: [Service Area System Classes]
      description: Requires `service_areas:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Level of Service: Service Area Sites ──
  /service-area-sites:
    get:
      operationId: listServiceAreaSites
      summary: List service area site links
      tags: [Service Area Sites]
      description: |
        Returns links between service areas and sites. Requires `service_areas:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: service_area_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by service area
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site
      responses:
        "200":
          description: Paginated list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ServiceAreaSite" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createServiceAreaSite
      summary: Link site to service area
      tags: [Service Area Sites]
      description: Requires `service_areas:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ServiceAreaSiteCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ServiceAreaSite" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { description: "Site already linked to this service area" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /service-area-sites/{id}:
    get:
      operationId: getServiceAreaSite
      summary: Get service area site link by ID
      tags: [Service Area Sites]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Link detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ServiceAreaSite" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteServiceAreaSite
      summary: Unlink site from service area
      tags: [Service Area Sites]
      description: Requires `service_areas:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Level of Service: Measures ──
  /los-measures:
    get:
      operationId: listLosMeasures
      summary: List LoS measures
      tags: [LoS Measures]
      description: |
        Returns a paginated list of Level of Service measures. Requires `los_measures:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - name: service_area_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by service area
        - name: category
          in: query
          schema: { type: string, enum: [quality, reliability, responsiveness, safety, sustainability, cost_efficiency, capacity] }
          description: Filter by measure category
        - name: type
          in: query
          schema: { type: string, enum: [community, technical] }
          description: Filter by measure type
        - name: data_source
          in: query
          schema: { type: string }
          description: Filter by data source type
        - name: is_active
          in: query
          schema: { type: boolean }
          description: Filter by active status
      responses:
        "200":
          description: Paginated LoS measure list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LosMeasure" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createLosMeasure
      summary: Create LoS measure
      tags: [LoS Measures]
      description: Requires `los_measures:write` scope. The service_area_id must reference an existing service area.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LosMeasureCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LosMeasure" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { description: "Measure with this name already exists in this service area" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /los-measures/{id}:
    get:
      operationId: getLosMeasure
      summary: Get LoS measure by ID
      tags: [LoS Measures]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: LoS measure detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LosMeasure" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateLosMeasure
      summary: Update LoS measure
      tags: [LoS Measures]
      description: Requires `los_measures:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LosMeasureUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LosMeasure" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: "Measure with this name already exists in this service area" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteLosMeasure
      summary: Delete LoS measure
      tags: [LoS Measures]
      description: |
        Requires `los_measures:write` scope. WARNING: Also deletes all associated measurements and targets history.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Level of Service: Measurements ──
  /los-measurements:
    get:
      operationId: listLosMeasurements
      summary: List LoS measurements
      tags: [LoS Measurements]
      description: |
        Returns a paginated list of LoS measurement values (time-series). Requires `los_measurements:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: los_measure_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by LoS measure
        - name: period_type
          in: query
          schema: { type: string, enum: [monthly, quarterly, semi_annual, annual] }
          description: Filter by period type
        - name: date_from
          in: query
          schema: { type: string, format: date }
          description: Filter measurements from this date (inclusive)
        - name: date_to
          in: query
          schema: { type: string, format: date }
          description: Filter measurements up to this date (inclusive)
        - name: is_auto
          in: query
          schema: { type: boolean }
          description: Filter by auto-calculated (true) or manual (false)
      responses:
        "200":
          description: Paginated LoS measurement list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LosMeasurement" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createLosMeasurement
      summary: Record LoS measurement
      tags: [LoS Measurements]
      description: Requires `los_measurements:write` scope. The los_measure_id must reference an existing LoS measure.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LosMeasurementCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LosMeasurement" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { description: "Measurement already exists for this measure and period start date" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /los-measurements/{id}:
    get:
      operationId: getLosMeasurement
      summary: Get LoS measurement by ID
      tags: [LoS Measurements]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: LoS measurement detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LosMeasurement" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateLosMeasurement
      summary: Update LoS measurement
      tags: [LoS Measurements]
      description: Requires `los_measurements:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LosMeasurementUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LosMeasurement" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteLosMeasurement
      summary: Delete LoS measurement
      tags: [LoS Measurements]
      description: Requires `los_measurements:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Level of Service: Targets History ──
  /los-targets-history:
    get:
      operationId: listLosTargetsHistory
      summary: List LoS targets history
      tags: [LoS Targets History]
      description: |
        Returns target change audit trail for LoS measures. Requires `los_measures:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: los_measure_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by LoS measure
      responses:
        "200":
          description: Paginated targets history list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LosTargetsHistory" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /los-targets-history/{id}:
    get:
      operationId: getLosTargetsHistoryEntry
      summary: Get LoS targets history entry by ID
      tags: [LoS Targets History]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Targets history entry detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/LosTargetsHistory" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Floorplans (enterprise++) ──
  /floorplans:
    get:
      operationId: listFloorplans
      summary: List floorplans
      tags: [Floorplans]
      description: Returns a paginated list of floorplan pages. Filter by `building_id` for a building's floors, or by `site_id` for site-level plans. Requires `floorplans:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: building_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by building (building-scoped floorplans)
        - name: site_id
          in: query
          schema: { type: string, format: uuid }
          description: Filter by site (site-scoped floorplans only)
        - name: status
          in: query
          schema: { type: string, enum: [pending, detecting, ready, failed] }
          description: Filter by detection status
      responses:
        "200":
          description: Paginated floorplan list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Floorplan" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createFloorplan
      summary: Create floorplan row
      tags: [Floorplans]
      description: Registers one floor (PDF page) in the database. Does not upload the PDF itself — that happens via Supabase Storage. Requires `floorplans:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FloorplanCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Floorplan" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /floorplans/{id}:
    get:
      operationId: getFloorplan
      summary: Get floorplan by ID
      tags: [Floorplans]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Floorplan detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Floorplan" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateFloorplan
      summary: Update floorplan
      tags: [Floorplans]
      description: Requires `floorplans:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FloorplanUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Floorplan" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteFloorplan
      summary: Delete floorplan
      tags: [Floorplans]
      description: Requires `floorplans:write` scope. Cascades to regions and asset placements on this floor.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /floorplan-regions:
    get:
      operationId: listFloorplanRegions
      summary: List floorplan regions
      tags: [Floorplans]
      description: Labeled rooms/zones on a floorplan. Requires `floorplan_regions:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: floorplan_id
          in: query
          schema: { type: string, format: uuid }
        - name: location_id
          in: query
          schema: { type: string, format: uuid }
        - name: reviewed
          in: query
          schema: { type: boolean }
      responses:
        "200":
          description: Paginated region list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/FloorplanRegion" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createFloorplanRegion
      summary: Create floorplan region
      tags: [Floorplans]
      description: Requires `floorplan_regions:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FloorplanRegionCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FloorplanRegion" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /floorplan-regions/{id}:
    get:
      operationId: getFloorplanRegion
      summary: Get floorplan region by ID
      tags: [Floorplans]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Region detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FloorplanRegion" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateFloorplanRegion
      summary: Update floorplan region
      tags: [Floorplans]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FloorplanRegionUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/FloorplanRegion" }
    delete:
      operationId: deleteFloorplanRegion
      summary: Delete floorplan region
      tags: [Floorplans]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }

  /asset-placements:
    get:
      operationId: listAssetPlacements
      summary: List asset placements
      tags: [Floorplans]
      description: Asset pin locations on floorplans (one per asset globally). Requires `asset_placements:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: asset_id
          in: query
          schema: { type: string, format: uuid }
        - name: floorplan_id
          in: query
          schema: { type: string, format: uuid }
        - name: region_id
          in: query
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Paginated placement list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetPlacement" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      operationId: createAssetPlacement
      summary: Place asset on floorplan (upsert by asset_id)
      tags: [Floorplans]
      description: |
        Places an asset at a given (x, y) on a floorplan. UPSERTS by asset_id — calling with an existing asset_id moves the pin instead of creating a duplicate. Use `bulk_create` with `resource=asset-placements` for batch placement. Requires `asset_placements:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetPlacementCreate" }
      responses:
        "201":
          description: Created or updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetPlacement" }

  /asset-placements/{id}:
    get:
      operationId: getAssetPlacement
      summary: Get asset placement by ID
      tags: [Floorplans]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Placement detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetPlacement" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetPlacement
      summary: Update asset placement
      tags: [Floorplans]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetPlacementUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetPlacement" }
    delete:
      operationId: deleteAssetPlacement
      summary: Delete asset placement
      tags: [Floorplans]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }

  # ── Users (read-only) ──
  /users:
    get:
      operationId: listUsers
      summary: List organization members
      tags: [Users]
      description: |
        Returns a paginated list of organization members with names, emails, and roles.
        Requires `users:read` scope. This endpoint exposes personal information (PII).
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated user list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/User" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /users/{id}:
    get:
      operationId: getUser
      summary: Get organization member
      tags: [Users]
      description: |
        Returns details for a single organization member. Requires `users:read` scope.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: User ID (Clerk user ID)
      responses:
        "200":
          description: User details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/User" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Bulk Operations ──
  # Bulk endpoints are available for all writable resources at /{resource}/bulk.
  # POST = bulk create, PATCH = bulk update. Max 100 items per request.
  # Each item is processed independently — partial success returns HTTP 207.
  # Counts as 1 request against the rate limit.

  /assets/bulk:
    post:
      operationId: bulkCreateAssets
      summary: Bulk create assets (max 100)
      tags: [Assets]
      description: |
        Create up to 100 assets in a single request. Each item is processed independently.
        Returns per-item results. Requires `assets:write` scope. Counts as 1 rate-limit request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              minItems: 1
              maxItems: 100
              items: { $ref: "#/components/schemas/AssetCreate" }
      responses:
        "201":
          description: All items created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BulkResponse" }
        "207": { $ref: "#/components/responses/MultiStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      operationId: bulkUpdateAssets
      summary: Bulk update assets (max 100)
      tags: [Assets]
      description: |
        Update up to 100 assets in a single request. Each item must include an `id` field.
        Returns per-item results. Requires `assets:write` scope. Counts as 1 rate-limit request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              minItems: 1
              maxItems: 100
              items:
                type: object
                required: [id]
                properties:
                  id: { type: string, format: uuid }
      responses:
        "200":
          description: All items updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BulkResponse" }
        "207": { $ref: "#/components/responses/MultiStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /work-orders/bulk:
    post:
      operationId: bulkCreateWorkOrders
      summary: Bulk create work orders (max 100)
      tags: [Work Orders]
      description: |
        Create up to 100 work orders in a single request. Requires `work_orders:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              minItems: 1
              maxItems: 100
              items: { $ref: "#/components/schemas/WorkOrderCreate" }
      responses:
        "201":
          description: All items created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BulkResponse" }
        "207": { $ref: "#/components/responses/MultiStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      operationId: bulkUpdateWorkOrders
      summary: Bulk update work orders (max 100)
      tags: [Work Orders]
      description: |
        Update up to 100 work orders in a single request. Each item must include an `id` field.
        Requires `work_orders:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              minItems: 1
              maxItems: 100
              items:
                type: object
                required: [id]
                properties:
                  id: { type: string, format: uuid }
      responses:
        "200":
          description: All items updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BulkResponse" }
        "207": { $ref: "#/components/responses/MultiStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /{resource}/bulk:
    post:
      operationId: bulkCreate
      summary: Bulk create any resource (max 100)
      tags: [Bulk Operations]
      description: |
        Generic bulk create endpoint. Replace `{resource}` with any writable resource
        (e.g. `sites`, `buildings`, `vendors`, `parts`, `pm-schedules`, etc.).
        Each item is processed independently — one failure does not affect others.
        Requires `{resource}:write` scope. Counts as 1 rate-limit request.
      parameters:
        - name: resource
          in: path
          required: true
          schema: { type: string }
          description: "Resource name (e.g. sites, buildings, vendors, parts)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              minItems: 1
              maxItems: 100
              items: { type: object }
      responses:
        "201":
          description: All items created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BulkResponse" }
        "207": { $ref: "#/components/responses/MultiStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      operationId: bulkUpdate
      summary: Bulk update any resource (max 100)
      tags: [Bulk Operations]
      description: |
        Generic bulk update endpoint. Replace `{resource}` with any writable resource.
        Each item must include an `id` field (UUID). Each item is processed independently.
        Requires `{resource}:write` scope. Counts as 1 rate-limit request.
      parameters:
        - name: resource
          in: path
          required: true
          schema: { type: string }
          description: "Resource name (e.g. assets, work-orders, sites)"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              minItems: 1
              maxItems: 100
              items:
                type: object
                required: [id]
                properties:
                  id: { type: string, format: uuid }
      responses:
        "200":
          description: All items updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BulkResponse" }
        "207": { $ref: "#/components/responses/MultiStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Infrastructure Feature Classes ──
  /infrastructure-feature-classes:
    get:
      operationId: listInfrastructureFeatureClasses
      summary: List infrastructure feature classes
      tags: [Infrastructure Feature Classes]
      description: |
        Returns a paginated list of infrastructure feature classes. Requires
        `infrastructure_feature_classes:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - in: query
          name: category
          schema:
            type: string
            enum: [transportation, water, wastewater, stormwater, structures, electrical, telecom, gas, roadside, other]
        - in: query
          name: is_builtin
          schema: { type: string, enum: ["true", "false"] }
      responses:
        "200":
          description: Paginated feature class list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureFeatureClass" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createInfrastructureFeatureClass
      summary: Create infrastructure feature class (tenant-defined)
      tags: [Infrastructure Feature Classes]
      description: |
        Requires `infrastructure_feature_classes:write` scope. `is_builtin` is forced to false.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureFeatureClassCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureFeatureClass" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409":
          description: Duplicate code for tenant
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /infrastructure-feature-classes/{code}:
    get:
      operationId: getInfrastructureFeatureClass
      summary: Get infrastructure feature class by code
      tags: [Infrastructure Feature Classes]
      parameters:
        - in: path
          name: code
          required: true
          schema: { type: string, pattern: "^[a-z][a-z0-9_]{0,49}$" }
      responses:
        "200":
          description: Asset class detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureFeatureClass" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureFeatureClass
      summary: Update infrastructure feature class
      tags: [Infrastructure Feature Classes]
      description: |
        `code`, `category`, and `is_builtin` are immutable; updates to builtin classes
        return 409. Requires `infrastructure_feature_classes:write` scope.
      parameters:
        - in: path
          name: code
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureFeatureClassUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureFeatureClass" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Builtin class protection violated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
    delete:
      operationId: deleteInfrastructureFeatureClass
      summary: Delete infrastructure feature class
      tags: [Infrastructure Feature Classes]
      description: |
        Builtin classes cannot be deleted. Classes referenced by any network are protected
        by FK and return 409.
      parameters:
        - in: path
          name: code
          required: true
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Builtin or referenced class cannot be deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  # ── Infrastructure Networks ──
  /infrastructure-networks:
    get:
      operationId: listInfrastructureNetworks
      summary: List infrastructure networks
      tags: [Infrastructure Networks]
      description: Requires `infrastructure_networks:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - in: query
          name: feature_class
          schema: { type: string, pattern: "^[a-z][a-z0-9_]{0,49}$" }
      responses:
        "200":
          description: Paginated network list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureNetwork" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureNetwork
      summary: Create infrastructure network
      tags: [Infrastructure Networks]
      description: Requires `infrastructure_networks:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureNetworkCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureNetwork" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /infrastructure-networks/{id}:
    get:
      operationId: getInfrastructureNetwork
      summary: Get infrastructure network by ID
      tags: [Infrastructure Networks]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Network detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureNetwork" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureNetwork
      summary: Update infrastructure network
      tags: [Infrastructure Networks]
      description: Requires `infrastructure_networks:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureNetworkUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureNetwork" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureNetwork
      summary: Delete infrastructure network
      tags: [Infrastructure Networks]
      description: |
        Cascades to all features in the network. Requires `infrastructure_networks:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Infrastructure Assets (Features) ──
  /infrastructure-assets:
    get:
      operationId: listInfrastructureAssets
      summary: List infrastructure assets (features)
      tags: [Infrastructure Assets]
      description: |
        Returns features (segments and nodes) with geometry as GeoJSON in EPSG:4326.
        Requires `infrastructure_assets:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - $ref: "#/components/parameters/search"
        - in: query
          name: network_id
          schema: { type: string, format: uuid }
        - in: query
          name: feature_type
          schema: { type: string, enum: [segment, node] }
        - in: query
          name: site_id
          schema: { type: string, format: uuid }
        - in: query
          name: status_id
          schema: { type: string }
        - in: query
          name: asset_type_id
          schema: { type: string, format: uuid }
        - in: query
          name: condition_min
          schema: { type: number }
          description: Minimum condition score (0-100)
        - in: query
          name: condition_max
          schema: { type: number }
          description: Maximum condition score (0-100)
        - in: query
          name: risk_score_min
          schema: { type: number }
          description: Minimum risk score
        - in: query
          name: risk_score_max
          schema: { type: number }
          description: Maximum risk score
        - in: query
          name: include_deleted
          schema: { type: boolean }
          description: Include soft-deleted features (default false)
      responses:
        "200":
          description: Paginated feature list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureAsset" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureAsset
      summary: Create infrastructure asset (feature)
      tags: [Infrastructure Assets]
      description: |
        `geometry` must be GeoJSON matching `feature_type`. Requires
        `infrastructure_assets:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAsset" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /infrastructure-assets/{id}:
    get:
      operationId: getInfrastructureAsset
      summary: Get infrastructure asset by ID
      tags: [Infrastructure Assets]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Feature detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAsset" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureAsset
      summary: Update infrastructure asset
      tags: [Infrastructure Assets]
      description: Requires `infrastructure_assets:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAsset" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureAsset
      summary: Soft-delete infrastructure asset
      tags: [Infrastructure Assets]
      description: Sets `deleted_at`. Requires `infrastructure_assets:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Asset Condition Assessments ──
  /asset-condition-assessments:
    get:
      operationId: listAssetConditionAssessments
      summary: List asset condition assessments
      tags: [Asset Condition Assessments]
      description: Requires `asset_condition_assessments:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - in: query
          name: asset_id
          schema: { type: string, format: uuid }
        - in: query
          name: assessor_id
          schema: { type: string }
        - in: query
          name: method
          schema: { type: string, enum: [visual, detailed, vendor] }
          description: Filter by assessment method
        - in: query
          name: assessed_on_from
          schema: { type: string, format: date }
          description: Assessments on/after this date
        - in: query
          name: assessed_on_to
          schema: { type: string, format: date }
          description: Assessments on/before this date
        - in: query
          name: condition_min
          schema: { type: number }
          description: Minimum condition score (0-100)
        - in: query
          name: condition_max
          schema: { type: number }
          description: Maximum condition score (0-100)
      responses:
        "200":
          description: Paginated assessment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AssetConditionAssessment" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createAssetConditionAssessment
      summary: Create asset condition assessment
      tags: [Asset Condition Assessments]
      description: "Requires `asset_condition_assessments:write` scope. Set `update_purchase_cost: true` to overwrite the asset's purchase cost with the CRV (create-time side-effect)."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetConditionAssessmentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetConditionAssessment" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /asset-condition-assessments/{id}:
    get:
      operationId: getAssetConditionAssessment
      summary: Get asset condition assessment by ID
      tags: [Asset Condition Assessments]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Assessment detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetConditionAssessment" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateAssetConditionAssessment
      summary: Update asset condition assessment
      tags: [Asset Condition Assessments]
      description: Requires `asset_condition_assessments:write` scope. The purchase-cost writeback is create-only and cannot be triggered via update.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssetConditionAssessmentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/AssetConditionAssessment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteAssetConditionAssessment
      summary: Soft-delete asset condition assessment
      tags: [Asset Condition Assessments]
      description: Requires `asset_condition_assessments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Infrastructure Asset Inspections ──
  /infrastructure-asset-inspections:
    get:
      operationId: listInfrastructureAssetInspections
      summary: List infrastructure asset inspections
      tags: [Infrastructure Asset Inspections]
      description: Requires `infrastructure_asset_inspections:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - in: query
          name: feature_id
          schema: { type: string, format: uuid }
        - in: query
          name: inspector_id
          schema: { type: string }
        - in: query
          name: method
          schema: { type: string }
          description: Filter by inspection method (e.g. CCTV, visual)
        - in: query
          name: inspection_date_from
          schema: { type: string, format: date }
          description: Inspections on/after this date
        - in: query
          name: inspection_date_to
          schema: { type: string, format: date }
          description: Inspections on/before this date
        - in: query
          name: condition_min
          schema: { type: number }
          description: Minimum condition score (0-100)
        - in: query
          name: condition_max
          schema: { type: number }
          description: Maximum condition score (0-100)
      responses:
        "200":
          description: Paginated inspection list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureAssetInspection" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureAssetInspection
      summary: Create infrastructure asset inspection
      tags: [Infrastructure Asset Inspections]
      description: Requires `infrastructure_asset_inspections:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetInspectionCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetInspection" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /infrastructure-asset-inspections/{id}:
    get:
      operationId: getInfrastructureAssetInspection
      summary: Get infrastructure asset inspection by ID
      tags: [Infrastructure Asset Inspections]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Inspection detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetInspection" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureAssetInspection
      summary: Update infrastructure asset inspection
      tags: [Infrastructure Asset Inspections]
      description: Requires `infrastructure_asset_inspections:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetInspectionUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetInspection" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureAssetInspection
      summary: Soft-delete infrastructure asset inspection
      tags: [Infrastructure Asset Inspections]
      description: Requires `infrastructure_asset_inspections:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Phase 4.5b — Infrastructure attached records ──

  /infrastructure-asset-costs:
    get:
      operationId: listInfrastructureAssetCosts
      summary: List infrastructure asset costs
      tags: [Infrastructure Asset Costs]
      description: Requires `infrastructure_asset_costs:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - { in: query, name: feature_id, schema: { type: string, format: uuid } }
        - { in: query, name: work_order_id, schema: { type: string, format: uuid } }
        - { in: query, name: category, schema: { type: string, enum: [Repair, PM, Operation, Replacement, Decommission, Other] } }
        - { in: query, name: cost_date_from, schema: { type: string, format: date } }
        - { in: query, name: cost_date_to, schema: { type: string, format: date } }
      responses:
        "200":
          description: Paginated cost list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureAssetCost" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureAssetCost
      summary: Create infrastructure asset cost
      tags: [Infrastructure Asset Costs]
      description: Requires `infrastructure_asset_costs:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetCostCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetCost" }
        "400": { $ref: "#/components/responses/BadRequest" }
  /infrastructure-asset-costs/{id}:
    get:
      operationId: getInfrastructureAssetCost
      summary: Get infrastructure asset cost by ID
      tags: [Infrastructure Asset Costs]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Cost detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetCost" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureAssetCost
      summary: Update infrastructure asset cost
      tags: [Infrastructure Asset Costs]
      description: Requires `infrastructure_asset_costs:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetCostUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetCost" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureAssetCost
      summary: Delete infrastructure asset cost
      tags: [Infrastructure Asset Costs]
      description: Requires `infrastructure_asset_costs:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /infrastructure-asset-parts:
    get:
      operationId: listInfrastructureAssetParts
      summary: List infrastructure asset parts
      tags: [Infrastructure Asset Parts]
      description: Requires `infrastructure_asset_parts:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - { in: query, name: feature_id, schema: { type: string, format: uuid } }
        - { in: query, name: part_id, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Paginated part list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureAssetPart" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureAssetPart
      summary: Create infrastructure asset part association
      tags: [Infrastructure Asset Parts]
      description: Requires `infrastructure_asset_parts:write` scope. The (feature_id, part_id) pair is unique.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetPartCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetPart" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "409": { $ref: "#/components/responses/Conflict" }
  /infrastructure-asset-parts/{id}:
    get:
      operationId: getInfrastructureAssetPart
      summary: Get infrastructure asset part by ID
      tags: [Infrastructure Asset Parts]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Part detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetPart" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureAssetPart
      summary: Update infrastructure asset part
      tags: [Infrastructure Asset Parts]
      description: Requires `infrastructure_asset_parts:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetPartUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetPart" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureAssetPart
      summary: Delete infrastructure asset part
      tags: [Infrastructure Asset Parts]
      description: Requires `infrastructure_asset_parts:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /infrastructure-asset-documents:
    get:
      operationId: listInfrastructureAssetDocuments
      summary: List infrastructure asset documents
      tags: [Infrastructure Asset Documents]
      description: Requires `infrastructure_asset_documents:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - { in: query, name: feature_id, schema: { type: string, format: uuid } }
        - { in: query, name: category, schema: { type: string } }
        - { in: query, name: search, schema: { type: string } }
      responses:
        "200":
          description: Paginated document list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureAssetDocument" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureAssetDocument
      summary: Create infrastructure asset document
      tags: [Infrastructure Asset Documents]
      description: Requires `infrastructure_asset_documents:write` scope. Upload bytes via the upload-url endpoint first, then POST the metadata here.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetDocumentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
  /infrastructure-asset-documents/{id}:
    get:
      operationId: getInfrastructureAssetDocument
      summary: Get infrastructure asset document by ID
      tags: [Infrastructure Asset Documents]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Document detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetDocument" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureAssetDocument
      summary: Update infrastructure asset document
      tags: [Infrastructure Asset Documents]
      description: Requires `infrastructure_asset_documents:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetDocumentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureAssetDocument
      summary: Delete infrastructure asset document
      tags: [Infrastructure Asset Documents]
      description: Requires `infrastructure_asset_documents:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /infrastructure-asset-comments:
    get:
      operationId: listInfrastructureAssetComments
      summary: List infrastructure asset comments
      tags: [Infrastructure Asset Comments]
      description: Requires `infrastructure_asset_comments:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - { in: query, name: feature_id, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Paginated comment list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureAssetComment" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureAssetComment
      summary: Create infrastructure asset comment
      tags: [Infrastructure Asset Comments]
      description: Requires `infrastructure_asset_comments:write` scope. user_id is assigned from the API key; any client-supplied user_id is ignored.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetCommentCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
  /infrastructure-asset-comments/{id}:
    get:
      operationId: getInfrastructureAssetComment
      summary: Get infrastructure asset comment by ID
      tags: [Infrastructure Asset Comments]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Comment detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetComment" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureAssetComment
      summary: Update infrastructure asset comment
      tags: [Infrastructure Asset Comments]
      description: Requires `infrastructure_asset_comments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureAssetCommentUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureAssetComment
      summary: Delete infrastructure asset comment
      tags: [Infrastructure Asset Comments]
      description: Requires `infrastructure_asset_comments:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /infrastructure-zones:
    get:
      operationId: listInfrastructureZones
      summary: List infrastructure zones
      tags: [Infrastructure Zones]
      description: Requires `infrastructure_zones:read` scope. Boundary is returned as a GeoJSON Polygon.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - { in: query, name: network_id, schema: { type: string, format: uuid } }
        - { in: query, name: kind, schema: { type: string, enum: [pressure_zone, dma, sewershed, storm_catchment, maintenance_district] } }
      responses:
        "200":
          description: Paginated zone list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureZone" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createInfrastructureZone
      summary: Create infrastructure zone
      tags: [Infrastructure Zones]
      description: Requires `infrastructure_zones:write` scope. boundary is a GeoJSON Polygon (MultiPolygon rejected). (network_id, name) is unique.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureZoneCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureZone" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "409": { $ref: "#/components/responses/Conflict" }
  /infrastructure-zones/{id}:
    get:
      operationId: getInfrastructureZone
      summary: Get infrastructure zone by ID
      tags: [Infrastructure Zones]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Zone detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureZone" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateInfrastructureZone
      summary: Update infrastructure zone
      tags: [Infrastructure Zones]
      description: Requires `infrastructure_zones:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InfrastructureZoneUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureZone" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteInfrastructureZone
      summary: Delete infrastructure zone
      tags: [Infrastructure Zones]
      description: Requires `infrastructure_zones:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /project-infrastructure-assets:
    get:
      operationId: listProjectInfrastructureAssets
      summary: List project ↔ infrastructure feature links
      tags: [Project Infrastructure Assets]
      description: Requires `project_infrastructure_assets:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - { in: query, name: project_id, schema: { type: string, format: uuid } }
        - { in: query, name: feature_id, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Paginated link list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectInfrastructureAsset" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createProjectInfrastructureAsset
      summary: Link a project to an infrastructure feature
      tags: [Project Infrastructure Assets]
      description: Requires `project_infrastructure_assets:write` scope. The (project_id, feature_id) pair is unique.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectInfrastructureAssetCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectInfrastructureAsset" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "409": { $ref: "#/components/responses/Conflict" }
  /project-infrastructure-assets/{id}:
    get:
      operationId: getProjectInfrastructureAsset
      summary: Get project ↔ infrastructure feature link by ID
      tags: [Project Infrastructure Assets]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Link detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectInfrastructureAsset" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      operationId: updateProjectInfrastructureAsset
      summary: Update project ↔ infrastructure feature link
      tags: [Project Infrastructure Assets]
      description: Requires `project_infrastructure_assets:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProjectInfrastructureAssetUpdate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ProjectInfrastructureAsset" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      operationId: deleteProjectInfrastructureAsset
      summary: Delete project ↔ infrastructure feature link
      tags: [Project Infrastructure Assets]
      description: Requires `project_infrastructure_assets:write` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200": { $ref: "#/components/responses/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /infrastructure-asset-risk-history:
    get:
      operationId: listInfrastructureAssetRiskHistory
      summary: List infrastructure asset risk history
      tags: [Infrastructure Asset Risk History]
      description: Read-only — entries are trigger-populated. Requires `infrastructure_asset_risk_history:read` scope.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - { in: query, name: feature_id, schema: { type: string, format: uuid } }
        - { in: query, name: source, schema: { type: string } }
        - { in: query, name: captured_at_from, schema: { type: string, format: date } }
        - { in: query, name: captured_at_to, schema: { type: string, format: date } }
      responses:
        "200":
          description: Paginated risk history list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InfrastructureAssetRiskHistoryEntry" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /infrastructure-asset-risk-history/{id}:
    get:
      operationId: getInfrastructureAssetRiskHistoryEntry
      summary: Get infrastructure asset risk history entry by ID
      tags: [Infrastructure Asset Risk History]
      description: Read-only. Requires `infrastructure_asset_risk_history:read` scope.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Risk history entry detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/InfrastructureAssetRiskHistoryEntry" }
        "404": { $ref: "#/components/responses/NotFound" }

# ──────────────────────────────────────────────────────────────
# Webhooks (outbound delivery payloads)
# ──────────────────────────────────────────────────────────────

webhooks:
  workOrderCreated:
    post:
      operationId: onWorkOrderCreated
      summary: Work order created
      tags: [Webhooks]
      description: Fired when a new work order is created.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
  workOrderUpdated:
    post:
      operationId: onWorkOrderUpdated
      summary: Work order updated
      tags: [Webhooks]
      description: Fired when a work order is modified.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
  workOrderDeleted:
    post:
      operationId: onWorkOrderDeleted
      summary: Work order deleted
      tags: [Webhooks]
      description: Fired when a work order is deleted.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
  assetCreated:
    post:
      operationId: onAssetCreated
      summary: Asset created
      tags: [Webhooks]
      description: Fired when a new asset is created.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
  assetUpdated:
    post:
      operationId: onAssetUpdated
      summary: Asset updated
      tags: [Webhooks]
      description: Fired when an asset is modified.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
  assetDeleted:
    post:
      operationId: onAssetDeleted
      summary: Asset deleted
      tags: [Webhooks]
      description: Fired when an asset is deleted.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
  workRequestCreated:
    post:
      operationId: onWorkRequestCreated
      summary: Work request created
      tags: [Webhooks]
      description: Fired when a new work request is submitted.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
  workRequestUpdated:
    post:
      operationId: onWorkRequestUpdated
      summary: Work request updated
      tags: [Webhooks]
      description: Fired when a work request is modified.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookDeliveryPayload" }
      responses:
        "200": { description: "Return any 2xx to acknowledge delivery" }
