{
  "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": {
        "operationId": "scoreCycle",
        "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.",
        "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.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"
          }
        }
      }
    },
    "/v1/scores/{cycle_id}": {
      "get": {
        "operationId": "getCycleScores",
        "tags": [
          "Scoring"
        ],
        "summary": "Get Cycle Scores",
        "description": "Retrieve the stored result of a particular cycle without scoring again. Historical scores stay unchanged even when a new cycle refreshes or removes a post. A recovered late response is not eligible for its original decision cycle. Current process-local state is lost on restart; durable recovery must be verified before rehearsal.",
        "parameters": [
          {
            "name": "cycle_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{8}T\\d{4}$",
              "example": "20260929T0205"
            },
            "description": "The cycle id sent in the original call."
          }
        ],
        "responses": {
          "200": {
            "description": "The response stored for this cycle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CycleResponse"
                },
                "example": {
                  "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"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/images/{gall_id}/{post_no}/{NN}": {
      "put": {
        "operationId": "putImage",
        "tags": [
          "Images"
        ],
        "summary": "Upload an Image",
        "description": "Proposed separate upload for each candidate image, using the original body position. HTTPS versus S3 and transfer details remain open. Repeating identical bytes is supported. A separate route does not establish that scoring can ignore missing image features; readiness and fallback behavior must be verified with the delivered model. Current image size protection is 5 MiB, not an agreed business quota.",
        "parameters": [
          {
            "name": "gall_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "example_gallery"
            },
            "description": "Gallery id."
          },
          {
            "name": "post_no",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 31900001
            },
            "description": "Post number."
          },
          {
            "name": "NN",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{2,}$",
              "example": "01"
            },
            "description": "The image's position in the original body: one-based, zero-padded to at least two digits, gaps allowed. The v1.3 layout `<gall_id>/<post_no>/<NN>.jpg`."
          },
          {
            "name": "X-Content-SHA256",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "Hex SHA-256 of the body. When present we verify it, and a mismatch is rejected with 400."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary",
                "description": "The JPEG, 1024 px on the long edge. Smaller originals are not upscaled."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored."
          },
          "200": {
            "description": "Already stored with the same bytes."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "409": {
            "description": "An image already exists at this key with different bytes. The existing image is preserved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "conflict",
                    "message": "image already exists with different bytes"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/outcomes": {
      "post": {
        "operationId": "postOutcomes",
        "tags": [
          "Outcomes"
        ],
        "summary": "Send Outcomes",
        "description": "Not in scope for trial. Reference endpoint for later integration: 1/6/24/48-hour board readings, hides, deletions, picker and score at placement. Daily operator outcomes may be shared separately by agreement during rehearsal; this endpoint is not a required trial call. Sampling ages differ from upload frequency. Payload and live-trial event-versus-hourly delivery remain to be agreed. The current handler replaces a placement record instead of merging partial readings, so current tests must send complete known snapshots. Storage is process-local.",
        "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/OutcomesRequest"
              },
              "example": {
                "outcomes": [
                  {
                    "placement_id": "example-4390001",
                    "gall_id": "example_gallery",
                    "post_no": 31899120,
                    "tier": "main",
                    "placed_at": "2026-09-29T02:40:00+09:00",
                    "picked_by": {
                      "type": "operator"
                    },
                    "score_at_placement": 0.62,
                    "views_at_placement": 1875,
                    "up_at_placement": 40,
                    "down_at_placement": 3,
                    "comment_at_placement": 21,
                    "readings": [
                      {
                        "age_h": 1,
                        "observed_at": "2026-09-29T03:40:00+09:00",
                        "views": 9120,
                        "up": 66,
                        "down": 8,
                        "comment_cnt": 30
                      }
                    ],
                    "hidden_at": null,
                    "deleted_at": null,
                    "deleted_by": null
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutcomesResponse"
                },
                "example": {
                  "received": 1,
                  "created": 1,
                  "updated": 0
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-trial-scope": "reference-only"
      }
    },
    "/v1/weights": {
      "put": {
        "operationId": "putWeights",
        "tags": [
          "Weights"
        ],
        "summary": "Set Weights",
        "description": "Not in scope for trial. This client-facing management endpoint is reference material for later integration; Surf configures the initial trial weights. Proposed versioned weight changes without retraining, effective next cycle. Access and activation rules need confirmation. Scores must reflect their actual model and weights; the current cached-score implementation needs correction before client-driven changes are enabled. Non-negative weights with a positive views, comments or up weight are current validation rules, not client-agreed ranges.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WeightsRequest"
              },
              "example": {
                "version": "w-1",
                "weights": {
                  "views": 1,
                  "comments": 0,
                  "up": 0,
                  "downvote_share": 0
                },
                "note": "first trial: views only"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WeightsAck"
                },
                "example": {
                  "version": "w-1",
                  "first_cycle_time": "2026-09-29T02:10:00+09:00"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-trial-scope": "reference-only"
      },
      "get": {
        "operationId": "getWeights",
        "tags": [
          "Weights"
        ],
        "summary": "Get Weights",
        "description": "Not in scope for trial. This client-facing management endpoint is reference material for later integration; Surf configures the initial trial weights. Inspect current and previous weight configurations. Verify score provenance before enabling client-driven changes: current version labels can accompany earlier cached scores. The intended starting model is delivered Model 0924 with down-vote weight 0.",
        "responses": {
          "200": {
            "description": "Current and previous weights.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WeightsState"
                },
                "example": {
                  "current": {
                    "version": "w-2",
                    "weights": {
                      "views": 1,
                      "comments": 0.5,
                      "up": 0.25,
                      "downvote_share": 0
                    },
                    "note": "add comments and up-votes",
                    "set_at": "2026-09-30T03:00:00+09:00",
                    "first_cycle_time": "2026-09-30T03:05:00+09:00"
                  },
                  "previous": {
                    "version": "w-1",
                    "weights": {
                      "views": 1,
                      "comments": 0,
                      "up": 0,
                      "downvote_share": 0
                    },
                    "note": "first trial: views only",
                    "set_at": "2026-09-29T02:02:11+09:00",
                    "first_cycle_time": "2026-09-29T02:05:00+09:00"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-trial-scope": "reference-only"
      }
    },
    "/v1/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Health"
        ],
        "summary": "Health",
        "description": "Current service information and last recorded cycle. Public in the current implementation. An ok response does not establish real-model readiness, durable delivery or an independent 15-minute no-data alert. Verify alert delivery with the rehearsal failure drill.",
        "responses": {
          "200": {
            "description": "Service state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "ok",
                  "time": "2026-09-29T02:05:30+09:00",
                  "api_version": "1",
                  "model_version": "m0924-weight0",
                  "weights_version": "w-1",
                  "last_cycle_id": "20260929T0205",
                  "last_cycle_received_at": "2026-09-29T02:05:03+09:00"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": []
      }
    }
  },
  "components": {
    "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."
      }
    },
    "schemas": {
      "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."
      },
      "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."
      },
      "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."
      },
      "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."
      },
      "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"
        ]
      },
      "PickedByOperator": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "operator"
            ],
            "example": "operator",
            "description": "The post was chosen by an operator."
          }
        },
        "required": [
          "type"
        ],
        "description": "Chosen by an operator."
      },
      "PickedByModel": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "model"
            ],
            "example": "model",
            "description": "The post was chosen from the model's ranking."
          },
          "model_version": {
            "type": "string",
            "example": "m0924-weight0",
            "description": "`model_version` of the scores the choice was made from."
          },
          "weights_version": {
            "type": "string",
            "example": "w-1",
            "description": "`weights_version` of those scores."
          },
          "cycle_id": {
            "type": "string",
            "pattern": "^\\d{8}T\\d{4}$",
            "example": "20260929T0240",
            "description": "The cycle whose response the choice was made from."
          }
        },
        "required": [
          "type",
          "model_version",
          "weights_version",
          "cycle_id"
        ],
        "description": "Chosen from the model's ranking."
      },
      "Reading": {
        "type": "object",
        "properties": {
          "age_h": {
            "type": "integer",
            "enum": [
              1,
              6,
              24,
              48
            ],
            "example": 1,
            "description": "Hours since placement: 1, 6, 24 or 48."
          },
          "observed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the counters were read.",
            "example": "2026-09-29T03:40:00+09:00",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          },
          "views": {
            "type": "integer",
            "minimum": 0,
            "description": "Cumulative views on the board copy.",
            "example": 9120,
            "nullable": true
          },
          "up": {
            "type": "integer",
            "minimum": 0,
            "description": "Cumulative up-votes on the board copy.",
            "example": 66,
            "nullable": true
          },
          "down": {
            "type": "integer",
            "minimum": 0,
            "description": "Cumulative down-votes on the board copy.",
            "example": 8,
            "nullable": true
          },
          "comment_cnt": {
            "type": "integer",
            "minimum": 0,
            "description": "Cumulative comments on the board copy.",
            "example": 30,
            "nullable": true
          }
        },
        "required": [
          "age_h",
          "observed_at",
          "views",
          "up",
          "down",
          "comment_cnt"
        ],
        "description": "Board-copy counters sampled at 1, 6, 24 or 48 hours after placement. Placement baselines support net-growth calculations. These are sampling ages, not upload frequencies; outcome and training-label semantics need payload review."
      },
      "OutcomeRecord": {
        "type": "object",
        "properties": {
          "placement_id": {
            "type": "string",
            "minLength": 1,
            "example": "example-4390001",
            "description": "DCinside placement identifier (delivered selected_id). Current implementation replaces the stored record on the same ID; send a complete known snapshot until incremental merging is agreed and implemented."
          },
          "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
          },
          "tier": {
            "type": "string",
            "enum": [
              "main",
              "light",
              "night",
              "app"
            ],
            "example": "main",
            "description": "The board the post was placed on. Delivered files: `rtb_head_tag`."
          },
          "placed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the post went onto the board. Also fixes the day and time block used for outcome percentiles. Delivered files: `exposed_at`.",
            "example": "2026-09-29T02:40:00+09:00",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          },
          "picked_by": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PickedByOperator"
              },
              {
                "$ref": "#/components/schemas/PickedByModel"
              }
            ],
            "description": "Who chose the post: `{\"type\": \"operator\"}`, or `{\"type\": \"model\", \"model_version\", \"weights_version\", \"cycle_id\"}`."
          },
          "score_at_placement": {
            "type": "number",
            "nullable": true,
            "example": 0.62,
            "description": "The publish score the post had when placed. Lets us compare model and operator picks on the same board."
          },
          "views_at_placement": {
            "type": "integer",
            "minimum": 0,
            "example": 1875,
            "description": "Exact, from the board's `origin_hit`. Net views are readings minus this. Delivered files: `rtb_start_counts.views_at_placement`."
          },
          "up_at_placement": {
            "type": "integer",
            "minimum": 0,
            "description": "Last gallery-side up-votes before placement.",
            "example": 40,
            "nullable": true
          },
          "down_at_placement": {
            "type": "integer",
            "minimum": 0,
            "description": "Last gallery-side down-votes before placement.",
            "example": 3,
            "nullable": true
          },
          "comment_at_placement": {
            "type": "integer",
            "minimum": 0,
            "description": "Last gallery-side comment count before placement.",
            "example": 21,
            "nullable": true
          },
          "readings": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "$ref": "#/components/schemas/Reading"
            },
            "description": "Known readings at 1, 6, 24 and 48 hours after placement. Daily rehearsal delivery may include several ages. On current repeated-placement updates include all known readings, because the handler replaces rather than merges."
          },
          "hidden_after_exposure": {
            "type": "boolean",
            "nullable": true,
            "example": true,
            "description": "The label DCinside has today: the post was hidden after it was exposed. Send it now, and use `hidden_at` once timed hides exist."
          },
          "hidden_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the post was taken down. DCinside's admin does not record this today, so send `null` until timed hides are logged through the unpublish path.",
            "example": "2026-09-29T05:00:00+09:00",
            "nullable": true,
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the post was deleted, if it was. Delivered files: `deletion.deleted_at`.",
            "example": "2026-09-29T06:00:00+09:00",
            "nullable": true,
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          },
          "deleted_by": {
            "type": "string",
            "enum": [
              "author",
              "admin",
              "auto",
              null
            ],
            "nullable": true,
            "example": null,
            "description": "Who deleted it: `author`, `admin` or `auto`. Delivered files: `deletion.deleted_by`."
          },
          "deleted_relative": {
            "type": "string",
            "enum": [
              "before_decision",
              "after_decision",
              null
            ],
            "nullable": true,
            "example": null,
            "description": "Whether the deletion came before or after the operator's decision. Only `after_decision` is an outcome of the placement. Delivered files: `deletion.deleted_relative`."
          }
        },
        "required": [
          "placement_id",
          "gall_id",
          "post_no",
          "tier",
          "placed_at",
          "picked_by",
          "views_at_placement",
          "readings"
        ],
        "description": "One placement of a post on a board, with its outcome readings."
      },
      "OutcomesRequest": {
        "type": "object",
        "properties": {
          "outcomes": {
            "type": "array",
            "maxItems": 5000,
            "items": {
              "$ref": "#/components/schemas/OutcomeRecord"
            },
            "description": "One record per placement. Current 5,000-record protection is an engineering limit, not an agreed quota. Daily operator-placed outcomes support rehearsal."
          }
        },
        "required": [
          "outcomes"
        ]
      },
      "OutcomesResponse": {
        "type": "object",
        "properties": {
          "received": {
            "type": "integer",
            "minimum": 0,
            "example": 1,
            "description": "Records in the request."
          },
          "created": {
            "type": "integer",
            "minimum": 0,
            "example": 1,
            "description": "Records stored as new placements."
          },
          "updated": {
            "type": "integer",
            "minimum": 0,
            "example": 0,
            "description": "Records that matched an existing `placement_id` and updated it."
          }
        },
        "required": [
          "received",
          "created",
          "updated"
        ]
      },
      "WeightSet": {
        "type": "object",
        "properties": {
          "views": {
            "type": "number",
            "minimum": 0,
            "example": 1,
            "description": "Weight on `p_views`."
          },
          "comments": {
            "type": "number",
            "minimum": 0,
            "example": 0,
            "description": "Weight on `p_comments`."
          },
          "up": {
            "type": "number",
            "minimum": 0,
            "example": 0,
            "description": "Weight on `p_up`."
          },
          "downvote_share": {
            "type": "number",
            "minimum": 0,
            "example": 0,
            "description": "Weight on the predicted down-vote share. It is subtracted, so send it as a positive number."
          },
          "hide": {
            "type": "integer",
            "enum": [
              0
            ],
            "example": 0,
            "description": "Reserved for the later hide head. Must be 0 or left out."
          },
          "deletion": {
            "type": "integer",
            "enum": [
              0
            ],
            "example": 0,
            "description": "Reserved for the later deletion head. Must be 0 or left out."
          }
        },
        "required": [
          "views",
          "comments",
          "up",
          "downvote_share"
        ],
        "description": "Publish-score weights. At least one of `views`, `comments` and `up` must be above 0."
      },
      "WeightsRequest": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._-]{1,64}$",
            "example": "w-1",
            "description": "Proposed stable configuration version. Current engineering protection: letters, digits, dot, underscore or hyphen, up to 64 characters. Same version/body can be retried; different configuration under that version conflicts."
          },
          "weights": {
            "$ref": "#/components/schemas/WeightSet"
          },
          "note": {
            "type": "string",
            "maxLength": 500,
            "example": "first trial: views only",
            "description": "Optional annotation; current implementation protection is 500 characters, not a client-agreed business limit."
          }
        },
        "required": [
          "version",
          "weights"
        ]
      },
      "WeightsAck": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "example": "w-1",
            "description": "The accepted version, scheduled for first_cycle_time; not necessarily active at receipt time."
          },
          "first_cycle_time": {
            "type": "string",
            "format": "date-time",
            "description": "The first cycle the weights apply to, which is the next one.",
            "example": "2026-09-29T02:10:00+09:00",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          }
        },
        "required": [
          "version",
          "first_cycle_time"
        ]
      },
      "WeightsRecord": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "example": "w-1",
            "description": "Weights version."
          },
          "weights": {
            "$ref": "#/components/schemas/WeightSet"
          },
          "note": {
            "type": "string",
            "nullable": true,
            "example": "first trial: views only",
            "description": "The note sent with the weights, or `null`."
          },
          "set_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the weights were accepted.",
            "example": "2026-09-29T02:02:11+09:00",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          },
          "first_cycle_time": {
            "type": "string",
            "format": "date-time",
            "description": "The first cycle the weights applied to.",
            "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})$"
          }
        },
        "required": [
          "version",
          "weights",
          "set_at",
          "first_cycle_time"
        ]
      },
      "WeightsState": {
        "type": "object",
        "properties": {
          "current": {
            "$ref": "#/components/schemas/WeightsRecord"
          },
          "previous": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WeightsRecord"
              }
            ],
            "nullable": true,
            "description": "The version before the current one, or `null` if there is none."
          }
        },
        "required": [
          "current",
          "previous"
        ]
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ],
            "example": "ok",
            "description": "Current service status. The existing handler returns ok; this does not establish real-model readiness, alert operation or durable state."
          },
          "time": {
            "type": "string",
            "format": "date-time",
            "description": "Our clock.",
            "example": "2026-09-29T02:05:30+09:00",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          },
          "api_version": {
            "type": "string",
            "example": "1",
            "description": "API major version."
          },
          "model_version": {
            "type": "string",
            "example": "m0924-weight0",
            "description": "Reported model label. Confirm the loaded artifact separately; the current adapter label does not prove Model 0924 is loaded."
          },
          "weights_version": {
            "type": "string",
            "example": "w-1",
            "description": "Weights in force."
          },
          "last_cycle_id": {
            "type": "string",
            "nullable": true,
            "example": "20260929T0205",
            "description": "The last cycle call received, or `null` before the first."
          },
          "last_cycle_received_at": {
            "type": "string",
            "format": "date-time",
            "description": "When that call arrived, or `null` before the first.",
            "example": "2026-09-29T02:05:03+09:00",
            "nullable": true,
            "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
          }
        },
        "required": [
          "status",
          "time",
          "api_version",
          "model_version",
          "weights_version",
          "last_cycle_id",
          "last_cycle_received_at"
        ]
      },
      "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"
        ]
      },
      "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"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        },
        "required": [
          "error"
        ]
      }
    },
    "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"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No such record.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "no cycle 20260929T0205"
              }
            }
          }
        }
      },
      "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"
              }
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "The body is not a JPEG.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unsupported_media_type",
                "message": "expected image/jpeg"
              }
            }
          }
        }
      },
      "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"
              }
            }
          }
        }
      }
    }
  },
  "x-contract-status": "proposed-scoring-only-rehearsal",
  "x-rehearsal-decisions": {
    "cycle_interval": "Proposed initial five minutes within the client-stated 2–5-minute range; current server supports five-minute boundaries only.",
    "pending_rescoring": "Surf proposes refreshing every pending candidate each new cycle; client confirmation pending.",
    "response_deadline": "To be agreed separately from the first-score target; no default SLA.",
    "first_score": "Client draft: at least 99% within two cycles of entry into the DCinside queue, measured from extracted_at.",
    "outcomes": "Daily operator outcomes may be shared separately by agreement. The outcomes endpoint is Not in scope for trial; it is not a required trial call or a client-draft pass condition.",
    "readiness": "Real model integration, timing/refresh changes, score provenance, durable recovery and independent no-data alerts must be verified before starting.",
    "weight_management": "Client-facing weight-management endpoints are Not in scope for trial. Surf configures the delivered Model 0924 down-vote-weight-0 setting."
  }
}
