# JobsPipe API ruleset (Spectral). The published OpenAPI document at https://jobspipe.dev/openapi.json
# is linted against it on every change: packages/openapi-spec runs `bun run lint:spectral` in CI.
# Maintained by JobsPipe. method: published
extends: ["spectral:oas"]
documentationUrl: https://jobspipe.dev/governance
rules:
  # openIdConnect security schemes cannot list scopes in OpenAPI 3.1, so this rule reports every
  # operation that names a scope on the oidc scheme. The scopes are published in the protected
  # resource metadata (RFC 9728) at https://api.jobspipe.dev/.well-known/oauth-protected-resource.
  oas3-operation-security-defined: off

  jobspipe-operation-id-camel-case:
    description: operationIds are lowerCamelCase verbs, so generated SDK methods read naturally.
    severity: error
    given: "$.paths[*][get,post,put,patch,delete]"
    then:
      field: operationId
      function: casing
      functionOptions: { type: camel }

  jobspipe-operation-tagged:
    description: Every operation belongs to exactly one tag, which is the API it is published under.
    severity: error
    given: "$.paths[*][get,post,put,patch,delete]"
    then:
      field: tags
      function: length
      functionOptions: { min: 1, max: 1 }

  jobspipe-servers-https:
    description: The API is only served over HTTPS from api.jobspipe.dev.
    severity: error
    given: "$.servers[*].url"
    then:
      function: pattern
      functionOptions: { match: "^https://api\\.jobspipe\\.dev" }

  jobspipe-path-kebab-case:
    description: Path segments are lowercase, with hyphens between words.
    severity: error
    given: "$.paths"
    then:
      field: "@key"
      function: pattern
      functionOptions: { match: "^(/([a-z0-9-]+|\\{[a-z_]+\\}))+$" }

  jobspipe-versioned-paths:
    description: Every path is under a major version, so a breaking change ships as a new version.
    severity: error
    given: "$.paths"
    then:
      field: "@key"
      function: pattern
      functionOptions: { match: "^/v[0-9]+/" }

  jobspipe-success-response-example:
    description: Every JSON success response carries an example.
    severity: warn
    given: "$.paths[*][get,post,put,patch,delete].responses[?(@property.match(/^2/))].content.application/json"
    then:
      field: example
      function: truthy

  jobspipe-request-body-example:
    description: Every JSON request body carries an example.
    severity: warn
    given: "$.paths[*][get,post,put,patch,delete].requestBody.content.application/json"
    then:
      field: example
      function: truthy

  jobspipe-unauthorized-documented:
    description: Operations that need a key document their 401.
    severity: warn
    given: "$.paths[?(!@property.match(/^\\/v1\\/sandbox/))][get,post,put,patch,delete].responses"
    then:
      field: "401"
      function: truthy

  jobspipe-error-body-json:
    description: Error responses are JSON, so an agent can read the error field instead of parsing HTML.
    severity: warn
    given: "$.paths[*][get,post,put,patch,delete].responses[?(@property.match(/^[45]/))].content"
    then:
      field: application/json
      function: truthy

  jobspipe-operation-summary-length:
    description: Summaries are short enough to read as a menu item.
    severity: info
    given: "$.paths[*][get,post,put,patch,delete].summary"
    then:
      function: length
      functionOptions: { max: 60 }

  jobspipe-tag-described:
    description: Every top-level tag has a description.
    severity: info
    given: "$.tags[*]"
    then:
      field: description
      function: truthy
