> ## Documentation Index
> Fetch the complete documentation index at: https://surf-dcinside-api.kr.ask.surf/llms.txt
> Use this file to discover all available pages before exploring further.

# Score a Cycle

> Proposed rehearsal cycle payload and score response

## Proposed rehearsal behavior

DCinside sends new candidates, engagement updates for all pending posts and removals. Surf proposes returning refreshed scores for every pending candidate in the response. Five minutes is the initial cadence proposed for review; it is not an agreed exclusive interval.

<Warning>
  This endpoint reference describes the rehearsal proposal. The deployed implementation's timing and model still need the changes listed in [Rehearsal and acceptance](/rollout#before-starting).
</Warning>

## Request guidance

| Field | Meaning |
| - | - |
| `cycle_id` | Stable request ID. The existing format is `YYYYMMDDTHHMM` in KST. Retry the same ID with the same body. |
| `cycle_time` | Scheduled time of this cycle, with an explicit offset. The current server enforces five-minute boundaries; confirm cadence before rehearsal. |
| `candidates` | All new candidates since the last acknowledged delivery. Send each post's text once. No candidate-count cap. |
| `engagement` | Current readings for every pending candidate, including newly sent candidates. Unknown counters are `null`. |
| `candidate_scores` | Proposed mapping for available admin snapshots. The existing wire shape requires this array, but it may be empty. Review its fields with the delivered model. |
| `removals` | Posts leaving the pending pool: saved, hidden, deleted or aged out. DCinside applies the 24-hour filter and sends aged-out removals. |

`extracted_at` means entry into DCinside's queue. It is the start of the first-score measurement, not the start of a separate wait. Candidate metadata and the exact required-field list remain part of the payload review.

