---
title: Distinguish focused from mentioned players on ████ █████
type: task
status: completed
description: >-
  Record which resolved players a feed item is ABOUT versus only mentions, so league per-player
  injury streams surface items about the player first. An LLM focus judge measured precision 0.91
  and recall 0.98 on a 75-item hand-labelled set.
base_uri: user:task/base/distinguish-focused-vs-mentioned-players.md
created_at: '2026-10-10T06:11:26.755Z'
entity_id: 04f43ce4-75fe-4de8-ac7d-ee7520edc978
finished_at: '2026-10-11T01:04:42.746Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[█████████████████████-████-██████████████-███████████]]
  - relates [[████████████████████████-████-██████-████-███-█████████]]
started_at: '2026-10-10T06:47:30.294Z'
tags:
  - user:tag/content-feed.md
  - user:tag/league-xo-football.md
updated_at: '2026-10-11T01:04:48.893Z'
visibility_analyzed_at: '2026-11-05T23:47:53.129Z'
---

## Overview

League's per-player Injuries stream (`GET /api/news?player_ids=<pid>&tag_uris=...nfl-injury.md`) matches any item whose `players` array contains the player, so a twelve-player injury report or a story that names a player in passing lands in every named player's file. Record, per item, which resolved players the item is ABOUT (focus) versus only mentions, and expose it through the content-feed query API so league can filter and rank on it.

Lane boundary: player RESOLUTION (which pid a name is) belongs to [[█████████████████████-████-██████████████-███████████]] and its alias-flywheel session. This task only judges the role of an already-resolved pid; it never adds or removes a pid. Boundary message sent to that session 2026-10-10.

## Background

- `feed_items.players` is a flat sorted jsonb pid array, three-state (NULL out of cohort or unattempted, `[]` terminal, populated). Written by the classification worker in [[██████████████████████-████-███████████████████████████]] via `resolve_players_batch` over title + body + extracted_text.
- League's route projects only `player_mention_count`; its filter is `player_ids` against `players` through the content-feed query layer ([[user:text/base/content-feed-query-api.md]]).
- Volume: 31.5k items carry 2+ players (26k in the last 60 days); about 730 new multi-player items a day.

### Measurement (2026-10-10)

Gold set: 75 injury-tagged NFL items from the last 30 days, stratified by player count (15 single, 30 with 2-3, 20 with 4-8, 10 with 9+), 308 item-player pairs hand-labelled, 218 focus. Committed as `data/benchmarks/player-focus/cases.json`; scored by `cli/content/benchmark-player-focus.mjs`.

| Arm | Focus precision | Focus recall |
| --- | --- | --- |
| Lead-window heuristic (surname in first 400 chars, singletons focus) | 0.95 | 0.75 |
| LLM focus judge, two runs | 0.91 / 0.92 | 0.98 / 0.98 |

- Single-player items are focus 15 of 15, so they skip the model.
- The heuristic loses the subjects of team injury reports and inactives lists, which sit far below the headline (recall 0.38 on the 9+ stratum).
- The judge's residual false positives are concentrated: one links-digest article contributes 8 of about 20, and the rest are judgment calls (the injured starter whose absence is the premise of a backup's story).

## Design

- **Representation**: new column `feed_items.focused_players` jsonb, a sorted subset of `players`, with the same three states. A subset array, rather than a role map, keeps the existing GIN `?|` filter shape and leaves `players` untouched for every current consumer. Focus-set size is the ranking signal: sole subject, then small co-subject sets, then large roundups, then mentions.
- **Production**: `repository/active/content-feed/lib/judge-player-focus.mjs`. Singletons are focus without a call; 2+ players get one thinking:false `content_classifier` call returning the focused subset, filtered to offered pids. A failed call yields no verdict, and the worker leaves the column NULL for the backfill to retry.
- **Worker**: one step after `resolve_players_batch`, plus `focused_players` in `write_back`, NULL outside the football cohort.
- **Staleness**: when the flywheel re-pass rewrites `players`, a row whose `focused_players` is not a subset of `players` is recomputed by the backfill driver.
- **Query API**: `player_role=focus` narrows `player_ids` matching to `focused_players`; the projection adds `focused_players`.
- **League**: owned by the /news injury-view lane (session `█████████████████████████████████████████`), which adds the route param in `api/routes/news.mjs` and wires the Injuries stream. This task hands it the table and column, the values, and how an unjudged item reads.

## Plan

- [x] Migration adding `focused_players` jsonb plus a GIN index; update [[██████████████████-████████]].
- [x] Worker step and write-back; restart `feed-item-classification-worker` on the inference host.
- [x] Backfill driver `cli/content/backfill-player-focus.mjs` (NULL or stale rows, injury-tagged and most recent first), instrumented per the background-job guideline.
- [x] Backfill run drained: `bgjob-player-focus-backfill`, log under `scratch/content-feed/player-focus/`, channel `player-focus-backfill`. Verify with a zero count of 2+ player rows whose `focused_players` is NULL, then delete the scratch slug.
- [x] content-feed-query filter and projection, plus the API doc.
- [x] Message the /news lane once the column is persisted and queryable: table and column, values, and that NULL means unjudged.
- [x] Update [[user:text/base/content-feed.md]] Players section.
