openapi: 3.1.0
info:
  title: Profanity / Political Term Check API
  description: >
    Self-contained REST API for checking usernames and display names against
    profanity, slurs, and political terms. Normalizes leet-speak and masks
    known-safe words (e.g. "classClown92", "assassin", "Scunthorpe") before
    matching, so common false positives aren't flagged. Ambiguous matches are
    escalated to an LLM fallback and the verdict is learned for next time.
  version: "1.0.0"
servers:
  - url: https://profanity.eat-sleep-code.com
paths:
  /api/check:
    post:
      operationId: checkText
      summary: Check a string for profanity, slurs, or political terms.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:
                  type: string
                  description: The username or display name to check.
                  example: classClown92
                locale:
                  type: string
                  description: >
                    BCP-47 locale for the response's `reason` text (and the LLM
                    fallback's judgment language). Falls back to en-US for any
                    unconfigured locale.
                  default: en-US
                  example: en-US
                rating:
                  type: string
                  enum: [G, PG]
                  description: >
                    "G" (default) blocks mild crude slang (e.g. "ass", "boobs")
                    alongside genuine profanity, slurs, and political terms.
                    "PG" allows that mild slang through but still blocks
                    genuinely offensive content, slurs, and political
                    terms. Any other value is treated as "G".
                  default: G
                  example: G
      responses:
        "200":
          description: Check result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckResult"
              examples:
                clean:
                  value:
                    flagged: false
                    category: null
                    matches: []
                    confidence: none
                    reason: No blocked terms found.
                    source: local
                flagged:
                  value:
                    flagged: true
                    category: profanity
                    matches: ["fuk"]
                    confidence: high
                    reason: Matched profanity term "fuk" as a whole word.
                    source: local
        "400":
          description: Missing `text`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/admin/terms:
    get:
      operationId: listDynamicTerms
      summary: List learned terms (allow/profanity/slurs/political).
      description: >
        Gated by Cloudflare Access in front of this path — see the Zero Trust
        dashboard. Not authenticated by the API itself.
      responses:
        "200":
          description: The current dynamic term lists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DynamicTermsResponse"
    post:
      operationId: addDynamicTerm
      summary: Add a term to a list by hand.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TermMutation"
      responses:
        "200":
          description: Updated term lists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DynamicTermsResponse"
        "400":
          description: Missing `list` or `term`, or an invalid list name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: removeDynamicTerm
      summary: Remove a term from a list by hand.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TermMutation"
      responses:
        "200":
          description: Updated term lists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DynamicTermsResponse"
        "400":
          description: Missing `list` or `term`, or an invalid list name.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    CheckResult:
      type: object
      properties:
        flagged:
          type: boolean
        category:
          type: [string, "null"]
          enum: [profanity, slur, political, null]
        matches:
          type: array
          items:
            type: string
        confidence:
          type: string
          enum: [high, medium, none]
        reason:
          type: string
          description: Human-readable explanation, localized to the request's `locale`.
        source:
          type: string
          enum: [local, learned, llm]
          description: >
            "local" = matched a static list term. "learned" = matched a term
            previously learned from an LLM verdict (its `reason` is that
            original verdict, reused verbatim). "llm" = the LLM was just
            consulted on this request.
        rating:
          type: string
          enum: [G, PG]
          description: The rating that was actually applied to this check.
    TermList:
      type: string
      enum: [allow, profanity, slurs, political]
    TermEntry:
      type: object
      properties:
        term:
          type: string
        reason:
          type: [string, "null"]
          description: Why this term was added — the LLM's original verdict, or an admin-supplied note.
        source:
          type: [string, "null"]
          enum: [llm, admin, null]
        addedAt:
          type: [string, "null"]
          format: date-time
    TermMutation:
      type: object
      required: [list, term]
      properties:
        list:
          $ref: "#/components/schemas/TermList"
        term:
          type: string
        reason:
          type: string
          description: Optional note explaining why this term was added by hand.
        locale:
          type: string
          default: en-US
    DynamicTermsResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            allow:
              type: array
              items:
                $ref: "#/components/schemas/TermEntry"
            profanity:
              type: array
              items:
                $ref: "#/components/schemas/TermEntry"
            slurs:
              type: array
              items:
                $ref: "#/components/schemas/TermEntry"
            political:
              type: array
              items:
                $ref: "#/components/schemas/TermEntry"
    Error:
      type: object
      properties:
        error:
          type: string