Use the field reference below for proposed types and examples. JSON may be plain or gzip-compressed. Current size and array protections are listed in [Conventions](/conventions#current-service-protections); do not truncate the full queue to fit them.

## Response and retry semantics

The proposed response contains post IDs, final scores, model and weight versions, scoring times and counts. Per-part predictions are optional. New cycles may change a pending post's score; retries of the same completed cycle return its original result.

The response deadline is **to be agreed**. A late response is skipped for that cycle, but its data must still be reconciled. Recover using [`GET /v1/scores/{cycle_id}`](/scores) or replay the original request. See the [worker integration notes](/client) for retry budgeting.

Examples below illustrate the proposed contract, not captured outputs from the current placeholder model.


## OpenAPI

````yaml openapi.json POST /v1/cycles
openapi: 3.0.3
info:
  title: DCinside RTB API
  description: >-
    Proposed contract for DCinside's scoring-only dress rehearsal. Real queue
    candidates go to Surf; DCinside logs would-be picks while operators continue
    publishing. The model does not publish during rehearsal. The intended model
    is the delivered Model 0924 with down-vote weight 0; the deployed
    transport-test adapter is not that artifact. Five-minute cycles, inline
    responses, pending re-scoring, payload details and authentication are Surf
    proposals for confirmation. The response deadline is not yet agreed.
    DCinside's draft first-score target is at least 99% within two cycles of
    queue entry, with no separate scoring wait. This specification describes
    target rehearsal behavior, not deployment readiness. See the rehearsal
    documentation for implementation gaps and acceptance criteria.
  version: 1.1.0-draft
servers:
  - url: https://{host}
    description: Issued by Surf together with the API key.
    variables:
      host:
        default: rtb.example.com
        description: >-
          Host name issued by Surf for the environment (rehearsal or
          production). The default is a placeholder.
security:
  - ApiKey: []
tags:
  - name: Scoring
    description: >-
      Proposed cycle scoring and retrieval for rehearsal; cadence, refresh
      policy and deadline need confirmation.
  - name: Images
    description: >-
      Proposed one-time image transfer; layout and scoring readiness behavior
      need confirmation.
  - name: Outcomes
    description: >-
      Not in scope for trial. Reference for later outcomes-endpoint integration.
      Daily operator outcomes may be shared separately by agreement.
  - name: Weights
    description: >-
      Not in scope for trial. Reference for later client-facing
      weight-management integration. Surf configures the initial trial model and
      weights.
  - name: Health
    description: >-
      Service information; not proof of model readiness, durable state or alert
      delivery.
paths:
  /v1/cycles:
    post:
      tags:
        - Scoring
      summary: Score a Cycle
      description: >-
        Proposed rehearsal cycle: send all new candidates, engagement for every
        pending candidate, available admin snapshots and removals. Surf proposes
        returning refreshed scores for every pending candidate on each new
        cycle. Five minutes is the initial proposal, not an agreed exclusive
        interval. First-score target is measured from queue entry; there is no
        separate scoring wait. Same cycle ID and same body retrieves the
        completed result without recalculation; a new cycle may change scores.
        Response deadline remains to be agreed. The current implementation still
        delays and caches per-post scores and must be updated before rehearsal.
      operationId: scoreCycle
      parameters:
        - name: Content-Encoding
          in: header
          required: false
          schema:
            type: string
            enum:
              - gzip
          description: >-
            Optional compression. Set to gzip only for a gzip-compressed body;
            plain JSON is also supported.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CycleRequest'
            example:
              cycle_id: 20260929T0205
              cycle_time: '2026-09-29T02:05:00+09:00'
              candidates:
                - gall_id: example_gallery
                  post_no: 31900001
                  title: Example original title
                  created_at: '2026-09-29T01:58:12+09:00'
                  extracted_at: '2026-09-29T02:03:40+09:00'
                  source: keyword
                  manual_flag: 0
                  images_in_post: 2
              engagement:
                - gall_id: example_gallery
                  post_no: 31900001
                  observed_at: '2026-09-29T02:04:00+09:00'
                  status: ok
                  views: 412
                  up: 9
                  down: 1
                  comment_cnt: 6
                  rtb_vote: 0
                - gall_id: example_gallery
                  post_no: 31899120
                  observed_at: '2026-09-29T02:04:00+09:00'
                  status: ok
                  views: 1875
                  up: null
                  down: null
                  comment_cnt: 21
                  rtb_vote: 0
              candidate_scores:
                - gall_id: example_gallery
                  post_no: 31900001
                  snapshot_at: '2026-09-29T02:05:00+09:00'
                  recommend_up_cnt: 3
                  recommend_up_cnt_m: 2
                  recommend_up_cnt_a: 1
                  comment_cnt: 2
                  comment_cnt_m: 1
                  comment_cnt_a: 0
                  hit_cnt: 5
                  hit_cnt_m: 3
                  hit_cnt_a: 1
                  dcbest_cnt: 0
                  dcbest_cnt_m: 0
                  dcbest_cnt_a: 0
                  memo_size: 1840
                  upimg_cnt: 2
                  upimg_height: 2410
              removals:
                - gall_id: example_gallery
                  post_no: 31898050
                  reason: hidden
                  decided_at: '2026-09-29T02:01:10+09:00'
      responses:
        '200':
          description: The scores for this cycle.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CycleResponse'
              examples:
                first_scores:
                  summary: >-
                    Illustrative proposal: new candidate scored in its first
                    received cycle
                  value:
                    cycle_id: 20260929T0205
                    cycle_time: '2026-09-29T02:05:00+09:00'
                    received_at: '2026-09-29T02:05:03+09:00'
                    scored_at: '2026-09-29T02:05:09+09:00'
                    model_version: m0924-weight0
                    weights_version: w-1
                    counts:
                      candidates: 1
                      engagement: 2
                      candidate_scores: 1
                      removals: 1
                      scored_now: 2
                      waiting: 0
                      not_scored: 0
                      results: 2
                    missing_feature_share: 0.11
                    warnings: []
                    results:
                      - gall_id: example_gallery
                        post_no: 31899120
                        status: scored
                        score: 0.62
                        scored_at_cycle: '2026-09-29T02:05:00+09:00'
                      - gall_id: example_gallery
                        post_no: 31900001
                        status: scored
                        score: 0.51
                        scored_at_cycle: '2026-09-29T02:05:00+09:00'
                refreshed_and_not_scored:
                  summary: >-
                    Illustrative proposal: scores refresh in the next cycle; one
                    row cannot be scored
                  value:
                    cycle_id: 20260929T0210
                    cycle_time: '2026-09-29T02:10:00+09:00'
                    received_at: '2026-09-29T02:10:02+09:00'
                    scored_at: '2026-09-29T02:10:03+09:00'
                    model_version: m0924-weight0
                    weights_version: w-1
                    counts:
                      candidates: 1
                      engagement: 3
                      candidate_scores: 3
                      removals: 0
                      scored_now: 2
                      waiting: 0
                      not_scored: 1
                      results: 3
                    missing_feature_share: 0
                    warnings: []
                    results:
                      - gall_id: example_gallery
                        post_no: 31899120
                        status: scored
                        score: 0.67
                        scored_at_cycle: '2026-09-29T02:10:00+09:00'
                      - gall_id: example_gallery
                        post_no: 31900001
                        status: scored
                        score: 0.56
                        scored_at_cycle: '2026-09-29T02:10:00+09:00'
                      - gall_id: example_gallery
                        post_no: 31900044
                        status: not_scored
                        score: null
                        reason: missing_title
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/TooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '500':
          $ref: '#/components/responses/ServerError'
        '503':
          $ref: '#/components/responses/ServerError'
components:
  schemas:
    CycleRequest:
      type: object
      properties:
        cycle_id:
          type: string
          pattern: ^\d{8}T\d{4}$
          example: 20260929T0205
          description: >-
            Idempotency key: the cycle time in KST as `YYYYMMDDTHHMM`, for
            example `20260929T0205`. The same id with the same body returns the
            stored response. The same id with a different body returns 409.
        cycle_time:
          type: string
          format: date-time
          description: >-
            Scheduled cycle time with an explicit offset; normalize to KST for
            cycle_id. Five minutes is proposed initially within the stated
            2–5-minute range. The current server enforces five-minute boundaries
            and rejects future cycles; changing cadence requires implementation
            support.
          example: '2026-09-29T02:05:00+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        candidates:
          type: array
          items:
            $ref: '#/components/schemas/Candidate'
          description: >-
            All posts that became candidates since the last successful call.
            Each post is sent once. May be empty. There is no candidate-count
            cap; the complete JSON body must fit within 16 MiB after
            decompression. Pending candidates are retained across cycles until
            removed.
        engagement:
          type: array
          maxItems: 5000
          items:
            $ref: '#/components/schemas/EngagementRow'
          description: >-
            One row for every pending candidate, every cycle. Pending means sent
            in this or an earlier call and not yet in `removals`. The current
            5,000-row cap is an engineering protection, not an agreed business
            quota; validate it against full-queue delivery.
        candidate_scores:
          type: array
          maxItems: 5000
          items:
            $ref: '#/components/schemas/CandidateScoreRow'
          description: >-
            Available admin snapshots, when present. Existing wire shape
            requires the array but permits []. Confirm feature mapping and
            requiredness with the client and delivered model. The current
            5,000-row cap is an engineering protection, not an agreed business
            quota; validate it against full-queue delivery.
        removals:
          type: array
          maxItems: 5000
          items:
            $ref: '#/components/schemas/Removal'
          description: >-
            Posts leaving the pending pool since the last successful call. May
            be empty. The current 5,000-row cap is an engineering protection,
            not an agreed business quota; validate it against full-queue
            delivery.
      required:
        - cycle_id
        - cycle_time
        - candidates
        - engagement
        - candidate_scores
        - removals
      description: >-
        Proposed rehearsal wire shape. Required arrays and extra metadata need
        payload review. Current other-array and request-size protections are
        engineering limits, not client quotas.
    CycleResponse:
      type: object
      properties:
        cycle_id:
          type: string
          pattern: ^\d{8}T\d{4}$
          example: 20260929T0205
          description: Echoed from the request.
        cycle_time:
          type: string
          format: date-time
          description: Echoed from the request.
          example: '2026-09-29T02:05:00+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        received_at:
          type: string
          format: date-time
          description: Our clock when the call arrived.
          example: '2026-09-29T02:05:03+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        scored_at:
          type: string
          format: date-time
          description: >-
            Proposed meaning: actual scoring completion time. The current
            handler records its pre-processing time here; fix and verify before
            using this field for latency measurement. Measure client receipt
            time separately.
          example: '2026-09-29T02:05:09+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        model_version:
          type: string
          example: m0924-weight0
          description: >-
            Identity of the actual model that produced the results. The current
            transport adapter uses a Model-0924-like label; that label alone is
            not proof the delivered artifact is loaded.
        weights_version:
          type: string
          example: w-1
          description: >-
            Weights that actually produced this cycle's scores. Verify that
            refreshed scores and their version match; do not relabel cached
            scores.
        counts:
          $ref: '#/components/schemas/Counts'
        missing_feature_share:
          type: number
          minimum: 0
          maximum: 1
          example: 0.11
          description: >-
            Implementation-specific diagnostic. The current adapter counts
            missing request counters, not missing values across a verified
            62-feature model. It is not a rehearsal acceptance metric.
        warnings:
          type: array
          items:
            type: string
          example: []
          description: >-
            Implementation warnings. Unknown fields are not guaranteed to be
            listed. An empty list does not prove model or data readiness.
        results:
          type: array
          items:
            $ref: '#/components/schemas/ResultRow'
          description: >-
            One result per pending candidate under the proposed refresh policy.
            Scores may change across new cycles; replay of a completed cycle is
            unchanged. Removed posts are absent from subsequent cycles.
      required:
        - cycle_id
        - cycle_time
        - received_at
        - scored_at
        - model_version
        - weights_version
        - counts
        - missing_feature_share
        - warnings
        - results
    Candidate:
      type: object
      properties:
        gall_id:
          type: string
          minLength: 1
          description: Post key, together with `post_no`.
          example: example_gallery
        post_no:
          type: integer
          minimum: 1
          description: >-
            Post number. Unique only within a gallery, so a post is always keyed
            by `gall_id` and `post_no` together.
          example: 31900001
        title:
          type: string
          example: Example original title
          description: >-
            Original extraction-time title before operator edits. Delivered
            field: queue.subject. Feature preprocessing and missing-title
            handling must be verified with Model 0924; they are not
            client-defined API limits.
        created_at:
          type: string
          format: date-time
          description: >-
            Post creation time, used for age handling. RFC 3339 with explicit
            offset; null is accepted by the current parser but cannot supply a
            valid age. Delivered field: queue.created_at.
          example: '2026-09-29T01:58:12+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
          nullable: true
        extracted_at:
          type: string
          format: date-time
          description: >-
            Entry into the DCinside extraction queue. Start of the first-score
            target: the client draft asks for at least 99% within two cycles of
            entering. No separate scoring wait. Null cannot establish that
            measurement. Delivered field: queue.extracted_at.
          example: '2026-09-29T02:03:40+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
          nullable: true
        source:
          type: string
          enum:
            - score
            - setting
            - keyword
            - vote
            - manual
            - unknown
          example: keyword
          description: >-
            Queue source using the delivered enumeration. Whether this metadata
            is mandatory and how to handle unknown sources remain part of
            payload review. Delivered field: queue.source.
        manual_flag:
          type: integer
          enum:
            - 0
            - 1
          example: 0
          description: >-
            1 when registered by hand from external monitoring. Proposed
            metadata mapping from queue.manual_flag; confirm requiredness during
            payload review.
        images_in_post:
          type: integer
          minimum: 0
          example: 2
          description: >-
            Image count in the original body. Proposed upload-reconciliation
            metadata from v1.3 image_manifest.images_in_post; confirm
            requiredness during payload review.
        image_positions:
          type: array
          uniqueItems: true
          items:
            type: integer
            minimum: 1
          example:
            - 1
            - 2
          description: >-
            Original one-based body positions expected for upload, preserving
            gaps for unavailable source images. Proposed mapping from v1.3
            image_manifest.image_index.
        body_html:
          type: string
          example: <p>Example body</p>
          description: >-
            Original body text/HTML, delivered as queue.body_html. Text is sent
            once. Confirm body representation and model use during integration;
            do not infer optional source content from this draft field.
        gallery_type:
          type: string
          enum:
            - main
            - minor
            - mini
          example: minor
          description: >-
            Gallery type metadata. Confirm whether it is supplied directly or
            resolved through a gallery registry, including new and unknown
            galleries.
        category_id:
          type: integer
          example: 12
          description: >-
            Gallery category identifier from DCinside. Confirm the registry and
            mapping; there is no client-agreed numeric category range of 1–47.
      required:
        - gall_id
        - post_no
        - title
        - created_at
        - extracted_at
        - source
        - manual_flag
        - images_in_post
      description: >-
        Proposed new-candidate wire shape based on delivered data. Send text
        once after queue entry. Required metadata fields need client payload
        review.
    EngagementRow:
      type: object
      properties:
        gall_id:
          type: string
          minLength: 1
          description: Post key, together with `post_no`.
          example: example_gallery
        post_no:
          type: integer
          minimum: 1
          description: >-
            Post number. Unique only within a gallery, so a post is always keyed
            by `gall_id` and `post_no` together.
          example: 31900001
        observed_at:
          type: string
          format: date-time
          description: >-
            Time these counters were observed, with explicit offset, at or
            before cycle_time. Preserve source precision. Feature-window and
            freshness rules must be verified with the model and the agreed
            cadence. Delivered field: engagement_ts.ts.
          example: '2026-09-29T02:04:00+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        status:
          type: string
          enum:
            - ok
            - missing
            - nomap
          example: ok
          description: >-
            Delivered observation state: ok, missing or nomap. The current
            adapter excludes missing/nomap rows from scoring. Confirm
            missing-data handling with the real model.
        views:
          type: integer
          minimum: 0
          description: >-
            Current view count. Keep null for an unknown reading. Surf can
            derive engagement history from successive observations; feature
            details must match the delivered model.
          example: 412
          nullable: true
        up:
          type: integer
          minimum: 0
          description: >-
            Recommendations, not the Send-to-Best counter. In v1 and v1.1 of the
            delivered files this column held the wrong counter, and v1.2
            corrected it.
          example: 9
          nullable: true
        down:
          type: integer
          minimum: 0
          description: Down-votes. Feeds the down-vote share input.
          example: 1
          nullable: true
        comment_cnt:
          type: integer
          minimum: 0
          description: Comment count.
          example: 6
          nullable: true
        rtb_vote:
          type: integer
          minimum: 0
          description: >-
            Send-to-Best votes, distinct from recommendations (up). This
            additional wire field is proposed for review, not a separate client
            requirement.
          example: 0
          nullable: true
      required:
        - gall_id
        - post_no
        - observed_at
        - status
        - views
        - up
        - down
        - comment_cnt
        - rtb_vote
      description: Current counters of one pending post, as read from the gallery.
    CandidateScoreRow:
      type: object
      properties:
        gall_id:
          type: string
          minLength: 1
          description: Post key, together with `post_no`.
          example: example_gallery
        post_no:
          type: integer
          minimum: 1
          description: >-
            Post number. Unique only within a gallery, so a post is always keyed
            by `gall_id` and `post_no` together.
          example: 31900001
        snapshot_at:
          type: string
          format: date-time
          description: >-
            Time the admin candidate-score table was read, at or before
            cycle_time. Delivered field: candidate_score_ts.snapshot_ts.
          example: '2026-09-29T02:05:00+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        recommend_up_cnt:
          type: integer
          minimum: 0
          description: >-
            Over-baseline recommendations, PC part. Proposed source mapping;
            confirm feature aggregation with the delivered model.
          example: 3
          nullable: true
        recommend_up_cnt_m:
          type: integer
          minimum: 0
          description: >-
            Over-baseline recommendations, mobile part. Proposed source mapping;
            confirm feature aggregation with the delivered model.
          example: 2
          nullable: true
        recommend_up_cnt_a:
          type: integer
          minimum: 0
          description: >-
            Over-baseline recommendations, app part. Proposed source mapping;
            confirm feature aggregation with the delivered model.
          example: 1
          nullable: true
        comment_cnt:
          type: integer
          minimum: 0
          description: >-
            Over-baseline comments, PC part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 2
          nullable: true
        comment_cnt_m:
          type: integer
          minimum: 0
          description: >-
            Over-baseline comments, mobile part. Proposed source mapping;
            confirm feature aggregation with the delivered model.
          example: 1
          nullable: true
        comment_cnt_a:
          type: integer
          minimum: 0
          description: >-
            Over-baseline comments, app part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 1
          nullable: true
        hit_cnt:
          type: integer
          minimum: 0
          description: >-
            Over-baseline views, PC part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 5
          nullable: true
        hit_cnt_m:
          type: integer
          minimum: 0
          description: >-
            Over-baseline views, mobile part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 4
          nullable: true
        hit_cnt_a:
          type: integer
          minimum: 0
          description: >-
            Over-baseline views, app part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 1
          nullable: true
        dcbest_cnt:
          type: integer
          minimum: 0
          description: >-
            Send-to-Best votes, PC part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 0
          nullable: true
        dcbest_cnt_m:
          type: integer
          minimum: 0
          description: >-
            Send-to-Best votes, mobile part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 0
          nullable: true
        dcbest_cnt_a:
          type: integer
          minimum: 0
          description: >-
            Send-to-Best votes, app part. Proposed source mapping; confirm
            feature aggregation with the delivered model.
          example: 0
          nullable: true
        memo_size:
          type: integer
          minimum: 0
          description: >-
            Body size as the admin table counts it. A model input as delivered,
            so there is nothing to compute on DCinside's side.
          example: 1840
          nullable: true
        upimg_cnt:
          type: integer
          minimum: 0
          description: Uploaded image count from the same table. An input.
          example: 2
          nullable: true
        upimg_height:
          type: integer
          minimum: 0
          maximum: 32767
          nullable: true
          example: 2410
          description: >-
            Total uploaded-image height as delivered, capped at 32,767 in the
            source export. This is a source-field convention, not a queue or
            image-count limit.
      required:
        - gall_id
        - post_no
        - snapshot_at
      description: >-
        Optional available admin-snapshot data carried in the proposed
        candidate_scores array. Verify the required features against Model 0924;
        the array may be empty.
    Removal:
      type: object
      properties:
        gall_id:
          type: string
          minLength: 1
          description: Post key, together with `post_no`.
          example: example_gallery
        post_no:
          type: integer
          minimum: 1
          description: >-
            Post number. Unique only within a gallery, so a post is always keyed
            by `gall_id` and `post_no` together.
          example: 31900001
        reason:
          type: string
          enum:
            - saved
            - hidden
            - deleted
            - aged_out
          example: hidden
          description: >-
            Why the post leaves the pending pool: `saved` (placed on a board),
            `hidden`, `deleted`, or `aged_out` (over 24 hours: DCinside applies
            a 24-hour age filter and sends a removal signal when a pending
            candidate crosses it). It stops being scored and stops appearing in
            responses. Delivered files: `queue.outcome`, `deletion`.
        tier:
          type: string
          enum:
            - main
            - light
            - night
            - app
          example: main
          description: >-
            Board tier. The current handler requires it for saved removals; this
            additional payload requirement needs confirmation before rehearsal.
        decided_at:
          type: string
          format: date-time
          description: >-
            When the decision or deletion happened. Kept for reconciliation. Not
            a model input. Delivered files: `queue.decided_at`,
            `deletion.deleted_at`.
          example: '2026-09-29T02:01:10+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        reason_code:
          type: string
          example: quota_full
          description: >-
            Optional operator reason code. The code list and delivery mapping
            remain to be agreed; do not infer a fixed enumeration from this
            example.
      required:
        - gall_id
        - post_no
        - reason
        - decided_at
      description: A post leaving the pending pool.
    Counts:
      type: object
      properties:
        candidates:
          type: integer
          minimum: 0
          example: 1
          description: Rows received in `candidates`.
        engagement:
          type: integer
          minimum: 0
          example: 2
          description: Rows received in `engagement`.
        candidate_scores:
          type: integer
          minimum: 0
          example: 1
          description: Rows received in `candidate_scores`.
        removals:
          type: integer
          minimum: 0
          example: 1
          description: Rows received in `removals`.
        scored_now:
          type: integer
          minimum: 0
          example: 1
          description: >-
            Posts scored in this cycle, including re-scoring under the proposed
            refresh policy. This is not a count of unique candidates over the
            full rehearsal.
        waiting:
          type: integer
          minimum: 0
          example: 1
          description: Results with status `waiting`.
        not_scored:
          type: integer
          minimum: 0
          example: 0
          description: Results with status `not_scored`.
        results:
          type: integer
          minimum: 0
          example: 2
          description: >-
            Rows in results for this cycle. Under the proposed refresh policy
            this includes newly computed scores for pending posts; historical
            replay stays unchanged.
      required:
        - candidates
        - engagement
        - candidate_scores
        - removals
        - scored_now
        - waiting
        - not_scored
        - results
      description: Row counts for this cycle.
    ResultRow:
      type: object
      properties:
        gall_id:
          type: string
          minLength: 1
          description: Post key, together with `post_no`.
          example: example_gallery
        post_no:
          type: integer
          minimum: 1
          description: >-
            Post number. Unique only within a gallery, so a post is always keyed
            by `gall_id` and `post_no` together.
          example: 31900001
        status:
          type: string
          enum:
            - scored
            - waiting
            - not_scored
          example: scored
          description: >-
            scored: a usable final score is present. waiting: score not ready,
            without a prescribed age-based delay. not_scored: inspect reason.
            First-score target still starts at queue entry.
        score:
          type: number
          nullable: true
          example: 0.62
          description: >-
            Final score computed by the intended delivered Model 0924 under the
            reported weights. Null unless scored. Current transport-adapter
            outputs must not be treated as trained-model predictions.
        p_views:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
          example: 0.62
          description: >-
            Optional views-head prediction from the real model; the intended
            target is top-quarter board views within the delivered tier/day/time
            grouping. Omit or use null when unavailable; not required for
            client-side final-score computation.
        p_comments:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
          example: 0.41
          description: >-
            Optional comments-head prediction if provided by the verified model
            pipeline. Omit or use null when unavailable; not required for
            client-side final-score computation.
        p_up:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
          example: 0.37
          description: >-
            Optional up-vote-head prediction if provided by the verified model
            pipeline. Omit or use null when unavailable; not required for
            client-side final-score computation.
        downvote_share:
          type: number
          minimum: 0
          maximum: 1
          nullable: true
          example: 0.08
          description: >-
            Optional predicted down-vote share from the real model. The initial
            down-vote penalty weight is 0. Omit or use null when unavailable;
            not required for client-side final-score computation.
        scored_at_cycle:
          type: string
          format: date-time
          description: >-
            Cycle that produced this row score. Under the proposed refresh
            policy it advances when the pending post is re-scored in a new
            cycle; replay of the same completed cycle preserves the original
            value.
          example: '2026-09-29T02:05:00+09:00'
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
        due_cycle:
          type: string
          format: date-time
          description: >-
            Legacy optional field, deprecated in the rehearsal proposal. It must
            not impose an extra waiting period or move the first-score
            measurement away from queue entry.
          pattern: >-
            ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$
          deprecated: true
        reason:
          type: string
          enum:
            - missing_title
            - missing_clock
            - post_unavailable
            - unknown_post
          example: missing_title
          description: >-
            Present when `status` is `not_scored`. `missing_title`: the title is
            empty. `missing_clock`: `created_at` or `extracted_at` is missing.
            `post_unavailable`: the latest engagement status is `missing` or
            `nomap`. `unknown_post`: the post appears in `engagement` without a
            candidate record.
      required:
        - gall_id
        - post_no
        - status
      description: The state of one pending post.
    Error:
      type: object
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
      required:
        - error
    ErrorBody:
      type: object
      properties:
        code:
          type: string
          example: bad_request
          description: Machine-readable code. See Errors.
        message:
          type: string
          example: 2 fields failed validation
          description: Human-readable summary.
        fields:
          type: array
          items:
            $ref: '#/components/schemas/ErrorField'
          description: Every failing field, when the error is about the body.
      required:
        - code
        - message
    ErrorField:
      type: object
      properties:
        path:
          type: string
          example: candidates[0].created_at
          description: Path of the failing field.
        problem:
          type: string
          example: timestamp must include an explicit offset
          description: Description of the validation failure.
      required:
        - path
        - problem
  responses:
    BadRequest:
      description: >-
        Malformed JSON or a request-schema failure. Semantic checks may return
        only the first error; no exhaustive-error guarantee.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: bad_request
              message: 2 fields failed validation
              fields:
                - path: candidates[0].created_at
                  problem: timestamp must include an explicit offset
                - path: engagement[3].views
                  problem: must be a non-negative integer or null
    Unauthorized:
      description: The API key is missing or unknown.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: API key missing or unknown
    Conflict:
      description: The id was already used with a different body. The first body stays.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: cycle_conflict
              message: cycle 20260929T0205 was already received with a different body
    TooLarge:
      description: >-
        Current implementation size protection: 16 MiB JSON after decompression
        or 5 MiB per image. These are engineering protections, not agreed client
        quotas.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: too_large
              message: JSON body exceeds 16 MiB after decompression
    Unprocessable:
      description: >-
        The body is valid JSON and matches the schema, but the values are not
        acceptable. Nothing is stored.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unprocessable
              message: cycle_time cannot be in the future
              fields:
                - path: cycle_time
                  problem: must not be in the future
    ServerError:
      description: >-
        Server failure. Skip late or unusable scores for the decision cycle,
        preserve unacknowledged data and reconcile delivery. Alert delivery must
        be verified separately.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: server_error
              message: internal error
  securitySchemes:
    ApiKey:
      type: apiKey
      name: Authorization
      in: header
      description: >-
        Proposed API key sent as the whole Authorization header value.
        Long-lived credential scope, handoff and rotation need confirmation.
        Per-endpoint scopes are not currently enforced.

````