openapi: 3.1.0
info:
  title: SuccessGraph REST API
  version: 1.1.0
  description: |
    Read-only graph of Success IT clients, tenants, products, and company groups.
    Agents should check `/api/health`, then use the `/api/v1/*` envelope routes.
    Unversioned routes remain available for legacy clients. MCP is optional.
servers:
  - url: https://successgraph.successit.net
    description: Production
  - url: http://localhost:3002
    description: Local development
security:
  - ApiKey: []
  - EntraBearer: []
paths:
  /api/health:
    get:
      operationId: getHealth
      summary: Readiness and actual index freshness
      security: []
      responses:
        "200":
          description: The graph is fresh or stale but readable
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
        "503":
          description: A required index is missing or unavailable
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
  /api/schema:
    get:
      operationId: getSchema
      summary: Retrieve this OpenAPI document
      security: []
      responses:
        "200":
          description: OpenAPI 3.1 YAML
          content:
            text/yaml:
              schema: { type: string }
  /api/v1/stats:
    get:
      operationId: getStatsV1
      summary: Graph statistics with freshness and provenance
      parameters:
        - $ref: "#/components/parameters/AllowStale"
      responses:
        "200":
          description: Standard graph envelope
          headers:
            X-SuccessGraph-Classification: { $ref: "#/components/headers/Classification" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatsEnvelope" }
        "503": { $ref: "#/components/responses/Unavailable" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/search:
    get:
      operationId: searchNodesV1
      summary: Bounded graph search with freshness and provenance
      parameters:
        - $ref: "#/components/parameters/SearchQuery"
        - $ref: "#/components/parameters/NodeType"
        - $ref: "#/components/parameters/Layer"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/AllowStale"
      responses:
        "200":
          description: Standard graph envelope; classification can be PASS or DEGRADED
          headers:
            X-SuccessGraph-Classification: { $ref: "#/components/headers/Classification" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SearchEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "503": { $ref: "#/components/responses/Unavailable" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/node/{key}:
    get:
      operationId: getNodeV1
      summary: Exact node context with freshness and provenance
      parameters:
        - $ref: "#/components/parameters/NodeKey"
        - $ref: "#/components/parameters/AllowStale"
      responses:
        "200":
          description: Standard graph envelope
          headers:
            X-SuccessGraph-Classification: { $ref: "#/components/headers/Classification" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NodeEnvelope" }
        "404":
          description: BLOCKED envelope with NODE_NOT_FOUND diagnostic
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GraphEnvelope" }
        "503": { $ref: "#/components/responses/Unavailable" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/v1/blast/{key}:
    get:
      operationId: getBlastRadiusV1
      summary: Bounded upstream and downstream traversal with provenance
      parameters:
        - $ref: "#/components/parameters/NodeKey"
        - $ref: "#/components/parameters/Depth"
        - $ref: "#/components/parameters/AllowStale"
      responses:
        "200":
          description: Standard graph envelope
          headers:
            X-SuccessGraph-Classification: { $ref: "#/components/headers/Classification" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BlastEnvelope" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404":
          description: BLOCKED envelope with NODE_NOT_FOUND diagnostic
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GraphEnvelope" }
        "503": { $ref: "#/components/responses/Unavailable" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/stats:
    get:
      operationId: getStatsLegacy
      summary: Legacy raw graph statistics
      deprecated: true
      responses:
        "200":
          description: Legacy statistics object without envelope
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Stats" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/search:
    get:
      operationId: searchNodesLegacy
      summary: Legacy raw graph search
      deprecated: true
      parameters:
        - $ref: "#/components/parameters/SearchQuery"
        - $ref: "#/components/parameters/NodeType"
        - $ref: "#/components/parameters/Layer"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Array of scored nodes; metadata remains a JSON string
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/LegacySearchResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/node/{key}:
    get:
      operationId: getNodeLegacy
      summary: Legacy raw node context
      deprecated: true
      parameters:
        - $ref: "#/components/parameters/NodeKey"
      responses:
        "200":
          description: Node and edge context without envelope
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LegacyNodeContext" }
        "404": { $ref: "#/components/responses/NotFound" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/blast/{key}:
    get:
      operationId: getBlastRadiusLegacy
      summary: Legacy raw traversal
      deprecated: true
      parameters:
        - $ref: "#/components/parameters/NodeKey"
        - $ref: "#/components/parameters/Depth"
      responses:
        "200":
          description: Upstream and downstream arrays without envelope
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BlastAnswer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/graph:
    get:
      operationId: getFullGraph
      summary: Full graph export for visualization
      description: Potentially large. Prefer targeted versioned operations for agents.
      responses:
        "200":
          description: All graph nodes and edges
          content:
            application/json:
              schema:
                type: object
                required: [nodes, edges]
                properties:
                  nodes:
                    type: array
                    items: { $ref: "#/components/schemas/Node" }
                  edges:
                    type: array
                    items: { $ref: "#/components/schemas/Edge" }
        "404":
          description: Full export is administratively disabled
        "413":
          description: The graph exceeds configured export bounds
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Runtime-injected key. Use one labelled key per client.
    EntraBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
  headers:
    Classification:
      description: Trust classification of the graph answer
      schema: { $ref: "#/components/schemas/Classification" }
  parameters:
    SearchQuery:
      name: q
      in: query
      required: true
      schema: { type: string, minLength: 1, maxLength: 200 }
      description: Name, code, acronym, or other bounded search term
    NodeType:
      name: type
      in: query
      schema: { type: string, enum: [client, tenant, product, group, category] }
    Layer:
      name: layer
      in: query
      schema: { type: string, enum: [catalyst, platform] }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    NodeKey:
      name: key
      in: path
      required: true
      schema: { type: string, minLength: 1, maxLength: 300 }
      description: Exact key such as client:A054 or tenant:AUTOCLINIC
    Depth:
      name: depth
      in: query
      schema: { type: integer, minimum: 1, maximum: 20, default: 10 }
    AllowStale:
      name: allow_stale
      in: query
      schema: { type: boolean, default: false }
      description: Acknowledge stale exploratory data; classification remains DEGRADED.
  responses:
    BadRequest:
      description: Invalid or out-of-bounds input
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Node not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing or invalid graph credential
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unavailable:
      description: One or more required indexes is unavailable
      content:
        application/json:
          schema: { $ref: "#/components/schemas/GraphEnvelope" }
    TooManyRequests:
      description: Per-client request limit exceeded
      headers:
        Retry-After:
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Classification:
      type: string
      enum: [PASS, DEGRADED, BLOCKED, UNAVAILABLE, AMBIGUOUS]
    Diagnostic:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
    IndexerProvenance:
      type: object
      required: [indexer, status, finished_at, age_days, node_count, edge_count]
      properties:
        indexer: { type: string, enum: [catalyst, platform, linker] }
        run_id: { type: [integer, "null"] }
        status: { type: string }
        finished_at: { type: [string, "null"] }
        age_days: { type: [number, "null"] }
        node_count: { type: [integer, "null"] }
        edge_count: { type: [integer, "null"] }
    Provenance:
      type: object
      required: [status, complete, stale_after_days, index_identity, snapshot_id, snapshot_sha256, generated_at, code_revision, source_revision, config_identity, indexers]
      properties:
        status: { type: string, enum: [fresh, stale, unavailable] }
        complete: { type: boolean }
        stale_after_days: { type: integer }
        index_identity: { type: string }
        snapshot_id: { type: [string, "null"] }
        snapshot_sha256: { type: [string, "null"] }
        generated_at: { type: [string, "null"] }
        code_revision: { type: [string, "null"] }
        source_revision: { type: [string, "null"] }
        config_identity: { type: [string, "null"] }
        indexers:
          type: array
          minItems: 3
          maxItems: 3
          items: { $ref: "#/components/schemas/IndexerProvenance" }
    GraphEnvelope:
      type: object
      required: [classification, answer, diagnostics, provenance, allow_stale_used]
      properties:
        classification: { $ref: "#/components/schemas/Classification" }
        answer: {}
        diagnostics:
          type: array
          items: { $ref: "#/components/schemas/Diagnostic" }
        provenance: { $ref: "#/components/schemas/Provenance" }
        allow_stale_used: { type: boolean }
    Node:
      type: object
      required: [node_key, name, node_type, layer]
      properties:
        node_key: { type: string }
        name: { type: string }
        node_type: { type: string, enum: [client, tenant, product, group, category] }
        layer: { type: string }
        metadata: { type: object, additionalProperties: true }
    LegacySearchResult:
      type: object
      required: [score, node_key, name, node_type, layer, metadata]
      properties:
        score: { type: number }
        node_key: { type: string }
        name: { type: string }
        node_type: { type: string }
        layer: { type: string }
        metadata: { type: string }
    SearchResult:
      allOf:
        - $ref: "#/components/schemas/Node"
        - type: object
          required: [score]
          properties:
            score: { type: number }
    Edge:
      type: object
      properties:
        source: { type: string }
        target: { type: string }
        edge_type: { type: string }
        confidence: { type: number }
    LegacyNodeContext:
      type: object
      required: [node, inbound, outbound]
      properties:
        node: { $ref: "#/components/schemas/Node" }
        inbound: { type: array, items: { type: object } }
        outbound: { type: array, items: { type: object } }
    NodeContextV1:
      type: object
      required: [node, inbound, outbound, inbound_truncated, outbound_truncated, metadata_truncated, complete]
      properties:
        node: { $ref: "#/components/schemas/Node" }
        inbound: { type: array, items: { type: object } }
        outbound: { type: array, items: { type: object } }
        inbound_truncated: { type: boolean }
        outbound_truncated: { type: boolean }
        metadata_truncated: { type: boolean }
        complete: { type: boolean }
    TraversalNode:
      type: object
      required: [node_key, name, node_type, layer, depth]
      properties:
        node_key: { type: string }
        name: { type: string }
        node_type: { type: string }
        layer: { type: string }
        depth: { type: integer }
    BlastAnswer:
      type: object
      required: [upstream, downstream]
      properties:
        node_key: { type: string }
        upstream_count: { type: integer }
        downstream_count: { type: integer }
        upstream:
          type: array
          items: { $ref: "#/components/schemas/TraversalNode" }
        downstream:
          type: array
          items: { $ref: "#/components/schemas/TraversalNode" }
        upstream_truncated: { type: boolean }
        downstream_truncated: { type: boolean }
        complete: { type: boolean }
        limits:
          type: object
          properties:
            depth: { type: integer }
            nodes_per_direction: { type: integer }
            paths_per_direction: { type: integer }
    SearchAnswer:
      type: object
      required: [query, total, total_is_lower_bound, scanned_candidates, complete, truncated, results]
      properties:
        query: { type: string }
        total: { type: integer }
        total_is_lower_bound: { type: boolean }
        scanned_candidates: { type: integer }
        complete: { type: boolean }
        truncated: { type: boolean }
        results:
          type: array
          items: { $ref: "#/components/schemas/SearchResult" }
    Stats:
      type: object
      required: [nodesByType, nodesByLayer, total_nodes, total_edges]
      properties:
        nodesByType: { type: array, items: { type: object } }
        nodesByLayer: { type: array, items: { type: object } }
        edgesByType: { type: array, items: { type: object } }
        total_nodes: { type: integer }
        total_edges: { type: integer }
    StatsV1:
      type: object
      required: [nodes_by_type, nodes_by_layer, edges_by_type, total_nodes, total_edges]
      properties:
        nodes_by_type: { type: array, items: { type: object } }
        nodes_by_layer: { type: array, items: { type: object } }
        edges_by_type: { type: array, items: { type: object } }
        total_nodes: { type: integer }
        total_edges: { type: integer }
    SearchEnvelope:
      allOf:
        - $ref: "#/components/schemas/GraphEnvelope"
        - type: object
          properties:
            answer: { $ref: "#/components/schemas/SearchAnswer" }
    NodeEnvelope:
      allOf:
        - $ref: "#/components/schemas/GraphEnvelope"
        - type: object
          properties:
            answer:
              oneOf:
                - $ref: "#/components/schemas/NodeContextV1"
                - type: "null"
    BlastEnvelope:
      allOf:
        - $ref: "#/components/schemas/GraphEnvelope"
        - type: object
          properties:
            answer:
              oneOf:
                - $ref: "#/components/schemas/BlastAnswer"
                - type: "null"
    StatsEnvelope:
      allOf:
        - $ref: "#/components/schemas/GraphEnvelope"
        - type: object
          properties:
            answer: { $ref: "#/components/schemas/StatsV1" }
    Health:
      type: object
      required: [status, classification, nodes, edges, indexFreshness, diagnostics, uptime]
      properties:
        status: { type: string, enum: [ok, degraded, unavailable] }
        classification: { $ref: "#/components/schemas/Classification" }
        nodes: { type: integer }
        edges: { type: integer }
        lastIndex:
          oneOf:
            - $ref: "#/components/schemas/IndexerProvenance"
            - type: "null"
        indexFreshness: { $ref: "#/components/schemas/Provenance" }
        diagnostics:
          type: array
          items: { $ref: "#/components/schemas/Diagnostic" }
        uptime: { type: number }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
