> ## 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.

# How Scoring Works

> The publish score, when a post is scored, what the model reads and how the weights work

<div style={{ maxWidth: 760 }}>
  <h2 style={{ fontSize: "22px", marginBottom: 8 }}>The publish score</h2>

  Each scored post gets one **publish score**. It combines four model outputs with weights that DCinside sets:

  ```text theme={null}
  score = w_v · P(views) + w_c · P(comments) + w_u · P(up) − w_d · downvote_share
  ```

  <Note>
    The first trial uses `views = 1` and every other weight `0`, so the score equals `p_views`. The four outputs are returned next to the score on every row, so DCinside can recompute it or try other weights offline.
  </Note>

  | Field | What it is |
  | - | - |
  | `p_views` | The model's chance that the post lands in the top quarter for views, within its tier, day and time block. |
  | `p_comments` | The same, for comments. |
  | `p_up` | The same, for up-votes. |
  | `downvote_share` | The model's predicted down-vote share for the post. |

  Every response carries `model_version` and `weights_version`, so any score can be traced to the model and the weights that produced it. Weights will be set with [`PUT /v1/weights`](/set-weights), which is not in MVP scope, and apply from the next cycle. Until then they stay at the first-trial setting.

  ***

  <h2 style={{ fontSize: "22px", marginBottom: 8 }}>When a post is scored</h2>

  A new candidate is **waiting** until its due cycle, then scored once:

  ```text theme={null}
  due cycle = ceil5( max(extracted_at, first cycle call that carries the post) + 15 minutes )
  ```

  `ceil5` rounds up to the next five-minute boundary. Once scored, the score is repeated unchanged in every later response until the post appears in `removals` or ages out after 24 hours.

  <CardGroup cols={3}>
    <Card title="scored" icon="check">
      The post has a score. `scored_at_cycle` says which cycle took it.
    </Card>

    <Card title="waiting" icon="hourglass">
      The post is inside its wait. `due_cycle` says when it will be scored.
    </Card>

    <Card title="not_scored" icon="ban">
      The post cannot be scored. `reason` says why.
    </Card>
  </CardGroup>

  | `reason` | When |
  | - | - |
  | `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. |

  ***

  <h2 style={{ fontSize: "22px", marginBottom: 8 }}>What the model reads</h2>

  The model reads 62 tabular inputs, the title text and, from the outcome store, how similar past posts performed. The 62 inputs come from the fields of the cycle call:

  | Group | Inputs | Carried by |
  | - | - | - |
  | Gallery | `gallery_type`, `category_id` | `gall_id`, and the gallery registry we hold |
  | Candidate record | `source`, `manual_flag`, title length, post age, queue age | `candidates[]` and `cycle_time` |
  | Cycle time | hour of day, day of week (KST) | `cycle_time` |
  | Candidate-score table | `memo_size`, `upimg_cnt`, `upimg_height`, recommendations, comments, views and Send-to-Best votes (each the sum of PC, mobile and app), and the age of the snapshot | `candidate_scores[]` |
  | Current engagement | views, up, down, comments, Send-to-Best votes, `status`, the age of the reading, and three ratios: up per view, comments per view, down-vote share | the latest `engagement[]` row |
  | Growth | rate and acceleration of the five counters over 5, 15 and 30 minutes, a valid-window flag for each window, a counter-reset flag, and the number of observations in the last 65 minutes | `engagement[]` over the last 65 minutes, which we accumulate from the cycle calls |

  A growth window counts as valid only if the last reading is within 10 minutes of the cycle and the window starts within 5 minutes of where it should. That is why `engagement` carries one row for every pending post in every cycle, and why the cycle interval starts at five minutes.

  Not read by the model, so not needed: the queue's extraction-time scores, comment tables, current counts of the original post, deletion records and the selected-post fields. `body_html` is optional and the current model does not read it.
</div>
