curated_feed.select

Turning the model’s judgements into the day’s selection.

Every number is computed here: the arithmetic, the score floor, the count cap and the ordering. The model supplies only what needs an article read, and is never told the floor or the cap — knowing them could only tempt it to pre-filter or pad. Sub-scores stay on disk, so tuning a coefficient is an offline sweep over a saved corpus rather than an API call per iteration.

This module also resolves what the response got wrong. A response can claim an article twice, name an index that does not exist, offer two discretionary picks, or quietly omit an article altogether. None of those should cost a day’s feed, so each is resolved by a stated rule, recorded in anomalies, and reported.

Attributes

Classes

Resolution

The response with its conflicts settled, ready to be scored.

Scored

One graded article with its arithmetic worked out.

Functions

resolve(→ Resolution)

Settle every conflicting claim, recording each resolution.

count_preference(→ int)

One point per distinct topic, capped.

compute_adjustments(→ list[curated_feed.models.Adjustment])

The ±1-bounded terms of scoring specification §2.3, in a fixed order.

score_one(→ Scored)

score = consequence + preference + adjustments.

build_rationale(→ str)

The line that is published: consequence, preference, and fixed adjustments.

select_day(→ curated_feed.models.SelectionResult)

Score, gate the unreadable, apply the floor, add the pick, cap, and rank.

format_summary(→ str)

Human-readable run report.

build_parser(→ argparse.ArgumentParser)

main(→ int)

Module Contents

curated_feed.select.SELECTION_STAGE = 'selections'
curated_feed.select.BELOW_FLOOR = 'below-floor'
curated_feed.select.OVER_CAP = 'over-cap'
curated_feed.select.PAYWALLED = 'paywalled'
curated_feed.select.UNACCOUNTED = 'unaccounted'
curated_feed.select.RECURRENCE = 'recurrence'
curated_feed.select.STALENESS = 'staleness'
curated_feed.select.PENALTY_PREFIX = 'penalty:'
class curated_feed.select.Resolution[source]

The response with its conflicts settled, ready to be scored.

graded: list[curated_feed.models.Graded] = []
dropped: dict[str, list[int]]
unaccounted: list[int] = []
anomalies: list[str] = []
class curated_feed.select.Scored[source]

One graded article with its arithmetic worked out.

graded: curated_feed.models.Graded
candidate: curated_feed.models.Candidate
preference: int
adjustments: list[curated_feed.models.Adjustment]
score: float
curated_feed.select.resolve(response: curated_feed.models.Response, count: int) → Resolution[source]

Settle every conflicting claim, recording each resolution.

Precedence, in order: a drop beats a grade, because hard exclusions are gates that nothing may bypass; a grade beats being someone else’s duplicate; and the first group to claim a duplicate keeps it.

curated_feed.select.count_preference(topics: collections.abc.Sequence[str], *, cap: int) → int[source]

One point per distinct topic, capped.

Names are not checked against the policy: an invented topic inflates the score, which is a policy problem rather than a parsing one, and no model text is published. Case and whitespace are folded so one topic cannot count twice.

>>> count_preference(["Privacy", "privacy", "Linux"], cap=3)
2
curated_feed.select.compute_adjustments(graded: curated_feed.models.Graded, candidate: curated_feed.models.Candidate, *, policy: curated_feed.config.PolicySettings, now: datetime.datetime) → list[curated_feed.models.Adjustment][source]

The ±1-bounded terms of scoring specification §2.3, in a fixed order.

curated_feed.select.score_one(graded: curated_feed.models.Graded, candidate: curated_feed.models.Candidate, *, policy: curated_feed.config.PolicySettings, now: datetime.datetime) → Scored[source]

score = consequence + preference + adjustments.

curated_feed.select.build_rationale(graded: curated_feed.models.Graded, *, preference: int, adjustments: collections.abc.Sequence[curated_feed.models.Adjustment], policy: curated_feed.config.PolicySettings) → str[source]

The line that is published: consequence, preference, and fixed adjustments.

Built from numbers and from the fixed reason codes this module assigns itself, never from unvalidated model text: the feed is public, so the privacy property is structural rather than a rule someone has to remember. Topics and penalty rule names are both free text a model wrote about the reader’s own interests, so neither reaches here; they stay in the selection log, which is not public.

discretionary_reason is the one piece of model text that does reach here. policy/prompt.md §7 has the model write it about the article — what would otherwise have kept it out — never about the reader, so publishing it verbatim carries none of the risk that keeps topics and penalty rule names out.

>>> build_rationale(
...     Graded(index=1, consequence=3), preference=0, adjustments=[],
...     policy=PolicySettings(),
... )
'Consequence 3 of 3. Preference 0.'
>>> build_rationale(
...     Graded(index=1, consequence=2, independent_sources=3), preference=2,
...     adjustments=[Adjustment(reason=RECURRENCE, value=1.0)],
...     policy=PolicySettings(),
... )
'Consequence 2 of 3. Preference +2. Reported independently by 3 outlets (+1).'
>>> build_rationale(
...     Graded(
...         index=1, consequence=1, discretionary=True,
...         discretionary_reason="an unfamiliar angle on a familiar story",
...     ),
...     preference=0, adjustments=[], policy=PolicySettings(),
... )
'Consequence 1 of 3. Preference 0. Discretionary pick: an unfamiliar angle on a familiar story.'
>>> build_rationale(
...     Graded(index=1, consequence=3), preference=0, adjustments=[],
...     policy=PolicySettings(rationale_mode="none"),
... )
''
curated_feed.select.select_day(response: curated_feed.models.Response, candidates: collections.abc.Sequence[curated_feed.models.Candidate], *, policy: curated_feed.config.PolicySettings, now: datetime.datetime, run_date: datetime.date, verdict_by_id: dict[str, curated_feed.models.Verdict] | None = None) → curated_feed.models.SelectionResult[source]

Score, gate the unreadable, apply the floor, add the pick, cap, and rank.

The order is the one in the scoring specification, and it matters: the discretionary pick is added after the floor so it can bypass it, and the cap is applied last so it is a cap on what would otherwise be published.

The paywall gate comes first of all, and it is a gate rather than a penalty: an article the reader cannot open is worth nothing whatever it is about, so no score and no discretionary pass gets past it. Gating here rather than after the cap is what lets the next article take the freed slot.

verdict_by_id is a parameter rather than a config lookup so that this module keeps reading its inputs from its arguments, and stays testable without a filesystem. Absent, or absent for a given candidate, means readable — the feature is then a no-op and a saved day replays unchanged.

curated_feed.select.format_summary(result: curated_feed.models.SelectionResult, *, path: pathlib.Path) → str[source]

Human-readable run report.

curated_feed.select.build_parser() → argparse.ArgumentParser[source]
curated_feed.select.main(argv: collections.abc.Sequence[str] | None = None) → int[source]