openapi: 3.1.0
info:
  title: SmartMatch OCR API
  version: "1.0.0"
  summary: Accuracy-assured document intelligence API (Target-Profile aware).
  description: |
    **SmartMatch OCR** is an accuracy-assurance layer on top of external OCR
    (e.g. Google Document AI). Given a **Target Profile** that defines *what to
    find*, SmartMatch locates candidates in a document, normalizes values,
    validates them against rules, routes uncertain items through a mandatory
    human double-check, and exposes only **Finalized** data externally.

    Every response involving a document carries `project_id` and
    `target_profile_version` so that every extraction is reproducible and
    auditable. Intermediate results (`/results`, review tasks, etc.) are
    permission-gated and **never** exposed to downstream business systems.

    **Base URL**

    ```
    https://smartmatch.gloding.com/v1
    ```

    **Authentication**

    All endpoints require a bearer token (OIDC/JWT). Role-based authorization
    is applied per endpoint (e.g. management endpoints are limited roles only).

    **Idempotency**

    Mutating endpoints accept an `Idempotency-Key` header to prevent duplicate
    processing (especially for long-running AI jobs).

  contact:
    name: SmartMatch OCR Support
    email: contact@gloding.com
    url: https://smartmatch.gloding.com
  license:
    name: Proprietary
servers:
  - url: https://smartmatch.gloding.com/v1
    description: Production
  - url: https://smartmatch.staging.gloding.com/v1
    description: Staging

tags:
  - name: Projects & Target Profiles
    description: Define *what to find* per project. Versions are immutable once published.
  - name: Documents
    description: Create and inspect Document containers.
  - name: Pages & Upload
    description: Add images or upload PDFs to a Document.
  - name: Pre-check
    description: Input quality & locatability gate.
  - name: AI jobs
    description: External OCR / extraction (async).
  - name: Validation
    description: Normalize, validate, and score extracted data.
  - name: Review & Double-check (HITL)
    description: Human review with mandatory second approver.
  - name: Finalization
    description: Produce and expose Finalized data externally.
  - name: Audit & Config
    description: Audit logs and applied configuration versions.

security:
  - bearerAuth: []

