openapi: 3.0.3
info:
  title: ODEI World Model API
  version: 2.0.0
  description: |
    Public API for the ODEI World Model — a live knowledge graph representing the
    ODEI Symbiosis partnership's goals, strategy, execution, and constitutional layer.

    ## Authentication

    Two authentication schemes are supported depending on your tier:

    - **Bearer (JWT)** — issued on sign-up, for free and basic tiers
    - **API Key** — `X-Api-Key` header, for pro and enterprise tiers

    Include the credential in every request:

    ```
    Authorization: Bearer <jwt>
    # or
    X-Api-Key: <key>
    ```

    Unauthenticated requests to world-model and guardrail routes return `401`.
    The `GET /health` and `GET /schema` endpoints are public (no auth required).

    ## Pricing Tiers

    | Tier       | Price     | Quota              | Auth method       |
    |------------|-----------|--------------------|--------------------|
    | Free       | $0        | 100 queries/day    | Bearer JWT         |
    | Basic      | $9/month  | 1,000 queries/day  | Bearer JWT         |
    | Pro        | $49/month | Unlimited          | API Key            |
    | Enterprise | Custom    | Unlimited + SLA    | API Key + dedicated|

    Quota applies to `POST /world-model/query` and `POST /guardrail/check`.
    `GET /world-model/live` counts as 1 query per call under free/basic tiers.

    ## Rate Limits

    All tiers: 20 requests/minute per IP (hard limit, returns `429`).
    Daily quota resets at 00:00 UTC.

    ## Response Envelope

    Every response uses a consistent envelope:

    ```json
    { "ok": true, "data": { ... } }
    { "ok": false, "error": "Human-readable message", "code": "ERROR_CODE" }
    ```

  contact:
    name: ODEI API Support
    url: https://api.odei.ai
  license:
    name: Proprietary
    url: https://api.odei.ai/terms

servers:
  - url: https://api.odei.ai/api/v2
    description: Production

security:
  - BearerAuth: []
  - ApiKeyAuth: []

tags:
  - name: world-model
    description: Live knowledge graph endpoints
  - name: guardrail
    description: Constitutional validation layer
  - name: system
    description: Health and schema discovery

