Curation v2 step 5: deep assessment, utility, diversified shortlist

assess.rs replaces score.rs (DEEP_INSTRUCTIONS, representative sample
with [BEGINNING]/[MIDDLE]/[END], facets, cached deep rows with --rescore
bypass), rank.rs adds the utility blend over present signals with gate
ramps and the leader-clustered shortlist (cap 2 → 3 → uncapped, protected
top-N, exploration reserve), editor.rs replaces select.rs with the §13
rendering and utility-ordered fallbacks. ScoredArticle is gone; Candidate
is the only flow type. deep_batch_size replaces score_batch_size.

Started by Codex (cut off by its usage limit mid-verification) and
finished by a Claude agent from docs/plans/curation-v2-briefs/step5.md;
reviewed against plan §12–§13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1rCLQeKBgnBo3oTgHuTMe
This commit is contained in:
2026-09-02 16:04:43 +00:00
co-authored by Claude Fable 5.1
parent 10f4afd091
commit 05a74a0dcf
22 changed files with 2710 additions and 1298 deletions
+79 -33
View File
@@ -205,6 +205,18 @@ pub fn serialize_candidate(candidate: &Candidate) -> String {
.insert("triage".into(), (triage.interest / 10.0).clamp(0.0, 1.0));
value.present.insert("triage".into(), true);
}
if let Some(deep) = candidate.assessment.deep.as_ref() {
value.raw.insert("quality".into(), deep.quality);
value.raw.insert("fit".into(), deep.fit);
value
.norm
.insert("quality".into(), (deep.quality / 10.0).clamp(0.0, 1.0));
value
.norm
.insert("fit".into(), (deep.fit / 10.0).clamp(0.0, 1.0));
value.present.insert("quality".into(), true);
value.present.insert("fit".into(), true);
}
serde_json::to_string(&value).unwrap_or_else(|_| "{}".into())
}
@@ -251,7 +263,7 @@ impl ExplainRow {
serde_json::from_str(&self.signals_json).ok()
}
/// Utility when step 5 has written it, else the preliminary blend.
/// Utility once the deep set has been ranked, else the preliminary blend.
pub fn score(&self) -> Option<f64> {
self.utility
.or_else(|| self.signals().and_then(|signals| signals.blend()))
@@ -305,7 +317,8 @@ pub async fn explain_row(
Ok(row.as_ref().map(ExplainRow::from_row))
}
/// The top `limit` rows by utility-or-blend that were not selected (§15.2).
/// The top `limit` rows that were not selected, by utility, falling back to
/// the preliminary blend for rows the ranker never reached (§15.2).
pub async fn near_misses(
db: &Db,
run_id: i64,
@@ -413,24 +426,34 @@ pub async fn render_explain(db: &Db, row: &ExplainRow) -> Result<String, sqlx::E
if !assessments.is_empty() {
let _ = writeln!(out, "assessments:");
for assessment in assessments {
let _ = writeln!(
out,
" {} · {} · score {} · fit {} · kind {} · category {} · paywalled={} · {}",
assessment.get::<String, _>("stage"),
assessment.get::<String, _>("model"),
fmt_opt(assessment.get::<Option<f64>, _>("score")),
fmt_opt(assessment.get::<Option<f64>, _>("fit")),
assessment
.get::<Option<String>, _>("kind")
.unwrap_or_else(|| "—".into()),
assessment
.get::<Option<String>, _>("category")
.unwrap_or_else(|| "—".into()),
assessment.get::<i64, _>("paywalled_guess") != 0,
assessment
.get::<Option<String>, _>("rationale")
.unwrap_or_default(),
);
let stage = assessment.get::<String, _>("stage");
let model = assessment.get::<String, _>("model");
let score = fmt_opt(assessment.get::<Option<f64>, _>("score"));
let rationale = assessment
.get::<Option<String>, _>("rationale")
.unwrap_or_default();
if stage == "deep" {
let _ = writeln!(
out,
" deep · {model} · quality {score} · fit {} · format {} · category {} · paywalled={} · {rationale}",
fmt_opt(assessment.get::<Option<f64>, _>("fit")),
assessment
.get::<Option<String>, _>("kind")
.unwrap_or_else(|| "—".into()),
assessment
.get::<Option<String>, _>("category")
.unwrap_or_else(|| "—".into()),
assessment.get::<i64, _>("paywalled_guess") != 0,
);
} else {
let _ = writeln!(
out,
" triage · {model} · interest {score} · kind {} · {rationale}",
assessment
.get::<Option<String>, _>("kind")
.unwrap_or_else(|| "—".into()),
);
}
if let Some(facets) = assessment.get::<Option<String>, _>("facets_json") {
let _ = writeln!(out, " facets: {facets}");
}
@@ -876,16 +899,21 @@ mod tests {
}
#[tokio::test]
async fn near_misses_rank_by_blend_and_skip_selected_and_excluded() {
let (_dir, db) = db_with_articles(&[1, 2, 3, 4, 5]).await;
async fn near_misses_rank_by_utility_then_blend_and_skip_selected_and_excluded() {
let (_dir, db) = db_with_articles(&[1, 2, 3, 4, 5, 6]).await;
let run_id = db.start_run(date(), Timestamp::now()).await.unwrap();
// Utility decides wherever the ranker wrote one; the preliminary blend
// only stands in for rows the deep set never reached. Article 4's blend
// would put it first, but its utility is the lowest; article 3 never
// got a utility and ranks on its blend.
let rows = [
(1, "selected", None, 0.9),
(2, "shortlisted", Some("not_selected"), 0.7),
(3, "eligible", Some("not_admitted"), 0.95),
(4, "shortlisted", Some("not_selected"), 0.1),
(1, "selected", None, 0.9, Some(90.0)),
(2, "shortlisted", Some("not_selected"), 0.1, Some(70.0)),
(3, "eligible", Some("not_admitted"), 0.95, None),
(4, "shortlisted", Some("not_selected"), 0.99, Some(10.0)),
(6, "assessed", Some("cluster_suppressed"), 0.5, Some(40.0)),
];
for (id, stage, reason, norm) in rows {
for (id, stage, reason, norm, utility) in rows {
let json = serialize_signals(&signals(10.0, norm), false);
write(
&db,
@@ -896,7 +924,7 @@ mod tests {
excluded_reason: reason,
admitted_by: None,
signals_json: &json,
utility: None,
utility,
rank_utility: None,
cluster_id: None,
cluster_rank: None,
@@ -912,18 +940,36 @@ mod tests {
let misses = near_misses(&db, run_id, 10).await.unwrap();
assert_eq!(
misses.iter().map(|row| row.article_id).collect::<Vec<_>>(),
vec![3, 2, 4]
vec![3, 2, 6, 4]
);
let text = explain_near_misses(&db, date(), None, 2).await.unwrap();
assert!(
text.contains("top 2 not selected, by preliminary blend"),
"{text}"
);
assert!(text.contains("top 2 not selected, by utility"), "{text}");
assert!(
text.contains("Article 3 · eligible, not_admitted"),
"{text}"
);
assert!(
text.contains("Article 2 · shortlisted, not_selected"),
"{text}"
);
assert!(!text.contains("Article 4"), "{text}");
// Without any utility the listing says so and orders by the blend.
sqlx::query("UPDATE candidate_runs SET utility = NULL WHERE run_id = ?")
.bind(run_id)
.execute(db.pool())
.await
.unwrap();
let misses = near_misses(&db, run_id, 10).await.unwrap();
assert_eq!(
misses.iter().map(|row| row.article_id).collect::<Vec<_>>(),
vec![4, 3, 6, 2]
);
let text = explain_near_misses(&db, date(), None, 1).await.unwrap();
assert!(
text.contains("top 1 not selected, by preliminary blend"),
"{text}"
);
}
#[tokio::test]