Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1rCLQeKBgnBo3oTgHuTMe
7.5 KiB
YOUR STEP: 4 — Triage replaces the gate (plan §21 step 4)
Steps 1–3 have landed: three-way votes and rating_events; the Claude editor (Llms { bulk, editor },
AnthropicBackend, per-provider UsageMeters, why lines, the Brief); Voyage embeddings,
signals.rs (interest/knn/feed/social/heuristic, percentile normalizer, preliminary blend),
telemetry.rs (candidate_runs writer), explain, features backfill|prune, and the full
[curation.ranking] config. Read git log -6, then the current src/curate/ (all files),
src/pipeline.rs, src/config.rs, src/db.rs, src/report.rs, src/main.rs, src/types.rs.
Scope (plan §7.3 reuse and churn rules, §8.1 hygiene, §10 triage, §11 admission and exploration,
§12.4 preliminary blend already exists, §15.4 log line parts, §17 DeepSeek row, §19 deep_keep):
- Migration
migrations/0003_drop_scores.sql:DROP TABLE scores;(deferred from step 1). Removedb::upsert_score,recently_low_scored_idsand everyscoresreader. The churn rule now readsarticle_assessments: an article whose latesttriagescore < recent_rejection_floor(3.0) ordeepscore < recent_rejection_floorwithinrecent_rejection_days(7) isrecently_rejected, unless auto-include (§7.3, §8.1). src/curate/triage.rs(§10):TRIAGE_INSTRUCTIONSandTRIAGE_PROMPT_VERSION = 1verbatim from the plan; per-article block exactly as §10 (id, title, feed (category), author, length · excerpt only, opening = first 200 words viaprompt_text+truncate_words,matches interests:fromtop_interestswith z ≥ 1.5 labelled strong (z ≥ 2.5) / weak,closest rated:from neighbours with cosine ≥ 0.55; omit lines with nothing). Batches ofdeepseek.triage_batch_size(25) throughbuffer_unordered(max_concurrent_requests)on the bulk client, budget check before each batch. Tolerant parsing generalized fromscore.rs::parse_score_response: a malformed item never sinks the batch, ids not in the batch are dropped, unknownkind→other,interestclamped 0–10. Persist every result toarticle_assessments (stage='triage', model, prompt_version, profile_version, score, kind, rationale=why, assessed_at). Cache (§7.3): skip articles with a reusabletriageordeepassessment (samemodelandprompt_version,assessed_atwithinassessment_reuse_days);generate --rescoreignores the cache. A failed batch leavestriageabsent for its articles. Pool cap (§10): if eligible >triage_max(800), triage the union of top0.7 × triage_maxby preliminary blend, top 100 byinterest, top 100 byknn(if active), all auto-includes, filled totriage_maxby blend; the rest getstage='eligible',excluded_reason='not_admitted'.src/curate/admit.rs(§8.1, §11): hygiene moves here (blocked,published_before= inissue_articlesfor any issue date before this run's date,recently_rejectedper item 1; auto-includes never excluded) writing the thincandidate_runsrows, and the union admission intodeep_keep(120) slots in this order, each retriever taking its top-N by its own signal among not-yet-admitted, not-excluded articles, recording every retriever that would have taken an article inadmitted_by(first = the one that admitted it):auto_include(uncapped) →triage(quota 60, floorinterest ≥ 5) →interest(20; floorsword_count ≥ semantic_min_words, notlooks_like_roundup, triageinterest ≥ 3if triaged) →knn(20, gate > 0, same floors) →exploration(5, §11.1) →blend(remaining). Inactive retrievers release their quota toblend. Not admitted →stage='triaged'(or'eligible'if never triaged) +excluded_reason='not_admitted'. Exploration (§11.1): five slots for articles ranked betweendeep_keepanddeep_keep × 2.5by the preliminary blend withword_count ≥ 300, not roundups, triageinterest ≥ 4; ordered bysha256(run_date || article_id); flaggedexploration = truethrough to the editor prompt.- Prefilter reduced (§18):
prefilter.rskeeps only hygiene helpers (is_blocked,is_auto_include,looks_like_roundup) and the text heuristic piecessignals.rsuses. Deleteprefilter::run,score_article's social/Scour/HN/multi-source terms,PrefilterContext, theprefilter_keepconfig key (replace uses withcuration.ranking.deep_keep; the config validationdeep_keep ≥ shortlist_keep ≥ target_article_countalready exists), andCurator::prefilter. A staleprefilter_keepkey in a config file must fail loudly with a message namingcuration.ranking.deep_keep(follow thebookorbit_dirprecedent inconfig.rs). - Pipeline: hygiene → embeddings → signals → preliminary blend → triage → admission → the
existing Stage A scoring (
score.rs, unchanged this step) over the admitted deep set → the editor → editorial, as today. The deep set (admitted) is what Stage A scores and what the editor sees; step 5 replaces Stage A with deep assessment and adds the shortlist. Telemetry stages now:excluded | eligible | triaged | admitted | assessed | shortlisted | selectedwith reasons per §7.4;admitted_byandexplorationwritten;signals_json.raw/norm/presentgaintriage(÷10 for norm). Stage timingstriageandadmit;StageCountsgainstriaged,admitted,admitted_by(map),exploration_admitted,exploration_selected. Log the admission line from §15.4:admission: triage 60 · interest 20 · knn 12 · exploration 5 · blend 23 · auto 0.--skip-llmskips triage; DeepSeek down or tripped ⇒ no triage, admission byinterest/knn/blend, editor still runs (§17). The editor prompt (§13 rendering) shows the triage score andflags: explorationwhere present.explainprints the triage assessment (score, kind, why) andadmitted_by. types.rs(§18): introduceAssessment { triage: Option<Triage>, deep: Option<Deep> }andTriage { interest, kind, why, model, prompt_version, assessed_at }(keepDeep/LlmScorecompatible with the existing Stage A output for now), andCandidate { article, auto_include, exploration, signals, assessment, utility: Option<f64>, cluster: Option<...>, admitted_by: Vec<String>, stage, excluded_reason }replacingScoredArticlewhere the pipeline flows through admission. Keep the change mechanical wherescore.rs/select.rsstill consume the old shape (step 5 retires them); an adapter is acceptable.runs.config_jsongainsTRIAGE_PROMPT_VERSION;config.example.tomland README updated (prefilter_keepremoved,[deepseek].triage_batch_size,--rescore).
Tests (§20 "Triage and deep parsing" triage half, "Admission", churn/cache): realistic triage
fixture parsed; malformed items do not sink a batch; unknown kind → other; every kind token in the
prompt round-trips; cached triage reused within assessment_reuse_days and ignored with --rescore;
churn rule excludes a recent low triage score and spares auto-includes; a strong-interest,
weak-heuristic, no-social article reaches the deep set; a 60-word stub with high interest similarity
is not admitted by interest/knn; quotas honoured; inactive retrievers release quota; exploration
deterministic per date and rotating across dates; auto-includes always admitted; excluded articles get
thin rows with the right reason; pool cap path yields not_admitted rows; the mocked full pipeline
still publishes with DeepSeek failing; migration 0003 drops scores.