paths:

  # ─────────────────────────────────────────────────────────────────────────────
  # GET /world-model/live
  # ─────────────────────────────────────────────────────────────────────────────
  /world-model/live:
    get:
      operationId: getWorldModelLive
      tags: [world-model]
      summary: Full graph snapshot
      description: |
        Returns the complete sanitized world-model graph — all nodes and edges
        across all six domains (FOUNDATION, VISION, STRATEGY, TACTICS, EXECUTION,
        TRACK). Sensitive fields (passwords, private keys, internal IDs) are stripped.

        Cached for 60 seconds server-side. Use `Cache-Control: no-cache` to force
        a fresh fetch (counts as an additional query against your quota).

        Free/Basic tiers: counts as 1 query.
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - name: domains
          in: query
          required: false
          description: |
            Comma-separated list of domains to include. If omitted, all domains
            are returned.
          schema:
            type: string
            example: FOUNDATION,VISION
        - name: types
          in: query
          required: false
          description: |
            Comma-separated list of node types to include (e.g. Goal,Guardrail).
            Filters both nodes and their incident edges.
          schema:
            type: string
            example: Goal,Guardrail,Value
        - name: include_edges
          in: query
          required: false
          description: Whether to include edges in the response. Default true.
          schema:
            type: boolean
            default: true
      responses:
        "200":
          description: Graph snapshot returned successfully
          headers:
            X-Quota-Remaining:
              description: Queries remaining today (free/basic only)
              schema:
                type: integer
            X-Cache:
              description: HIT or MISS
              schema:
                type: string
                enum: [HIT, MISS]
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorldModelLiveResponse"
              example:
                ok: true
                data:
                  kind: public_projection
                  generatedAt: "2026-02-22T13:31:43.493Z"
                  timestamp: "2026-02-22T14:00:00.000Z"
                  summary:
                    nodes: 91
                    edges: 91
                    domains:
                      FOUNDATION: 25
                      VISION: 12
                      STRATEGY: 16
                      TACTICS: 8
                      EXECUTION: 11
                      TRACK: 19
                  nodes:
                    - id: "n_37db95b4"
                      title: "Evening Critic"
                      type: "Guardrail"
                      domain: "FOUNDATION"
                      labels: ["Guardrail", "Entity", "OdeiNode"]
                  edges:
                    - id: "e_abc123"
                      type: "ENFORCES"
                      source: "n_37db95b4"
                      target: "n_eb7b922d"
                      properties: {}
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─────────────────────────────────────────────────────────────────────────────
  # POST /world-model/query
  # ─────────────────────────────────────────────────────────────────────────────
  /world-model/query:
    post:
      operationId: queryWorldModel
      tags: [world-model]
      summary: Query the graph
      description: |
        Query the world model by domain filter, full-text search term, node ID
        lookup, or relationship traversal. Combine filters — all supplied
        constraints are ANDed.

        **Query modes:**

        | Mode            | Required field | Description                                      |
        |-----------------|----------------|--------------------------------------------------|
        | Domain filter   | `domain`       | Return all nodes in one or more domains          |
        | Text search     | `search`       | Full-text match on title + summary + description |
        | Node lookup     | `node_id`      | Fetch a single node by its opaque ID             |
        | Traversal       | `traverse`     | BFS from a node, up to configurable depth        |

        All modes support `node_types` and `limit` filters.
        Counts against your daily quota (1 per call).
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorldModelQueryRequest"
            examples:
              domain_filter:
                summary: Return all FOUNDATION nodes
                value:
                  domain: FOUNDATION
                  limit: 50
              text_search:
                summary: Search for guardrails
                value:
                  search: "financial"
                  node_types: ["Guardrail"]
              node_lookup:
                summary: Fetch one node by ID
                value:
                  node_id: "n_37db95b4"
              traversal:
                summary: BFS from a node, depth 2
                value:
                  traverse:
                    from_id: "n_37db95b4"
                    depth: 2
                    direction: both
      responses:
        "200":
          description: Query results
          headers:
            X-Quota-Remaining:
              description: Queries remaining today (free/basic only)
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorldModelQueryResponse"
              example:
                ok: true
                data:
                  query_id: "q_01hx9a"
                  matched_nodes: 3
                  matched_edges: 2
                  nodes:
                    - id: "n_58740fbc"
                      title: "Financial Guardrail"
                      type: "Guardrail"
                      domain: "FOUNDATION"
                      labels: ["Guardrail", "Entity", "OdeiNode"]
                      properties:
                        trigger: "Any financial action >$1000"
                        response: "Require human review"
                        severity: "HIGH"
                  edges: []
                  truncated: false
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─────────────────────────────────────────────────────────────────────────────
  # POST /guardrail/check
  # ─────────────────────────────────────────────────────────────────────────────
  /guardrail/check:
    post:
      operationId: guardrailCheck
      tags: [guardrail]
      summary: Constitutional validation
      description: |
        Validate a proposed action against the ODEI constitutional layer
        (Guardrails, Principles, Policies in the FOUNDATION domain).

        The check is deterministic and rule-based — no LLM inference, low latency
        (<50ms). For complex decisions requiring reasoning, use
        `POST /world-model/query` to retrieve relevant guardrails and reason
        client-side.

        **Severity levels:**

        | Severity | Meaning                                              |
        |----------|------------------------------------------------------|
        | NONE     | Action is constitutionally clear                     |
        | LOW      | Informational flag, no blocking                      |
        | MEDIUM   | Caution warranted; log and review post-execution     |
        | HIGH     | Human review required before execution               |
        | BLOCKED  | Action violates a hard constitutional rule; stop     |

        Counts against your daily quota (1 per call).
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GuardrailCheckRequest"
            examples:
              safe_action:
                summary: Routine action — passes cleanly
                value:
                  action: "Update project README with latest deployment steps"
                  context: "Internal documentation task"
              high_severity:
                summary: Large financial action — requires human review
                value:
                  action: "Transfer $2500 USDC to partner wallet for infrastructure upgrade"
                  context: "Monthly infrastructure cost settlement"
              blocked:
                summary: Unknown wallet — blocked
                value:
                  action: "Send $50 to unknown wallet 0xDEAD..."
                  context: "Automated payment from daemon"
      responses:
        "200":
          description: Guardrail check result
          headers:
            X-Quota-Remaining:
              description: Queries remaining today (free/basic only)
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GuardrailCheckResponse"
              examples:
                passed:
                  summary: Action passed
                  value:
                    ok: true
                    data:
                      passed: true
                      severity: NONE
                      reason: "No constitutional triggers matched."
                      rules_checked: 3
                      matched_guardrails: []
                      recommendation: null
                      check_id: "gc_01hx9b"
                      checked_at: "2026-02-22T14:00:00.000Z"
                high_severity:
                  summary: HIGH severity — human review required
                  value:
                    ok: true
                    data:
                      passed: false
                      severity: HIGH
                      reason: "Action references an amount exceeding $1,000. Human review required before execution."
                      rules_checked: 3
                      matched_guardrails:
                        - id: "n_58740fbc"
                          title: "Financial Guardrail"
                          trigger: "amount > $1000"
                          severity: HIGH
                      recommendation: "Pause execution. Obtain explicit human approval before proceeding."
                      check_id: "gc_01hx9c"
                      checked_at: "2026-02-22T14:00:01.000Z"
                blocked:
                  summary: BLOCKED — hard stop
                  value:
                    ok: true
                    data:
                      passed: false
                      severity: BLOCKED
                      reason: "Action references an unknown wallet. This is a hard constitutional block."
                      rules_checked: 3
                      matched_guardrails:
                        - id: "rule_unknown_wallet"
                          title: "Unknown Wallet Block"
                          trigger: "unknown wallet"
                          severity: BLOCKED
                      recommendation: "Do not proceed. Verify wallet identity through a trusted source before retrying."
                      check_id: "gc_01hx9d"
                      checked_at: "2026-02-22T14:00:02.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  # ─────────────────────────────────────────────────────────────────────────────
  # GET /health
  # ─────────────────────────────────────────────────────────────────────────────
  /health:
    get:
      operationId: getHealth
      tags: [system]
      summary: API health and graph statistics
      description: |
        Returns service health status, uptime, and current graph statistics.
        This endpoint is public — no authentication required.
        Used by monitoring systems and the public status page.
      security: []
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              example:
                ok: true
                data:
                  service: odei-api-v2
                  status: healthy
                  version: "2.0.0"
                  uptime_seconds: 86400
                  started_at: "2026-02-21T14:00:00.000Z"
                  timestamp: "2026-02-22T14:00:00.000Z"
                  graph:
                    node_count: 91
                    edge_count: 91
                    domain_counts:
                      FOUNDATION: 25
                      VISION: 12
                      STRATEGY: 16
                      TACTICS: 8
                      EXECUTION: 11
                      TRACK: 19
                    last_updated: "2026-02-22T13:31:43.493Z"
                  upstream:
                    worldmodel_reachable: true
                    worldmodel_latency_ms: 12
        "503":
          description: Service degraded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"

  # ─────────────────────────────────────────────────────────────────────────────
  # GET /schema
  # ─────────────────────────────────────────────────────────────────────────────
  /schema:
    get:
      operationId: getSchema
      tags: [system]
      summary: Node types, relationship types, domain counts
      description: |
        Returns the authoritative schema for the ODEI world model graph:
        all node types organized by layer, all relationship types, and
        current node counts per domain.

        This endpoint is public — no authentication required.
        Useful for building clients, generating type definitions, and
        understanding what fields are available on each node type.

        Schema version follows semver and increments on breaking changes.
      security: []
      responses:
        "200":
          description: Schema returned successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchemaResponse"
              example:
                ok: true
                data:
                  schema_version: "1.3.0"
                  generated_at: "2026-02-22T13:31:43.493Z"
                  node_type_count: 59
                  relationship_type_count: 92
                  domains:
                    - name: FOUNDATION
                      node_count: 25
                      description: "Identity, values, principles, guardrails, partnerships"
                    - name: VISION
                      node_count: 12
                      description: "Goals, objectives, key results"
                    - name: STRATEGY
                      node_count: 16
                      description: "Strategies, milestones, risks"
                    - name: TACTICS
                      node_count: 8
                      description: "Projects, initiatives"
                    - name: EXECUTION
                      node_count: 11
                      description: "Tasks, decisions, actions, work sessions"
                    - name: TRACK
                      node_count: 19
                      description: "Metrics, signals, audit log, evidence"
                  node_types:
                    - name: Guardrail
                      layer: policy
                      required_properties: [response, summary, title, trigger]
                      optional_properties: [description, id, metadata, severity, status, tags]
                    - name: Value
                      layer: foundation
                      required_properties: [description, summary, title]
                      optional_properties: [id, metadata, priority, status, tags]
                    - name: Goal
                      layer: vision
                      required_properties: [summary, title]
                      optional_properties: [description, id, metadata, status, tags]
                  relationship_types:
                    - name: ENFORCES
                      from_layer: policy
                      to_layer: foundation
                      directional: true
                      cardinality: "N:M"
                    - name: PURSUES_GOAL
                      from_layer: foundation
                      to_layer: vision
                      directional: true
                      cardinality: "1:N"
        "500":
          $ref: "#/components/responses/InternalError"