paths:
  # ---------------------------------------------------------------------------
  # Projects & Target Profiles
  # ---------------------------------------------------------------------------
  /projects:
    post:
      tags: [Projects & Target Profiles]
      summary: Create a project
      description: Issue a new `project_id` (billing / access boundary / audit axis).
      operationId: createProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectCreate"
      responses:
        "201":
          description: Project created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /projects/{project_id}:
    parameters:
      - $ref: "#/components/parameters/ProjectId"
    get:
      tags: [Projects & Target Profiles]
      summary: Get a project
      operationId: getProject
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Project" }
        "404": { $ref: "#/components/responses/NotFound" }

  /projects/{project_id}/target-profiles:
    parameters:
      - $ref: "#/components/parameters/ProjectId"
    post:
      tags: [Projects & Target Profiles]
      summary: Create a Target Profile (draft)
      description: |
        Create a new Target Profile draft inside a project. The draft is mutable
        until a version is published via
        `POST /target-profiles/{profile_id}/versions`.
      operationId: createTargetProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TargetProfileDraft"
      responses:
        "201":
          description: Draft created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TargetProfile" }
        "422":
          description: Definition invalid
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: PROFILE_INVALID
                message: "Target 't_total_amount' is missing 'expected_type'."
                request_id: req_01HE...

  /target-profiles/{profile_id}:
    parameters:
      - $ref: "#/components/parameters/ProfileId"
    get:
      tags: [Projects & Target Profiles]
      summary: Get a Target Profile (including drafts)
      operationId: getTargetProfile
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TargetProfile" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: PROFILE_NOT_FOUND
                message: "Target Profile not found."
                request_id: req_01HE...

  /target-profiles/{profile_id}/versions:
    parameters:
      - $ref: "#/components/parameters/ProfileId"
    post:
      tags: [Projects & Target Profiles]
      summary: Publish an immutable Target Profile version
      description: |
        Freezes the current draft into an immutable `target_profile_version`.
        Once published, that version cannot be edited — any changes must be
        published as a new version. Documents being processed reference a fixed
        version throughout their lifecycle.
      operationId: publishTargetProfileVersion
      responses:
        "201":
          description: Version published
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TargetProfile" }
        "409":
          description: Version conflict
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: PROFILE_VERSION_CONFLICT
                message: "Active version cannot be re-published."
                request_id: req_01HE...

  /target-profiles/{profile_id}/versions/{version}:
    parameters:
      - $ref: "#/components/parameters/ProfileId"
      - name: version
        in: path
        required: true
        schema: { type: string, example: "3" }
    get:
      tags: [Projects & Target Profiles]
      summary: Get a specific Target Profile version (immutable)
      operationId: getTargetProfileVersion
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TargetProfile" }

  /target-profiles/{profile_id}/diff:
    parameters:
      - $ref: "#/components/parameters/ProfileId"
      - { name: from, in: query, required: true, schema: { type: string } }
      - { name: to, in: query, required: true, schema: { type: string } }
    get:
      tags: [Projects & Target Profiles]
      summary: Diff between two Target Profile versions
      operationId: diffTargetProfileVersions
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TargetProfileDiff" }

  # ---------------------------------------------------------------------------
  # Documents
  # ---------------------------------------------------------------------------
  /documents:
    post:
      tags: [Documents]
      summary: Create a Document container
      description: |
        Issues a new `document_id`. Binds the document to a `project_id` and a
        published `target_profile_version` — both are pinned for the lifetime
        of this document (status transitions, review, audit all reference it).
      operationId: createDocument
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DocumentCreate" }
      responses:
        "201":
          description: Document created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Document" }
              example:
                document_id: doc_01HE9Q2T8M0000000000
                project_id: prj_inv_jp
                target_profile_version: "3"
                external_ref: erp-invoice-99812
                status: Uploaded
                created_at: "2026-03-04T09:21:02Z"
                updated_at: "2026-03-04T09:21:02Z"
        "400":
          description: Invalid payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: INPUT_INVALID_FORMAT
                message: "target_profile_version is required."
                request_id: req_01HE...

    get:
      tags: [Documents]
      summary: List / search documents
      description: Operational & audit-oriented search by state, time range, or external reference.
      operationId: listDocuments
      parameters:
        - { name: status, in: query, schema: { $ref: "#/components/schemas/DocumentStatus" } }
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { name: external_ref, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Document" }

  /documents/{document_id}:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    get:
      tags: [Documents]
      summary: Get document metadata, status, and score summary
      operationId: getDocument
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Document" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ---------------------------------------------------------------------------
  # Pages & Upload
  # ---------------------------------------------------------------------------
  /documents/{document_id}/pages:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [Pages & Upload]
      summary: Add a page (image)
      description: Add a single image page. Call multiple times for multi-page documents.
      operationId: addPage
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
                  description: JPEG / PNG / TIFF
                page_no:
                  type: integer
                  minimum: 1
                  description: Optional explicit page number. Assigned sequentially if omitted.
      responses:
        "201":
          description: Page added
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Page" }
        "400":
          description: Invalid file format
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: INPUT_INVALID_FORMAT
                message: "Unsupported image format."
                request_id: req_01HE...
        "413":
          description: Too large
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: INPUT_TOO_LARGE
                message: "Image exceeds max size."
                request_id: req_01HE...

  /documents/{document_id}/upload-pdf:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [Pages & Upload]
      summary: Upload a PDF (server-side page splitting)
      operationId: uploadPdf
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [pdf]
              properties:
                pdf: { type: string, format: binary }
      responses:
        "201":
          description: Pages created
          content:
            application/json:
              schema:
                type: object
                required: [pages]
                properties:
                  pages:
                    type: array
                    items: { $ref: "#/components/schemas/Page" }

  /documents/{document_id}/pages/{page_no}:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
      - { name: page_no, in: path, required: true, schema: { type: integer, minimum: 1 } }
    get:
      tags: [Pages & Upload]
      summary: Get page metadata
      description: Original image URLs are only returned to authorized roles.
      operationId: getPage
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Page" }

  # ---------------------------------------------------------------------------
  # Pre-check
  # ---------------------------------------------------------------------------
  /documents/{document_id}/precheck:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [Pre-check]
      summary: Run input quality & locatability gate
      operationId: runPrecheck
      responses:
        "202":
          description: Accepted (may run async)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QualityResult" }
        "422":
          description: Pre-check failed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: PRECHECK_FAILED
                message: "Could not analyze document."
                request_id: req_01HE...
    get:
      tags: [Pre-check]
      summary: Get the latest pre-check result
      operationId: getPrecheck
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QualityResult" }

  # ---------------------------------------------------------------------------
  # AI jobs
  # ---------------------------------------------------------------------------
  /documents/{document_id}/ai-jobs:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [AI jobs]
      summary: Start an external AI (OCR / extraction) job
      description: Starts an asynchronous job against the configured provider (e.g. `google_document_ai`).
      operationId: startAIJob
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                ai_request_config_version:
                  type: string
                  description: Pin the AI-side configuration version (processor + params).
      responses:
        "202":
          description: Job submitted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AIJob" }
        "409":
          description: Duplicate job
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: AI_JOB_CONFLICT
                message: "A job is already running for this document."
                request_id: req_01HE...
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: AI_RATE_LIMITED
                message: "Too many requests. Retry later."
                request_id: req_01HE...
        "502":
          description: AI provider error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: AI_PROVIDER_ERROR
                message: "External AI returned an error."
                request_id: req_01HE...
        "504":
          description: AI provider timeout
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: AI_PROVIDER_TIMEOUT
                message: "External AI timed out."
                request_id: req_01HE...

  /ai-jobs/{job_id}:
    parameters:
      - { name: job_id, in: path, required: true, schema: { type: string } }
    get:
      tags: [AI jobs]
      summary: Get AI job status
      operationId: getAIJob
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AIJob" }

  # ---------------------------------------------------------------------------
  # Validation (intermediate)
  # ---------------------------------------------------------------------------
  /documents/{document_id}/validate:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [Validation]
      summary: Run normalize + validate + score
      description: |
        Requires the AI job to be in `Done` state. Produces field-level
        validation results and a document-level score. Does not finalize the
        document — REVIEW_REQUIRED items will route to HITL.
      operationId: runValidate
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Result" }
        "409":
          description: Invalid state transition
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: INVALID_STATE_TRANSITION
                message: "Validate requires AI job to be Done."
                request_id: req_01HE...

  /documents/{document_id}/results:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    get:
      tags: [Validation]
      summary: Get intermediate validation results (authorized roles only)
      description: |
        Returns the current `Result` for a document. **Intermediate results are
        strictly internal** — they must never be forwarded to external business
        systems. Use `/final` for external consumption.
      operationId: getResults
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Result" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # ---------------------------------------------------------------------------
  # Review & Double-check (HITL)
  # ---------------------------------------------------------------------------
  /documents/{document_id}/review-tasks:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    get:
      tags: [Review & Double-check (HITL)]
      summary: Get open review tasks for a document
      description: |
        Returns fields that require human review, with evidence (bounding box,
        candidate values, reason codes, target_id and match_info so the
        reviewer knows *what was being searched*).
      operationId: getReviewTasks
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [tasks]
                properties:
                  tasks:
                    type: array
                    items: { $ref: "#/components/schemas/ReviewTask" }

  /documents/{document_id}/reviews:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [Review & Double-check (HITL)]
      summary: Submit a primary review (Reviewer)
      operationId: submitReview
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReviewSubmission" }
      responses:
        "200":
          description: Review recorded
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewRecord" }
        "422":
          description: Invalid review payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: REVIEW_PAYLOAD_INVALID
                message: "Reason code is required when correcting a value."
                request_id: req_01HE...

  /documents/{document_id}/approvals:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [Review & Double-check (HITL)]
      summary: Submit a secondary approval (Approver)
      description: |
        The Approver **must be a different user** from the Reviewer. If their
        judgments disagree, the document is escalated to a Supervisor.
      operationId: submitApproval
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApprovalSubmission" }
      responses:
        "200":
          description: Approval recorded
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewRecord" }
        "403":
          description: Same user for review and approval
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: DOUBLECHECK_SAME_USER
                message: "Reviewer and Approver must be different users."
                request_id: req_01HE...
        "409":
          description: Reviewer/Approver disagree
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: DOUBLECHECK_MISMATCH
                message: "Review and approval do not match. Escalate to Supervisor."
                request_id: req_01HE...

  # ---------------------------------------------------------------------------
  # Finalization
  # ---------------------------------------------------------------------------
  /documents/{document_id}/finalize:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    post:
      tags: [Finalization]
      summary: Finalize a document
      description: |
        Transitions the document to `Finalized`. Allowed only when all required
        gates are passed (validation OK or review/approval completed, no
        unresolved NOT_FOUND / AMBIGUOUS_MATCH on required targets).
      operationId: finalizeDocument
      responses:
        "200":
          description: Finalized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Document" }
        "409":
          description: Not eligible for finalization
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: REVIEW_REQUIRED
                message: "Pending review items must be resolved before finalizing."
                request_id: req_01HE...

  /documents/{document_id}/final:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    get:
      tags: [Finalization]
      summary: Get the Finalized data (external consumption)
      description: |
        Returns the Finalized payload for a document. If the document is not
        yet in `Finalized` state, the endpoint responds with `409 NOT_FINALIZED`.
        The payload always carries `project_id` and `target_profile_version`
        for reproducibility.
      operationId: getFinal
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FinalResult" }
        "409":
          description: Not finalized yet
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
              example:
                code: NOT_FINALIZED
                message: "Document is not Finalized."
                request_id: req_01HE...

  # ---------------------------------------------------------------------------
  # Audit & Config
  # ---------------------------------------------------------------------------
  /documents/{document_id}/audit-logs:
    parameters: [{ $ref: "#/components/parameters/DocumentId" }]
    get:
      tags: [Audit & Config]
      summary: Get audit log entries for a document
      operationId: getAuditLogs
      parameters:
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { name: type, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/AuditLogEntry" }

  /configs:
    get:
      tags: [Audit & Config]
      summary: List applied configuration versions
      operationId: listConfigs
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/ConfigVersions" }

# =============================================================================
# Components
# =============================================================================
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    DocumentId:
      name: document_id
      in: path
      required: true
      schema: { type: string, example: doc_01HE9Q2T8M0000000000 }
    ProjectId:
      name: project_id
      in: path
      required: true
      schema: { type: string, example: prj_inv_jp }
    ProfileId:
      name: profile_id
      in: path
      required: true
      schema: { type: string, example: tp_invoice }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, example: 9a0b2c7e-... }
      description: Client-supplied key to safely retry mutating requests.

  responses:
    Unauthorized:
      description: Missing or invalid token
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
          example:
            code: AUTH_UNAUTHORIZED
            message: "Invalid or expired token."
            request_id: req_01HE...
    Forbidden:
      description: Authenticated but not authorized
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
          example:
            code: AUTH_FORBIDDEN
            message: "Missing required role."
            request_id: req_01HE...
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

  schemas:
    DocumentStatus:
      type: string
      enum:
        - Uploaded
        - Prechecked
        - AIProcessed
        - Interpreted
        - Validated
        - ReviewRequired
        - Reviewed
        - Approved
        - Finalized
        - Error

    FieldType:
      type: string
      enum: [string, integer, decimal, date, datetime, enum, structured, boolean]

    ValidationStatus:
      type: string
      enum: [VALID, REVIEW_REQUIRED, INVALID]

    OverallStatus:
      type: string
      enum: [OK, REVIEW_REQUIRED, NG]

    QualityStatus:
      type: string
      enum: [QUALITY_OK, QUALITY_WARNING, QUALITY_NG]

    MatchType:
      type: string
      enum: [exact, regex, nearby, layout, table]

    FieldReasonCode:
      type: string
      description: |
        Field-level validation reason codes (stored in `FieldValidation.reason_codes`).
      enum:
        - FORMAT_ERROR
        - TYPE_MISMATCH
        - OUT_OF_RANGE
        - MISSING_REQUIRED
        - INCONSISTENT
        - LOW_CONFIDENCE
        - NORMALIZATION_FAILED
        - FORBIDDEN_PATTERN
        - NOT_FOUND
        - AMBIGUOUS_MATCH
        - LOW_EVIDENCE

    ApiError:
      type: object
      required: [code, message, request_id]
      properties:
        code:
          type: string
          description: Machine-readable error code (see error codes reference).
        message: { type: string }
        details:
          type: object
          additionalProperties: true
          description: |
            Optional structured info (e.g. `field_key`, `page_no`) for clients
            to surface per-field errors.
        request_id: { type: string }

    Project:
      type: object
      required: [project_id, name, created_at]
      properties:
        project_id: { type: string, example: prj_inv_jp }
        name: { type: string, example: "Invoice Extraction — JP" }
        created_at: { type: string, format: date-time }
      example:
        project_id: prj_inv_jp
        name: "Invoice Extraction — JP"
        created_at: "2026-01-25T02:14:08Z"

    ProjectCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }

    TargetProfile:
      type: object
      description: |
        Defines *what to find* in a document. A profile can exist as a mutable
        draft; once a version is published it is immutable and pinned onto
        any document that is processed with it.
      required: [profile_id, project_id, name, version, targets, created_at]
      properties:
        profile_id: { type: string, example: tp_invoice }
        project_id: { type: string, example: prj_inv_jp }
        name: { type: string, example: "Standard Invoice" }
        version:
          type: string
          description: Immutable once published (`draft` while mutable).
          example: "3"
        targets:
          type: array
          items: { $ref: "#/components/schemas/TargetDefinition" }
        created_at: { type: string, format: date-time }
      example:
        profile_id: tp_invoice
        project_id: prj_inv_jp
        name: "Standard Invoice"
        version: "3"
        created_at: "2026-01-25T02:14:08Z"
        targets:
          - target_id: t_total_amount
            display_name: "Total amount"
            required: true
            expected_type: decimal
            synonyms: ["合計", "総額", "Total", "Amount due"]
            anchors: ["合計金額", "ご請求額"]
            match_rules:
              - "regex:^\\d{1,3}(,\\d{3})*(\\.\\d{2})?$"
              - "layout:same_row_right_of(合計)"
          - target_id: t_invoice_date
            display_name: "Invoice date"
            required: true
            expected_type: date
            synonyms: ["発行日", "請求日", "Invoice date"]
            anchors: ["発行日", "日付"]

    TargetProfileDraft:
      type: object
      required: [name, targets]
      properties:
        name: { type: string }
        targets:
          type: array
          items: { $ref: "#/components/schemas/TargetDefinition" }

    TargetDefinition:
      type: object
      required: [target_id, display_name, required, expected_type]
      properties:
        target_id: { type: string, example: t_total_amount }
        display_name: { type: string, example: "Total amount" }
        required: { type: boolean }
        expected_type: { $ref: "#/components/schemas/FieldType" }
        synonyms:
          type: array
          items: { type: string }
          description: Wording variants / synonyms used when matching anchors.
          example: ["合計", "総額", "Total", "Amount due"]
        anchors:
          type: array
          items: { type: string }
          description: Nearby labels used to disambiguate candidates.
          example: ["合計金額", "ご請求額"]
        match_rules:
          type: array
          items: { type: string }
          description: Regex / proximity / structural rule expressions.
          example:
            - "regex:^\\d{1,3}(,\\d{3})*(\\.\\d{2})?$"
            - "layout:same_row_right_of(合計)"

    TargetProfileDiff:
      type: object
      properties:
        from: { type: string }
        to: { type: string }
        added:
          type: array
          items: { $ref: "#/components/schemas/TargetDefinition" }
        removed:
          type: array
          items: { $ref: "#/components/schemas/TargetDefinition" }
        modified:
          type: array
          items:
            type: object
            properties:
              target_id: { type: string }
              before: { $ref: "#/components/schemas/TargetDefinition" }
              after: { $ref: "#/components/schemas/TargetDefinition" }

    Document:
      type: object
      required: [document_id, project_id, target_profile_version, status, created_at]
      properties:
        document_id: { type: string, example: doc_01HE9Q2T8M0000000000 }
        project_id: { type: string }
        target_profile_version:
          type: string
          description: Target Profile version pinned at processing start.
        external_ref:
          type: string
          description: Free-form external system reference (optional).
        status: { $ref: "#/components/schemas/DocumentStatus" }
        quality: { $ref: "#/components/schemas/QualityResult" }
        scores: { $ref: "#/components/schemas/ScoreSummary" }
        config_versions: { $ref: "#/components/schemas/ConfigVersions" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
      example:
        document_id: doc_01HE9Q2T8M0000000000
        project_id: prj_inv_jp
        target_profile_version: "3"
        external_ref: erp-invoice-99812
        status: Validated
        quality:
          quality_status: QUALITY_OK
          quality_score: 94
          reason_codes: []
        scores:
          document_score: 86
          quality_score: 94
        config_versions:
          quality_rules_version: "1.2"
          ai_request_config_version: gdai-invoice-parser@2
          normalization_rules_version: "4.1"
          validation_rules_version: "4.1"
          scoring_rules_version: "2.0"
          target_profile_version: "3"
        created_at: "2026-03-04T09:21:02Z"
        updated_at: "2026-03-04T09:22:17Z"

    DocumentCreate:
      type: object
      required: [project_id, target_profile_version]
      properties:
        project_id: { type: string }
        target_profile_version: { type: string }
        external_ref: { type: string }
      example:
        project_id: prj_inv_jp
        target_profile_version: "3"
        external_ref: erp-invoice-99812

    Page:
      type: object
      required: [page_no]
      properties:
        page_no: { type: integer, minimum: 1 }
        width: { type: integer }
        height: { type: integer }
        image_url:
          type: string
          description: Signed URL; returned only to authorized roles.
      example:
        page_no: 1
        width: 2480
        height: 3508
        image_url: "https://storage.smartmatch.gloding.com/signed/doc_01HE.../p1.png?..."

    QualityResult:
      type: object
      required: [quality_status, quality_score, reason_codes]
      properties:
        quality_status: { $ref: "#/components/schemas/QualityStatus" }
        quality_score: { type: integer, minimum: 0, maximum: 100 }
        reason_codes:
          type: array
          items: { type: string }
          example: [LOW_RESOLUTION, BLUR]
        guidance_message:
          type: string
          description: End-user-oriented guidance for re-capture.
      example:
        quality_status: QUALITY_WARNING
        quality_score: 72
        reason_codes: [LOW_RESOLUTION]
        guidance_message: "Please re-capture at 300 DPI or higher for better results."

    AIJob:
      type: object
      required: [job_id, document_id, status, provider, started_at]
      properties:
        job_id: { type: string }
        document_id: { type: string }
        status:
          type: string
          enum: [Submitted, Running, Done, Error]
        provider:
          type: string
          example: google_document_ai
        ai_request_config_version: { type: string }
        started_at: { type: string, format: date-time }
        ended_at: { type: string, format: date-time }
      example:
        job_id: job_01HE9Q5R2F...
        document_id: doc_01HE9Q2T8M0000000000
        status: Done
        provider: google_document_ai
        ai_request_config_version: gdai-invoice-parser@2
        started_at: "2026-03-04T09:21:10Z"
        ended_at: "2026-03-04T09:21:42Z"

    Result:
      type: object
      description: Intermediate validation result (internal / authorized only).
      required:
        - document_id
        - project_id
        - target_profile_version
        - fields
        - validation_summary
        - scores
      properties:
        document_id: { type: string }
        project_id: { type: string }
        target_profile_version: { type: string }
        fields:
          type: array
          items: { $ref: "#/components/schemas/FieldResult" }
        validation_summary: { $ref: "#/components/schemas/ValidationSummary" }
        scores: { $ref: "#/components/schemas/ScoreSummary" }
      example:
        document_id: doc_01HE9Q2T8M0000000000
        project_id: prj_inv_jp
        target_profile_version: "3"
        validation_summary:
          overall_status: REVIEW_REQUIRED
          invalid_count: 0
          review_required_count: 1
        scores:
          document_score: 86
          quality_score: 94
        fields:
          - field_key: invoice_total
            target_id: t_total_amount
            target_profile_version: "3"
            field_type: decimal
            raw_value: "¥12,400"
            normalized_value: "12400"
            ai_confidence: 0.72
            system_score: 68
            candidates:
              - { value: "12400", ai_confidence: 0.72 }
              - { value: "1240", ai_confidence: 0.18 }
            evidence:
              page_no: 1
              bbox: { x: 0.61, y: 0.48, w: 0.12, h: 0.03 }
              context_text: "合計金額 ¥12,400"
            match_info:
              match_type: nearby
              anchor_terms: ["合計金額"]
              candidate_count: 2
            validation:
              status: REVIEW_REQUIRED
              reason_codes: [LOW_CONFIDENCE]
          - field_key: invoice_date
            target_id: t_invoice_date
            target_profile_version: "3"
            field_type: date
            raw_value: "令和6年3月4日"
            normalized_value: "2024-03-04"
            ai_confidence: 0.97
            system_score: 95
            evidence:
              page_no: 1
              bbox: { x: 0.72, y: 0.08, w: 0.16, h: 0.02 }
            match_info:
              match_type: exact
              anchor_terms: ["発行日"]
              candidate_count: 1
            validation:
              status: VALID
              reason_codes: []

    FieldResult:
      type: object
      required: [field_key, target_id, raw_value, normalized_value, validation]
      properties:
        field_key: { type: string, example: invoice_total }
        target_id: { type: string, example: t_total_amount }
        target_profile_version: { type: string }
        field_type: { $ref: "#/components/schemas/FieldType" }
        raw_value: { type: string }
        normalized_value: { type: string }
        candidates:
          type: array
          items: { $ref: "#/components/schemas/CandidateValue" }
        evidence: { $ref: "#/components/schemas/Evidence" }
        match_info: { $ref: "#/components/schemas/MatchInfo" }
        ai_confidence: { type: number, minimum: 0, maximum: 1 }
        system_score: { type: integer, minimum: 0, maximum: 100 }
        validation: { $ref: "#/components/schemas/FieldValidation" }

    CandidateValue:
      type: object
      required: [value, ai_confidence]
      properties:
        value: { type: string }
        ai_confidence: { type: number, minimum: 0, maximum: 1 }

    Evidence:
      type: object
      required: [page_no, bbox]
      properties:
        page_no: { type: integer, minimum: 1 }
        bbox:
          type: object
          required: [x, y, w, h]
          properties:
            x: { type: number }
            y: { type: number }
            w: { type: number }
            h: { type: number }
        context_text:
          type: string
          description: Surrounding text for reviewer/auditor context.

    MatchInfo:
      type: object
      description: How a candidate was located in the document.
      properties:
        match_type: { $ref: "#/components/schemas/MatchType" }
        anchor_terms:
          type: array
          items: { type: string }
        candidate_count: { type: integer, minimum: 0 }

    FieldValidation:
      type: object
      required: [status]
      properties:
        status: { $ref: "#/components/schemas/ValidationStatus" }
        reason_codes:
          type: array
          items: { $ref: "#/components/schemas/FieldReasonCode" }

    ValidationSummary:
      type: object
      required: [overall_status]
      properties:
        overall_status: { $ref: "#/components/schemas/OverallStatus" }
        invalid_count: { type: integer, minimum: 0 }
        review_required_count: { type: integer, minimum: 0 }

    ScoreSummary:
      type: object
      properties:
        document_score: { type: integer, minimum: 0, maximum: 100 }
        quality_score: { type: integer, minimum: 0, maximum: 100 }

    ConfigVersions:
      type: object
      properties:
        quality_rules_version: { type: string }
        ai_request_config_version: { type: string }
        normalization_rules_version: { type: string }
        validation_rules_version: { type: string }
        scoring_rules_version: { type: string }
        target_profile_version: { type: string }
      example:
        quality_rules_version: "1.2"
        ai_request_config_version: gdai-invoice-parser@2
        normalization_rules_version: "4.1"
        validation_rules_version: "4.1"
        scoring_rules_version: "2.0"
        target_profile_version: "3"

    ReviewTask:
      type: object
      required: [field_key, target_id, validation]
      properties:
        field_key: { type: string }
        target_id: { type: string }
        display_name: { type: string }
        candidates:
          type: array
          items: { $ref: "#/components/schemas/CandidateValue" }
        evidence: { $ref: "#/components/schemas/Evidence" }
        match_info: { $ref: "#/components/schemas/MatchInfo" }
        validation: { $ref: "#/components/schemas/FieldValidation" }
        system_score: { type: integer, minimum: 0, maximum: 100 }
      example:
        field_key: invoice_total
        target_id: t_total_amount
        display_name: "Total amount"
        system_score: 68
        candidates:
          - { value: "12400", ai_confidence: 0.72 }
          - { value: "1240", ai_confidence: 0.18 }
        evidence:
          page_no: 1
          bbox: { x: 0.61, y: 0.48, w: 0.12, h: 0.03 }
          context_text: "合計金額 ¥12,400"
        match_info:
          match_type: nearby
          anchor_terms: ["合計金額"]
          candidate_count: 2
        validation:
          status: REVIEW_REQUIRED
          reason_codes: [LOW_CONFIDENCE]

    ReviewSubmission:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            required: [field_key, action]
            properties:
              field_key: { type: string }
              action:
                type: string
                enum: [ACCEPT, CORRECT, REJECT, UNKNOWN]
              corrected_value:
                type: string
                description: Required when `action=CORRECT`.
              reason_code:
                type: string
                description: Reviewer-selected reason code.
              note: { type: string }
      example:
        items:
          - field_key: invoice_total
            action: CORRECT
            corrected_value: "12400"
            reason_code: LOW_CONFIDENCE
            note: "Confirmed against 合計金額 row."

    ApprovalSubmission:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            required: [field_key, action]
            properties:
              field_key: { type: string }
              action:
                type: string
                enum: [APPROVE, DISAGREE]
              reason_code: { type: string }
              note: { type: string }
      example:
        items:
          - field_key: invoice_total
            action: APPROVE

    ReviewRecord:
      type: object
      properties:
        document_id: { type: string }
        project_id: { type: string }
        target_profile_version: { type: string }
        reviewer_id: { type: string }
        approver_id: { type: string }
        decisions:
          type: array
          items:
            type: object
            properties:
              field_key: { type: string }
              target_id: { type: string }
              before: { type: string }
              after: { type: string }
              action: { type: string }
              reason_code: { type: string }
              decided_at: { type: string, format: date-time }
              decided_by: { type: string }
      example:
        document_id: doc_01HE9Q2T8M0000000000
        project_id: prj_inv_jp
        target_profile_version: "3"
        reviewer_id: user_reviewer_01
        approver_id: user_approver_07
        decisions:
          - field_key: invoice_total
            target_id: t_total_amount
            before: "1240"
            after: "12400"
            action: CORRECT
            reason_code: LOW_CONFIDENCE
            decided_at: "2026-03-04T09:32:11Z"
            decided_by: user_reviewer_01

    FinalResult:
      type: object
      description: |
        External-facing Finalized payload. Always carries `project_id` and
        `target_profile_version` for audit / reproducibility.
      required: [document_id, project_id, target_profile_version, fields, finalized_at]
      properties:
        document_id: { type: string }
        project_id: { type: string }
        target_profile_version: { type: string }
        external_ref: { type: string }
        fields:
          type: array
          items:
            type: object
            required: [field_key, target_id, value]
            properties:
              field_key: { type: string }
              target_id: { type: string }
              value: { type: string }
              field_type: { $ref: "#/components/schemas/FieldType" }
              evidence: { $ref: "#/components/schemas/Evidence" }
        finalized_at: { type: string, format: date-time }
        finalized_by: { type: string }
      example:
        document_id: doc_01HE9Q2T8M0000000000
        project_id: prj_inv_jp
        target_profile_version: "3"
        external_ref: erp-invoice-99812
        finalized_at: "2026-03-04T09:35:02Z"
        finalized_by: user_approver_07
        fields:
          - field_key: invoice_total
            target_id: t_total_amount
            value: "12400"
            field_type: decimal
            evidence:
              page_no: 1
              bbox: { x: 0.61, y: 0.48, w: 0.12, h: 0.03 }
          - field_key: invoice_date
            target_id: t_invoice_date
            value: "2024-03-04"
            field_type: date
            evidence:
              page_no: 1
              bbox: { x: 0.72, y: 0.08, w: 0.16, h: 0.02 }

    AuditLogEntry:
      type: object
      required: [audit_id, document_id, project_id, type, actor_id, at]
      properties:
        audit_id: { type: string }
        document_id: { type: string }
        project_id: { type: string }
        target_profile_version: { type: string }
        type:
          type: string
          example: state_transition
          description: |
            Examples: `state_transition`, `ai_job_submitted`, `ai_job_done`,
            `review_submitted`, `approval_submitted`, `finalize`, `delete`.
        actor_id: { type: string }
        payload:
          type: object
          additionalProperties: true
        at: { type: string, format: date-time }
      example:
        audit_id: aud_01HE9QR...
        document_id: doc_01HE9Q2T8M0000000000
        project_id: prj_inv_jp
        target_profile_version: "3"
        type: state_transition
        actor_id: user_reviewer_01
        payload:
          from: ReviewRequired
          to: Reviewed
        at: "2026-03-04T09:32:11Z"
