# /openapi/usl-emoji-validation.yaml
openapi: 3.1.0
info:
  title: USL Emoji Validation and Scoring API
  version: 1.0.0
  description: >
    Evidence-based emoji concept ingestion, validation, and scoring API for usl.ph3ar.com.
    Aligns scoring outputs to Unicode emoji proposal factors and integrates inventory checks
    against Unicode data files and external emoji APIs (optional).
servers:
  - url: https://usl.ph3ar.com/api/v1

tags:
  - name: scoring
  - name: proposals
  - name: evidence
  - name: datasets
  - name: system

security:
  - ApiKeyAuth: []

paths:
  /health:
    get:
      tags: [system]
      summary: Health check
      operationId: healthCheck
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [status, time]
                properties:
                  status: { type: string, enum: [ok] }
                  time: { type: string, format: date-time }

  /emoji/score:
    post:
      tags: [scoring]
      summary: Stateless scoring for any user
      description: >
        Scores a concept without storing it. Runs: schema validation -> gates -> feature scoring -> response.
      operationId: scoreEmoji
      security: []  # Make this public if you want; remove if you require key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScoreRequest"
      responses:
        "200":
          description: Score response
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScoreResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /emoji/proposals:
    post:
      tags: [proposals]
      summary: Ingest a proposal (persist)
      operationId: ingestProposal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProposalIngest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [proposal_id, status]
                properties:
                  proposal_id: { $ref: "#/components/schemas/UUID" }
                  status: { type: string, enum: [stored] }
        "400":
          $ref: "#/components/responses/BadRequest"
    get:
      tags: [proposals]
      summary: List proposals
      operationId: listProposals
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - in: query
          name: cursor
          schema: { type: string }
      responses:
        "200":
          description: List
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/ProposalSummary"
                  next_cursor:
                    type: string

  /emoji/proposals/{proposal_id}:
    get:
      tags: [proposals]
      summary: Get proposal
      operationId: getProposal
      parameters:
        - $ref: "#/components/parameters/ProposalId"
      responses:
        "200":
          description: Proposal
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProposalIngest"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [proposals]
      summary: Delete proposal
      operationId: deleteProposal
      parameters:
        - $ref: "#/components/parameters/ProposalId"
      responses:
        "204":
          description: Deleted
        "404":
          $ref: "#/components/responses/NotFound"

  /emoji/proposals/{proposal_id}/validate:
    post:
      tags: [proposals]
      summary: Validate proposal (gates + issues + nearest emoji)
      operationId: validateProposal
      parameters:
        - $ref: "#/components/parameters/ProposalId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ValidateOptions"
      responses:
        "200":
          description: Validation results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationResult"
        "404":
          $ref: "#/components/responses/NotFound"

  /emoji/proposals/{proposal_id}/score:
    post:
      tags: [proposals]
      summary: Score a stored proposal
      operationId: scoreStoredProposal
      parameters:
        - $ref: "#/components/parameters/ProposalId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScoreOptions"
      responses:
        "200":
          description: Score response
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScoreResponse"
        "404":
          $ref: "#/components/responses/NotFound"

  /emoji/proposals/{proposal_id}/evidence:
    post:
      tags: [evidence]
      summary: Attach evidence items to a stored proposal
      operationId: addEvidence
      parameters:
        - $ref: "#/components/parameters/ProposalId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [evidence]
              properties:
                evidence:
                  type: array
                  maxItems: 500
                  items:
                    $ref: "#/components/schemas/EvidenceItem"
      responses:
        "200":
          description: Evidence attached
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [proposal_id, evidence_count]
                properties:
                  proposal_id: { $ref: "#/components/schemas/UUID" }
                  evidence_count: { type: integer, minimum: 0 }
        "404":
          $ref: "#/components/responses/NotFound"

  # ----------------------------
  # Dataset-backed endpoints
  # ----------------------------

  /datasets/emoji/inventory/search:
    get:
      tags: [datasets]
      summary: Search emoji inventory (Unicode data + optional external API)
      description: >
        Searches canonical inventory using Unicode emoji data files (UCD). Optionally augments results
        using emoji-api.com if enabled server-side.
      operationId: searchEmojiInventory
      security: []
      parameters:
        - in: query
          name: q
          required: true
          schema: { type: string, minLength: 1 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - in: query
          name: include_external
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InventorySearchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /datasets/emoji/inventory/exists:
    post:
      tags: [datasets]
      summary: Check whether a codepoint or sequence already exists (canonical)
      description: >
        Checks Unicode emoji inventory for existence of a codepoint/sequence and returns the canonical record.
      operationId: emojiExists
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [sequence]
              properties:
                sequence:
                  description: "Space-separated hex scalars, e.g. '1F600' or '1F468 200D 1F4BB'."
                  type: string
                  pattern: "^[0-9A-Fa-f]{4,6}( [0-9A-Fa-f]{4,6})*$"
      responses:
        "200":
          description: Exists result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InventoryExistsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /datasets/emoji/offline-pdf/keywords/search:
    get:
      tags: [datasets]
      summary: Search offline keywords (Emoji List v17.0 PDF)
      description: >
        Convenience endpoint for UI keyword lookup from Emoji List v17.0. Non-authoritative; use Unicode data files for production.
      operationId: searchOfflinePdfKeywords
      security: []
      parameters:
        - in: query
          name: q
          required: true
          schema: { type: string, minLength: 1 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
      responses:
        "200":
          description: Keyword hits
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [items, source]
                properties:
                  source:
                    type: object
                    additionalProperties: false
                    required: [note]
                    properties:
                      note:
                        type: string
                        example: "Derived from Emoji List v17.0 PDF for UI convenience; not authoritative."
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/OfflinePdfHit"

  /datasets/uts39/confusables/check:
    post:
      tags: [datasets]
      summary: UTS #39 confusables check
      description: >
        Checks an identifier or label against UTS #39 confusables and returns a risk assessment.
      operationId: uts39ConfusablesCheck
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [input]
              properties:
                input: { type: string, minLength: 1, maxLength: 512 }
                profile:
                  type: string
                  enum: [identifier, display, mixed_script]
                  default: identifier
      responses:
        "200":
          description: Confusables result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConfusablesResponse"
        "400":
          $ref: "#/components/responses/BadRequest"

  /datasets/unihan/lookup:
    get:
      tags: [datasets]
      summary: Unihan lookup for a single codepoint
      description: >
        Returns Unihan fields for a given CJK Unified Ideograph codepoint.
      operationId: unihanLookup
      security: []
      parameters:
        - in: query
          name: codepoint
          required: true
          schema:
            type: string
            pattern: "^(U\\+)?[0-9A-Fa-f]{4,6}$"
      responses:
        "200":
          description: Unihan record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnihanRecord"
        "400":
          $ref: "#/components/responses/BadRequest"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    ProposalId:
      name: proposal_id
      in: path
      required: true
      schema: { $ref: "#/components/schemas/UUID" }

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Not Found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    UUID:
      type: string
      format: uuid

    ISO8601DateTime:
      type: string
      format: date-time

    LanguageTag:
      type: string
      pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$"

    Url:
      type: string
      format: uri

    Error:
      type: object
      additionalProperties: false
      required: [error, message]
      properties:
        error: { type: string }
        message: { type: string }
        details:
          type: object
          additionalProperties: true

    Submitter:
      type: object
      additionalProperties: false
      required: [submitter_type]
      properties:
        submitter_type:
          type: string
          enum: [anonymous, individual, organization]
        display_name: { type: string }
        contact:
          type: object
          additionalProperties: false
          properties:
            email: { type: string, format: email }
            url: { $ref: "#/components/schemas/Url" }

    EvidenceItem:
      type: object
      additionalProperties: false
      required: [type, source, confidence]
      properties:
        type:
          type: string
          enum:
            - unicode_guideline_reference
            - unicode_proposal_reference
            - trend_link
            - social_link
            - corpus_snippet
            - translation_table
            - image_mock
            - api_query
            - manual_note
            - local_dataset_reference
        source:
          type: object
          additionalProperties: false
          required: [name]
          properties:
            name:
              type: string
              enum:
                - unicode.org
                - emoji-api.com
                - openmoji.org
                - emojibase.dev
                - carpedm20/emoji
                - emojipedia.org
                - user_provided
                - usl.ph3ar.com
                - local_ucd
                - local_unihan
                - local_uts39
                - local_pdf
            url: { $ref: "#/components/schemas/Url" }
            retrieved_at: { $ref: "#/components/schemas/ISO8601DateTime" }
            query: { type: string }
            notes: { type: string }
        confidence:
          type: number
          minimum: 0
          maximum: 1
        payload:
          type: object
          additionalProperties: true

    UsageExample:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        text: { type: string, minLength: 10 }
        locale: { $ref: "#/components/schemas/LanguageTag" }
        context_tags:
          type: array
          maxItems: 20
          items: { type: string }

    NearestEmoji:
      type: object
      additionalProperties: false
      required: [emoji, reason]
      properties:
        emoji: { type: string, minLength: 1, maxLength: 16 }
        reason: { type: string, minLength: 10 }
        similarity_hint:
          type: number
          minimum: 0
          maximum: 1

    VisualSpec:
      type: object
      additionalProperties: false
      required: [description]
      properties:
        description: { type: string, minLength: 20 }
        must_not_look_like:
          type: array
          maxItems: 20
          items: { type: string }
        key_features:
          type: array
          minItems: 1
          maxItems: 20
          items: { type: string, minLength: 3 }
        style_constraints:
          type: array
          maxItems: 20
          items: { type: string }

    ConceptDefinition:
      type: object
      additionalProperties: false
      required:
        - concept_name
        - short_name
        - category
        - concept_description
        - usage_examples
        - nearest_existing_emoji
        - visual_spec
      properties:
        concept_name: { type: string, minLength: 1 }
        short_name:
          type: string
          pattern: "^[a-z0-9][a-z0-9_]{1,48}[a-z0-9]$"
        category:
          type: string
          enum:
            - smileys_emotion
            - people_body
            - animals_nature
            - food_drink
            - travel_places
            - activities
            - objects
            - symbols
            - flags
            - other
        concept_description: { type: string, minLength: 40 }
        keywords:
          type: array
          minItems: 3
          maxItems: 30
          items: { type: string, minLength: 2 }
        synonyms:
          type: array
          maxItems: 50
          items: { type: string, minLength: 2 }
        antonyms:
          type: array
          maxItems: 50
          items: { type: string, minLength: 2 }
        usage_examples:
          type: array
          minItems: 5
          maxItems: 50
          items: { $ref: "#/components/schemas/UsageExample" }
        nearest_existing_emoji:
          type: array
          minItems: 3
          maxItems: 12
          items: { $ref: "#/components/schemas/NearestEmoji" }
        visual_spec: { $ref: "#/components/schemas/VisualSpec" }
        exclusions:
          type: object
          additionalProperties: false
          properties:
            brand_related: { type: boolean }
            political: { type: boolean }
            religious: { type: boolean }
            adult: { type: boolean }
            violence_or_weapons: { type: boolean }

    ProposalIngest:
      type: object
      additionalProperties: false
      required: [type, proposal, submitter, created_at]
      properties:
        type: { type: string, const: proposal_ingest }
        proposal_id: { $ref: "#/components/schemas/UUID" }
        created_at: { $ref: "#/components/schemas/ISO8601DateTime" }
        submitter: { $ref: "#/components/schemas/Submitter" }
        proposal: { $ref: "#/components/schemas/ConceptDefinition" }
        evidence:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/EvidenceItem" }

    ScoreRequest:
      type: object
      additionalProperties: false
      required: [type, proposal, created_at]
      properties:
        type: { type: string, const: score_request }
        request_id: { $ref: "#/components/schemas/UUID" }
        created_at: { $ref: "#/components/schemas/ISO8601DateTime" }
        proposal: { $ref: "#/components/schemas/ConceptDefinition" }
        evidence:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/EvidenceItem" }
        engine: { $ref: "#/components/schemas/EngineConfig" }

    EngineConfig:
      type: object
      additionalProperties: false
      properties:
        model_version: { type: string, default: v1 }
        strict_gates: { type: boolean, default: true }
        redundancy_similarity_reject_threshold:
          type: number
          minimum: 0
          maximum: 1
          default: 0.90
        redundancy_similarity_warning_threshold:
          type: number
          minimum: 0
          maximum: 1
          default: 0.85
        demand_time_window_days:
          type: integer
          minimum: 30
          maximum: 3650
          default: 1825
        include_external_inventory:
          type: boolean
          default: false

    ValidateOptions:
      type: object
      additionalProperties: false
      properties:
        include_external_inventory:
          type: boolean
          default: false

    ScoreOptions:
      type: object
      additionalProperties: false
      properties:
        include_external_inventory:
          type: boolean
          default: false
        include_evidence_echo:
          type: boolean
          default: true

    GateResult:
      type: object
      additionalProperties: false
      required: [name, status]
      properties:
        name:
          type: string
          enum:
            - minimum_quality
            - brand_policy
            - political_religious_policy
            - safety_policy
            - redundancy_check
            - inventory_exists_check
            - confusables_risk
        status: { type: string, enum: [pass, warn, fail] }
        details: { type: string }

    ValidationResult:
      type: object
      additionalProperties: false
      required: [proposal_id, gates, issues, nearest_existing_emoji_ranked]
      properties:
        proposal_id: { $ref: "#/components/schemas/UUID" }
        gates:
          type: array
          minItems: 1
          maxItems: 20
          items: { $ref: "#/components/schemas/GateResult" }
        issues:
          type: array
          items: { type: string, minLength: 5 }
        nearest_existing_emoji_ranked:
          type: array
          items: { $ref: "#/components/schemas/NearestEmoji" }
        evidence:
          type: array
          items: { $ref: "#/components/schemas/EvidenceItem" }

    ScoreBreakdown:
      type: object
      additionalProperties: false
      required:
        - demand
        - universality
        - semantic_gap
        - longevity
        - visual_distinctiveness
        - neutrality
        - completeness
      properties:
        demand: { type: number, minimum: 0, maximum: 1 }
        universality: { type: number, minimum: 0, maximum: 1 }
        semantic_gap: { type: number, minimum: 0, maximum: 1 }
        longevity: { type: number, minimum: 0, maximum: 1 }
        visual_distinctiveness: { type: number, minimum: 0, maximum: 1 }
        neutrality: { type: number, minimum: 0, maximum: 1 }
        completeness: { type: number, minimum: 0, maximum: 1 }

    Driver:
      type: object
      additionalProperties: false
      required: [feature, direction, impact]
      properties:
        feature: { type: string }
        direction: { type: string, enum: [positive, negative] }
        impact: { type: number }
        evidence_refs:
          type: array
          maxItems: 20
          items: { type: integer, minimum: 0 }

    ScoreResponse:
      type: object
      additionalProperties: false
      required:
        - type
        - created_at
        - proposal_short_name
        - score_0_100
        - p_accept
        - breakdown
        - gates
        - top_drivers
        - recommendations
      properties:
        type: { type: string, const: score_response }
        created_at: { $ref: "#/components/schemas/ISO8601DateTime" }
        proposal_short_name: { type: string }
        score_0_100: { type: number, minimum: 0, maximum: 100 }
        p_accept: { type: number, minimum: 0, maximum: 1 }
        confidence_interval:
          type: object
          additionalProperties: false
          required: [low, high]
          properties:
            low: { type: number, minimum: 0, maximum: 1 }
            high: { type: number, minimum: 0, maximum: 1 }
        breakdown: { $ref: "#/components/schemas/ScoreBreakdown" }
        gates:
          type: array
          minItems: 1
          maxItems: 20
          items: { $ref: "#/components/schemas/GateResult" }
        nearest_existing_emoji_ranked:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/NearestEmoji" }
        top_drivers:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/Driver" }
        recommendations:
          type: array
          maxItems: 30
          items: { type: string, minLength: 10 }
        evidence:
          type: array
          maxItems: 500
          items: { $ref: "#/components/schemas/EvidenceItem" }

    ProposalSummary:
      type: object
      additionalProperties: false
      required: [proposal_id, short_name, concept_name, created_at]
      properties:
        proposal_id: { $ref: "#/components/schemas/UUID" }
        short_name: { type: string }
        concept_name: { type: string }
        created_at: { $ref: "#/components/schemas/ISO8601DateTime" }

    InventoryEmojiRecord:
      type: object
      additionalProperties: false
      required: [sequence, codepoints, is_emoji, sources]
      properties:
        sequence:
          type: string
          description: "Space-separated hex scalars."
        codepoints:
          type: array
          items: { type: string, pattern: "^[0-9A-Fa-f]{4,6}$" }
        is_emoji: { type: boolean }
        properties:
          type: array
          items: { type: string }
        cldr_short_name: { type: string }
        keywords:
          type: array
          items: { type: string }
        sources:
          type: array
          items:
            type: string
            enum: [local_ucd, local_pdf, external_emoji_api]

    InventorySearchResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/InventoryEmojiRecord" }

    InventoryExistsResponse:
      type: object
      additionalProperties: false
      required: [exists, record]
      properties:
        exists: { type: boolean }
        record:
          oneOf:
            - { $ref: "#/components/schemas/InventoryEmojiRecord" }
            - { type: "null" }

    OfflinePdfHit:
      type: object
      additionalProperties: false
      required: [code, cldr_short_name, matched]
      properties:
        code:
          type: string
          description: "Rendered as 'U+....' or sequences like 'U+.... U+....'."
        cldr_short_name: { type: string }
        matched:
          type: array
          items: { type: string }

    ConfusablesResponse:
      type: object
      additionalProperties: false
      required: [input, risk, skeleton, warnings]
      properties:
        input: { type: string }
        risk:
          type: string
          enum: [low, medium, high]
        skeleton: { type: string }
        warnings:
          type: array
          items: { type: string }
        confusable_matches:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [other, other_skeleton, note]
            properties:
              other: { type: string }
              other_skeleton: { type: string }
              note: { type: string }

    UnihanRecord:
      type: object
      additionalProperties: true
      required: [codepoint]
      properties:
        codepoint: { type: string, example: "U+4E00" }
        fields:
          type: object
          additionalProperties: true