# ─────────────────────────────────────────────────────────────────────────────
# Components
# ─────────────────────────────────────────────────────────────────────────────
components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT issued at sign-up. Include as `Authorization: Bearer <token>`.
        Valid for 30 days; refresh via account dashboard.
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: |
        Static API key for Pro and Enterprise tiers.
        Include as `X-Api-Key: <key>`.

  schemas:

    # ── Shared envelope ──────────────────────────────────────────────────────
    ErrorResponse:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          enum: [false]
        error:
          type: string
          description: Human-readable error message
          example: "Invalid request body: 'action' field is required"
        code:
          type: string
          description: Machine-readable error code
          example: "INVALID_REQUEST"

    # ── Graph primitives ─────────────────────────────────────────────────────
    GraphNode:
      type: object
      required: [id, title, type, domain, labels]
      properties:
        id:
          type: string
          description: Opaque node ID (stable across sessions)
          example: "n_37db95b4"
        title:
          type: string
          description: Human-readable node title
          example: "Evening Critic"
        type:
          type: string
          description: Node type from the schema (e.g. Guardrail, Goal, Value)
          example: "Guardrail"
        domain:
          type: string
          description: Graph domain
          enum: [FOUNDATION, VISION, STRATEGY, TACTICS, EXECUTION, TRACK]
          example: "FOUNDATION"
        labels:
          type: array
          items:
            type: string
          description: Neo4j labels on this node
          example: ["Guardrail", "Entity", "OdeiNode"]
        properties:
          type: object
          additionalProperties: true
          description: |
            Additional node properties from the schema. Fields vary by node type.
            Sensitive properties are always stripped from the public projection.

    GraphEdge:
      type: object
      required: [id, type, source, target]
      properties:
        id:
          type: string
          description: Opaque edge ID
          example: "e_abc123"
        type:
          type: string
          description: Relationship type (e.g. ENFORCES, PURSUES_GOAL)
          example: "ENFORCES"
        source:
          type: string
          description: Source node ID
          example: "n_37db95b4"
        target:
          type: string
          description: Target node ID
          example: "n_eb7b922d"
        properties:
          type: object
          additionalProperties: true
          description: Edge properties (varies by relationship type)

    GraphSummary:
      type: object
      required: [nodes, edges, domains]
      properties:
        nodes:
          type: integer
          description: Total node count in this response
          example: 91
        edges:
          type: integer
          description: Total edge count in this response
          example: 91
        domains:
          type: object
          description: Node count per domain
          additionalProperties:
            type: integer
          example:
            FOUNDATION: 25
            VISION: 12
            STRATEGY: 16
            TACTICS: 8
            EXECUTION: 11
            TRACK: 19

    # ── /world-model/live ────────────────────────────────────────────────────
    WorldModelLiveData:
      type: object
      required: [kind, generatedAt, timestamp, summary, nodes, edges]
      properties:
        kind:
          type: string
          enum: [public_projection]
        generatedAt:
          type: string
          format: date-time
          description: When the graph snapshot was generated from Neo4j
        timestamp:
          type: string
          format: date-time
          description: When this API response was assembled
        summary:
          $ref: "#/components/schemas/GraphSummary"
        nodes:
          type: array
          items:
            $ref: "#/components/schemas/GraphNode"
        edges:
          type: array
          items:
            $ref: "#/components/schemas/GraphEdge"

    WorldModelLiveResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          $ref: "#/components/schemas/WorldModelLiveData"

    # ── /world-model/query ───────────────────────────────────────────────────
    TraverseParams:
      type: object
      required: [from_id]
      properties:
        from_id:
          type: string
          description: Starting node ID for BFS traversal
          example: "n_37db95b4"
        depth:
          type: integer
          minimum: 1
          maximum: 5
          default: 1
          description: Maximum traversal depth (capped at 5)
        direction:
          type: string
          enum: [outbound, inbound, both]
          default: both
          description: Edge direction to follow
        relationship_types:
          type: array
          items:
            type: string
          description: |
            Filter to specific relationship types (e.g. ENFORCES, PURSUES_GOAL).
            If omitted, all relationship types are followed.
          example: ["ENFORCES", "GOVERNED_BY"]

    WorldModelQueryRequest:
      type: object
      description: |
        At least one of `domain`, `search`, `node_id`, or `traverse` must be supplied.
      properties:
        domain:
          type: string
          description: Filter nodes by domain
          enum: [FOUNDATION, VISION, STRATEGY, TACTICS, EXECUTION, TRACK]
          example: FOUNDATION
        search:
          type: string
          description: |
            Full-text search across title, summary, and description fields.
            Case-insensitive, partial match supported.
          minLength: 2
          maxLength: 200
          example: "financial guardrail"
        node_id:
          type: string
          description: Fetch a specific node by its opaque ID
          example: "n_37db95b4"
        traverse:
          $ref: "#/components/schemas/TraverseParams"
        node_types:
          type: array
          items:
            type: string
          description: |
            Filter results to these node types. Applied after the primary
            query mode. Common values: Goal, Guardrail, Value, Principle,
            Task, Project, Initiative, Insight, Pattern.
          example: ["Guardrail", "Principle"]
        limit:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
          description: Maximum number of nodes to return
        include_edges:
          type: boolean
          default: true
          description: Whether to include edges between returned nodes

    WorldModelQueryData:
      type: object
      required: [query_id, matched_nodes, matched_edges, nodes, edges, truncated]
      properties:
        query_id:
          type: string
          description: Unique ID for this query (use in support requests)
          example: "q_01hx9a"
        matched_nodes:
          type: integer
          description: Number of nodes returned
        matched_edges:
          type: integer
          description: Number of edges returned
        nodes:
          type: array
          items:
            $ref: "#/components/schemas/GraphNode"
        edges:
          type: array
          items:
            $ref: "#/components/schemas/GraphEdge"
        truncated:
          type: boolean
          description: True if results were cut at the `limit` value

    WorldModelQueryResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          $ref: "#/components/schemas/WorldModelQueryData"

    # ── /guardrail/check ─────────────────────────────────────────────────────
    GuardrailCheckRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          description: The proposed action to validate
          minLength: 3
          maxLength: 2000
          example: "Transfer $2500 USDC to partner wallet for infrastructure upgrade"
        context:
          type: string
          description: |
            Additional context that helps the rules engine make a more accurate
            determination. Include entity names, amounts, timing, and rationale.
          maxLength: 4000
          example: "Monthly infrastructure cost settlement, pre-approved by Anton"
        actor:
          type: string
          description: |
            Identity of the actor requesting the action (agent ID, user ID, or
            system name). Used for audit logging.
          maxLength: 100
          example: "daemon:grok-daemon"
        correlation_id:
          type: string
          description: Caller-provided correlation ID for tracing
          maxLength: 64
          example: "req_01hx9a_abc"

    MatchedGuardrail:
      type: object
      required: [id, title, trigger, severity]
      properties:
        id:
          type: string
          description: Node ID of the matched guardrail (or synthetic rule ID)
          example: "n_58740fbc"
        title:
          type: string
          description: Guardrail title
          example: "Financial Guardrail"
        trigger:
          type: string
          description: The trigger condition that matched
          example: "amount > $1000"
        severity:
          type: string
          enum: [LOW, MEDIUM, HIGH, BLOCKED]

    GuardrailCheckData:
      type: object
      required: [passed, severity, reason, rules_checked, matched_guardrails, check_id, checked_at]
      properties:
        passed:
          type: boolean
          description: |
            True if the action is constitutionally clear (severity NONE or LOW).
            False if severity is MEDIUM, HIGH, or BLOCKED.
        severity:
          type: string
          enum: [NONE, LOW, MEDIUM, HIGH, BLOCKED]
          description: Highest severity level among all matched rules
        reason:
          type: string
          description: Human-readable explanation of the outcome
          example: "Action references an amount exceeding $1,000. Human review required before execution."
        rules_checked:
          type: integer
          description: Number of guardrail rules evaluated
          example: 3
        matched_guardrails:
          type: array
          items:
            $ref: "#/components/schemas/MatchedGuardrail"
          description: Guardrails that triggered (empty if passed cleanly)
        recommendation:
          type: string
          nullable: true
          description: Actionable next step for the caller, or null if none needed
          example: "Pause execution. Obtain explicit human approval before proceeding."
        check_id:
          type: string
          description: Unique ID for this check (use in audit logs and support requests)
          example: "gc_01hx9b"
        checked_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when the check was performed

    GuardrailCheckResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          $ref: "#/components/schemas/GuardrailCheckData"

    # ── /health ──────────────────────────────────────────────────────────────
    UpstreamStatus:
      type: object
      required: [worldmodel_reachable]
      properties:
        worldmodel_reachable:
          type: boolean
        worldmodel_latency_ms:
          type: integer
          nullable: true

    GraphStats:
      type: object
      required: [node_count, edge_count, domain_counts]
      properties:
        node_count:
          type: integer
        edge_count:
          type: integer
        domain_counts:
          type: object
          additionalProperties:
            type: integer
        last_updated:
          type: string
          format: date-time
          nullable: true

    HealthData:
      type: object
      required: [service, status, version, uptime_seconds, started_at, timestamp]
      properties:
        service:
          type: string
          example: "odei-api-v2"
        status:
          type: string
          enum: [healthy, degraded, unhealthy]
        version:
          type: string
          example: "2.0.0"
        uptime_seconds:
          type: integer
        started_at:
          type: string
          format: date-time
        timestamp:
          type: string
          format: date-time
        graph:
          $ref: "#/components/schemas/GraphStats"
        upstream:
          $ref: "#/components/schemas/UpstreamStatus"

    HealthResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
        data:
          $ref: "#/components/schemas/HealthData"

    # ── /schema ──────────────────────────────────────────────────────────────
    DomainInfo:
      type: object
      required: [name, node_count]
      properties:
        name:
          type: string
          enum: [FOUNDATION, VISION, STRATEGY, TACTICS, EXECUTION, TRACK]
        node_count:
          type: integer
        description:
          type: string

    NodeTypeInfo:
      type: object
      required: [name, layer]
      properties:
        name:
          type: string
          example: "Guardrail"
        layer:
          type: string
          example: "policy"
        required_properties:
          type: array
          items:
            type: string
          example: [response, summary, title, trigger]
        optional_properties:
          type: array
          items:
            type: string
          example: [description, id, metadata, severity, status, tags]

    RelationshipTypeInfo:
      type: object
      required: [name, from_layer, to_layer, directional]
      properties:
        name:
          type: string
          example: "ENFORCES"
        from_layer:
          type: string
          example: "policy"
        to_layer:
          type: string
          example: "foundation"
        directional:
          type: boolean
        cardinality:
          type: string
          example: "N:M"

    SchemaData:
      type: object
      required: [schema_version, generated_at, node_type_count, relationship_type_count, domains, node_types, relationship_types]
      properties:
        schema_version:
          type: string
          example: "1.3.0"
        generated_at:
          type: string
          format: date-time
        node_type_count:
          type: integer
          example: 59
        relationship_type_count:
          type: integer
          example: 92
        domains:
          type: array
          items:
            $ref: "#/components/schemas/DomainInfo"
        node_types:
          type: array
          items:
            $ref: "#/components/schemas/NodeTypeInfo"
        relationship_types:
          type: array
          items:
            $ref: "#/components/schemas/RelationshipTypeInfo"

    SchemaResponse:
      type: object
      required: [ok, data]
      properties:
        ok:
          type: boolean
          enum: [true]
        data:
          $ref: "#/components/schemas/SchemaData"

  # ── Reusable responses ─────────────────────────────────────────────────────
  responses:
    BadRequest:
      description: Invalid request body or parameters
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            ok: false
            error: "At least one of domain, search, node_id, or traverse must be provided"
            code: "INVALID_REQUEST"

    Unauthorized:
      description: Missing or invalid authentication credential
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            ok: false
            error: "Authentication required. Provide a valid Bearer token or X-Api-Key header."
            code: "UNAUTHORIZED"

    RateLimited:
      description: Rate limit or daily quota exceeded
      headers:
        Retry-After:
          description: Seconds until the rate limit window resets
          schema:
            type: integer
        X-Quota-Remaining:
          description: Queries remaining today (0 when quota exhausted)
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            ok: false
            error: "Daily quota exceeded. Upgrade to Basic ($9/mo) for 1,000 queries/day."
            code: "QUOTA_EXCEEDED"

    InternalError:
      description: Unexpected server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            ok: false
            error: "Internal error. Contact support with the request ID."
            code: "INTERNAL_ERROR"
