Credit ratings to the author, and less to aggregator feeds
A rating on an article that arrived only via an aggregator (HN, Lobsters, Reddit, Scour) used to count fully against that aggregator feed. Now the aggregator feed gets a quarter of the credit and the article's author gets the full credit, so future articles by the same author from any feed carry the history. The feed signal is the mean over rated direct feeds and the rated author. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
This commit is contained in:
@@ -497,7 +497,7 @@ Computed in `src/curate/signals.rs` after embeddings. Every signal is `Option<f6
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `interest` | z-scored standing-interest match (§9.1) | no embedding, or fewer than 30 eligible articles have embeddings (then use raw top-1 cosine and log it) |
|
| `interest` | z-scored standing-interest match (§9.1) | no embedding, or fewer than 30 eligible articles have embeddings (then use raw top-1 cosine and log it) |
|
||||||
| `knn` | signed rated-neighbour preference (§9.2) | no embedding, or gate closed |
|
| `knn` | signed rated-neighbour preference (§9.2) | no embedding, or gate closed |
|
||||||
| `feed` | mean Beta-smoothed rating rate over the article's distinct direct feeds (§9.3) | no direct feed with any rating, or gate closed |
|
| `feed` | mean Beta-smoothed rating rate over the article's distinct direct feeds and author (§9.3) | no direct feed or author with any rating, or gate closed |
|
||||||
| `social` | existing `composite_social_score` | no `social` rows |
|
| `social` | existing `composite_social_score` | no `social` rows |
|
||||||
| `heuristic` | `longform_points(word_count)` − excerpt-only penalty − roundup penalty, from `prefilter.rs` with the social, Scour/HN, multi-source, and feed-prior terms **removed** | never |
|
| `heuristic` | `longform_points(word_count)` − excerpt-only penalty − roundup penalty, from `prefilter.rs` with the social, Scour/HN, multi-source, and feed-prior terms **removed** | never |
|
||||||
|
|
||||||
@@ -531,7 +531,9 @@ preference: 14 rated articles with embeddings → knn gate 0.35; feed gate 0.0 (
|
|||||||
|
|
||||||
### 9.3 Feed affinity
|
### 9.3 Feed affinity
|
||||||
|
|
||||||
From the same rating set. Credit each rating's `value` to the article's distinct direct feeds (`SourceKind::Feed`), split evenly; if there are none, to `best_entry_id`'s feed. Per feed: `rate = (up + 1) / (up + down + 2)` where `up = Σ max(value, 0)` and `down = Σ max(−value, 0)`, decayed as in §9.2. A candidate's `feed` signal is the **mean** over its distinct direct feeds that have any rating (never the max). Gate: `feed_floor = 15`, `feed_full = 40` attributable ratings.
|
From the same rating set. Credit each rating's decayed `value` to the article's distinct direct feeds (`SourceKind::Feed`), split evenly; if there are none, use `best_entry_id`'s feed. For an aggregator-only article, that fallback aggregator feed receives only `0.25 ×` the feed credit. When an article has an author, its normalized author key (trimmed, internal whitespace collapsed, lowercased) separately receives the full credit, so future articles by that author carry the history across any feed.
|
||||||
|
|
||||||
|
Per feed and author: `rate = (up + 1) / (up + down + 2)` where `up = Σ max(value × decay, 0)` and `down = Σ max(−value × decay, 0)`. A candidate's `feed` signal is the **mean** over its distinct direct feeds and author that have any rating (never the max). A rating is attributable if it credits at least one feed or author. Gate: `feed_floor = 15`, `feed_full = 40` attributable ratings.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -987,7 +989,7 @@ No test touches the network. Mock backends for all three providers, following th
|
|||||||
- **Rating events**: latest explicit event wins; `cleared` removes an article from the learned set; migration copies old rows with the right labels and values; the CLI `set`/`clear` append rows with `source = 'cli'`.
|
- **Rating events**: latest explicit event wins; `cleared` removes an article from the learned set; migration copies old rows with the right labels and values; the CLI `set`/`clear` append rows with `source = 'cli'`.
|
||||||
- **Embeddings**: BLOB round trip; wrong length and non-finite rejected; cache hit on same hash, miss on changed text/model/dimension; response mapped by index and length-checked; a failed batch does not abort the others; embedded text contains no feed name or author.
|
- **Embeddings**: BLOB round trip; wrong length and non-finite rejected; cache hit on same hash, miss on changed text/model/dimension; response mapped by index and length-checked; a failed batch does not abort the others; embedded text contains no feed name or author.
|
||||||
- **Interest z-scores**: a broad interest with uniformly high cosine does not dominate; a specific interest with one strong match does; raw fallback under 30 articles.
|
- **Interest z-scores**: a broad interest with uniformly high cosine does not dominate; a specific interest with one strong match does; raw fallback under 30 articles.
|
||||||
- **Preference**: one loved article gives a positive `knn` to a near neighbour; two unrelated loved clusters both score high (the anti-centroid test); `good` moves the signal 0.35× as much as `loved`; decay halves at the half-life; gate is 0 below `knn_floor`, 1 at `knn_full`, linear between; feed credit sums to 1 across direct feeds; feed affinity uses the mean.
|
- **Preference**: one loved article gives a positive `knn` to a near neighbour; two unrelated loved clusters both score high (the anti-centroid test); `good` moves the signal 0.35× as much as `loved`; decay halves at the half-life; gate is 0 below `knn_floor`, 1 at `knn_full`, linear between; ordinary feed credit sums to 1 across direct feeds; aggregator-only credit is 0.25× to its feed and 1× to its author; feed affinity uses the mean of rated feeds and author.
|
||||||
- **Normalization**: a constant signal normalizes to 0.5 for everyone; ties get equal percentiles (400 identical zeros → all 0.5, no id ramp); absent values do not shift others; effective weights sum to 1; a candidate missing a signal is scored on the rest.
|
- **Normalization**: a constant signal normalizes to 0.5 for everyone; ties get equal percentiles (400 identical zeros → all 0.5, no id ramp); absent values do not shift others; effective weights sum to 1; a candidate missing a signal is scored on the rest.
|
||||||
- **Triage and deep parsing**: realistic fixtures; malformed items do not sink a batch; unknown facet tokens degrade to `None`; every enum token in both prompts round-trips; cached assessments are reused within `assessment_reuse_days` and ignored with `--rescore`.
|
- **Triage and deep parsing**: realistic fixtures; malformed items do not sink a batch; unknown facet tokens degrade to `None`; every enum token in both prompts round-trips; cached assessments are reused within `assessment_reuse_days` and ignored with `--rescore`.
|
||||||
- **Admission**: 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; auto-includes always admitted; excluded articles get thin rows with the right reason.
|
- **Admission**: 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; auto-includes always admitted; excluded articles get thin rows with the right reason.
|
||||||
|
|||||||
+137
-18
@@ -23,6 +23,9 @@ pub const INTEREST_ZSCORE_MIN_ARTICLES: usize = 30;
|
|||||||
const ZSCORE_STD_FLOOR: f64 = 1e-3;
|
const ZSCORE_STD_FLOOR: f64 = 1e-3;
|
||||||
/// How many interests and rated neighbours `signals_json` records (§7.5).
|
/// How many interests and rated neighbours `signals_json` records (§7.5).
|
||||||
const RECORDED_TOP: usize = 3;
|
const RECORDED_TOP: usize = 3;
|
||||||
|
/// Aggregators carried the link rather than authored the article, so they get
|
||||||
|
/// only a small share of an aggregator-only article's feed-affinity credit.
|
||||||
|
pub const AGGREGATOR_FEED_SHARE: f64 = 0.25;
|
||||||
|
|
||||||
/// The signal names that go through the percentile normalizer, in the order
|
/// The signal names that go through the percentile normalizer, in the order
|
||||||
/// they are rendered (§12.2). LLM scores (`triage`, `quality`, `fit`) are
|
/// they are rendered (§12.2). LLM scores (`triage`, `quality`, `fit`) are
|
||||||
@@ -126,6 +129,10 @@ pub struct RatedExample {
|
|||||||
pub embedding: Vec<f32>,
|
pub embedding: Vec<f32>,
|
||||||
/// Distinct direct feeds that carried the rated article (§9.3).
|
/// Distinct direct feeds that carried the rated article (§9.3).
|
||||||
pub feeds: Vec<FeedId>,
|
pub feeds: Vec<FeedId>,
|
||||||
|
/// Whitespace-normalized, lowercase author key (§9.3).
|
||||||
|
pub author: Option<String>,
|
||||||
|
/// Whether the article arrived only through link aggregators (§9.3).
|
||||||
|
pub aggregator_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RatedExample {
|
impl RatedExample {
|
||||||
@@ -153,6 +160,7 @@ impl FeedRate {
|
|||||||
pub struct PreferenceState {
|
pub struct PreferenceState {
|
||||||
pub examples: Vec<RatedExample>,
|
pub examples: Vec<RatedExample>,
|
||||||
feed_rates: HashMap<FeedId, FeedRate>,
|
feed_rates: HashMap<FeedId, FeedRate>,
|
||||||
|
author_rates: HashMap<String, FeedRate>,
|
||||||
pub attributable_feed_ratings: usize,
|
pub attributable_feed_ratings: usize,
|
||||||
pub knn_gate: f64,
|
pub knn_gate: f64,
|
||||||
pub feed_gate: f64,
|
pub feed_gate: f64,
|
||||||
@@ -160,8 +168,11 @@ pub struct PreferenceState {
|
|||||||
|
|
||||||
impl PreferenceState {
|
impl PreferenceState {
|
||||||
/// Build the state from already-loaded examples (pure; tests use this).
|
/// Build the state from already-loaded examples (pure; tests use this).
|
||||||
pub fn build(examples: Vec<RatedExample>, ranking: &RankingConfig) -> Self {
|
pub fn build(mut examples: Vec<RatedExample>, ranking: &RankingConfig) -> Self {
|
||||||
let (feed_rates, attributable_feed_ratings) = feed_rates(&examples);
|
for example in &mut examples {
|
||||||
|
example.author = normalize_author(example.author.as_deref());
|
||||||
|
}
|
||||||
|
let (feed_rates, author_rates, attributable_feed_ratings) = feed_rates(&examples);
|
||||||
Self {
|
Self {
|
||||||
knn_gate: gate(examples.len(), ranking.knn_floor, ranking.knn_full),
|
knn_gate: gate(examples.len(), ranking.knn_floor, ranking.knn_full),
|
||||||
feed_gate: gate(
|
feed_gate: gate(
|
||||||
@@ -171,6 +182,7 @@ impl PreferenceState {
|
|||||||
),
|
),
|
||||||
examples,
|
examples,
|
||||||
feed_rates,
|
feed_rates,
|
||||||
|
author_rates,
|
||||||
attributable_feed_ratings,
|
attributable_feed_ratings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,12 +206,14 @@ impl PreferenceState {
|
|||||||
let Some(embedding) = embeddings.get(&rating.article_id).cloned() else {
|
let Some(embedding) = embeddings.get(&rating.article_id).cloned() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let feeds = db
|
let article = db.get_article(rating.article_id).await?;
|
||||||
.get_article(rating.article_id)
|
let feeds = article.as_ref().map(direct_feeds).unwrap_or_default();
|
||||||
.await?
|
let author = article
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(direct_feeds)
|
.and_then(|article| normalize_author(article.author.as_deref()));
|
||||||
.unwrap_or_default();
|
let aggregator_only = article
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(crate::discovery::aggregator_only);
|
||||||
let age_days = (now.as_second() - rating.event_at.as_second()).max(0) as f64 / 86_400.0;
|
let age_days = (now.as_second() - rating.event_at.as_second()).max(0) as f64 / 86_400.0;
|
||||||
examples.push(RatedExample {
|
examples.push(RatedExample {
|
||||||
article_id: rating.article_id,
|
article_id: rating.article_id,
|
||||||
@@ -209,6 +223,8 @@ impl PreferenceState {
|
|||||||
decay: decay(age_days, ranking.rating_half_life_days),
|
decay: decay(age_days, ranking.rating_half_life_days),
|
||||||
embedding,
|
embedding,
|
||||||
feeds,
|
feeds,
|
||||||
|
author,
|
||||||
|
aggregator_only,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(Self::build(examples, ranking))
|
Ok(Self::build(examples, ranking))
|
||||||
@@ -298,16 +314,23 @@ impl PreferenceState {
|
|||||||
(knn, neighbours)
|
(knn, neighbours)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mean Beta-smoothed rate over the article's rated direct feeds (§9.3).
|
/// Mean Beta-smoothed rate over the article's rated direct feeds and author
|
||||||
|
/// (§9.3).
|
||||||
pub fn feed(&self, article: &Article) -> Option<f64> {
|
pub fn feed(&self, article: &Article) -> Option<f64> {
|
||||||
if self.feed_gate <= 0.0 {
|
if self.feed_gate <= 0.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let rates = direct_feeds(article)
|
let mut rates = direct_feeds(article)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|feed| self.feed_rates.get(&feed))
|
.filter_map(|feed| self.feed_rates.get(&feed))
|
||||||
.map(|rate| rate.rate())
|
.map(|rate| rate.rate())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
if let Some(rate) = normalize_author(article.author.as_deref())
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|author| self.author_rates.get(author))
|
||||||
|
{
|
||||||
|
rates.push(rate.rate());
|
||||||
|
}
|
||||||
(!rates.is_empty()).then(|| rates.iter().sum::<f64>() / rates.len() as f64)
|
(!rates.is_empty()).then(|| rates.iter().sum::<f64>() / rates.len() as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +338,13 @@ impl PreferenceState {
|
|||||||
pub fn feed_credit(&self, feed: FeedId) -> Option<(f64, f64)> {
|
pub fn feed_credit(&self, feed: FeedId) -> Option<(f64, f64)> {
|
||||||
self.feed_rates.get(&feed).map(|rate| (rate.up, rate.down))
|
self.feed_rates.get(&feed).map(|rate| (rate.up, rate.down))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-author `(up, down)` credit, exposed for tests of §9.3.
|
||||||
|
pub fn author_credit(&self, author: &str) -> Option<(f64, f64)> {
|
||||||
|
normalize_author(Some(author))
|
||||||
|
.and_then(|author| self.author_rates.get(&author))
|
||||||
|
.map(|rate| (rate.up, rate.down))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `0.5 ^ (age_days / half_life_days)` (§9.2).
|
/// `0.5 ^ (age_days / half_life_days)` (§9.2).
|
||||||
@@ -354,22 +384,46 @@ pub fn direct_feeds(article: &Article) -> Vec<FeedId> {
|
|||||||
feeds
|
feeds
|
||||||
}
|
}
|
||||||
|
|
||||||
fn feed_rates(examples: &[RatedExample]) -> (HashMap<FeedId, FeedRate>, usize) {
|
fn normalize_author(author: Option<&str>) -> Option<String> {
|
||||||
let mut rates: HashMap<FeedId, FeedRate> = HashMap::new();
|
let normalized = author?
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
.to_lowercase();
|
||||||
|
(!normalized.is_empty()).then_some(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn feed_rates(
|
||||||
|
examples: &[RatedExample],
|
||||||
|
) -> (HashMap<FeedId, FeedRate>, HashMap<String, FeedRate>, usize) {
|
||||||
|
let mut feed_rates: HashMap<FeedId, FeedRate> = HashMap::new();
|
||||||
|
let mut author_rates: HashMap<String, FeedRate> = HashMap::new();
|
||||||
let mut attributable = 0;
|
let mut attributable = 0;
|
||||||
for example in examples {
|
for example in examples {
|
||||||
if example.feeds.is_empty() {
|
let weight = example.weight();
|
||||||
continue;
|
if !example.feeds.is_empty() {
|
||||||
}
|
let feed_weight = if example.aggregator_only {
|
||||||
attributable += 1;
|
weight * AGGREGATOR_FEED_SHARE
|
||||||
let credit = example.weight() / example.feeds.len() as f64;
|
} else {
|
||||||
|
weight
|
||||||
|
};
|
||||||
|
let credit = feed_weight / example.feeds.len() as f64;
|
||||||
for feed in &example.feeds {
|
for feed in &example.feeds {
|
||||||
let rate = rates.entry(*feed).or_default();
|
let rate = feed_rates.entry(*feed).or_default();
|
||||||
rate.up += credit.max(0.0);
|
rate.up += credit.max(0.0);
|
||||||
rate.down += (-credit).max(0.0);
|
rate.down += (-credit).max(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(rates, attributable)
|
if let Some(author) = &example.author {
|
||||||
|
let rate = author_rates.entry(author.clone()).or_default();
|
||||||
|
rate.up += weight.max(0.0);
|
||||||
|
rate.down += (-weight).max(0.0);
|
||||||
|
}
|
||||||
|
if !example.feeds.is_empty() || example.author.is_some() {
|
||||||
|
attributable += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(feed_rates, author_rates, attributable)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The interest match of §9.1 for one article.
|
/// The interest match of §9.1 for one article.
|
||||||
@@ -620,6 +674,8 @@ mod tests {
|
|||||||
decay: 1.0,
|
decay: 1.0,
|
||||||
embedding: unit(embedding),
|
embedding: unit(embedding),
|
||||||
feeds: vec![id],
|
feeds: vec![id],
|
||||||
|
author: None,
|
||||||
|
aggregator_only: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,6 +901,69 @@ mod tests {
|
|||||||
assert_eq!(state.feed(&article(9, &[99])), None);
|
assert_eq!(state.feed(&article(9, &[99])), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregator_only_rating_splits_credit_between_feed_and_author() {
|
||||||
|
let mut rated = example(1, "loved", 1.0, &[1.0, 0.0]);
|
||||||
|
rated.decay = 0.4;
|
||||||
|
rated.feeds = vec![10];
|
||||||
|
rated.author = Some("example author".into());
|
||||||
|
rated.aggregator_only = true;
|
||||||
|
let state = PreferenceState::build(vec![rated], &ranking());
|
||||||
|
|
||||||
|
assert!((state.feed_credit(10).unwrap().0 - 0.25 * 0.4).abs() < 1e-9);
|
||||||
|
assert!((state.author_credit("example author").unwrap().0 - 0.4).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn author_affinity_applies_across_feeds_with_normalized_keys() {
|
||||||
|
let mut ranking = ranking();
|
||||||
|
ranking.feed_floor = 0;
|
||||||
|
ranking.feed_full = 1;
|
||||||
|
let mut rated = example(1, "loved", 1.0, &[1.0, 0.0]);
|
||||||
|
rated.feeds = vec![10];
|
||||||
|
rated.author = Some("Ada Lovelace".into());
|
||||||
|
let state = PreferenceState::build(vec![rated], &ranking);
|
||||||
|
let mut candidate = article(2, &[99]);
|
||||||
|
candidate.author = Some(" ADA lovelace ".into());
|
||||||
|
|
||||||
|
assert_eq!(state.examples[0].author.as_deref(), Some("ada lovelace"));
|
||||||
|
assert_eq!(state.feed_credit(99), None);
|
||||||
|
assert!((state.feed(&candidate).unwrap() - 2.0 / 3.0).abs() < 1e-9);
|
||||||
|
assert_eq!(state.author_credit(" ADA Lovelace "), Some((1.0, 0.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn feed_affinity_means_rated_feed_and_rated_author() {
|
||||||
|
let mut ranking = ranking();
|
||||||
|
ranking.feed_floor = 0;
|
||||||
|
ranking.feed_full = 1;
|
||||||
|
let mut feed_loved = example(1, "loved", 1.0, &[1.0, 0.0]);
|
||||||
|
feed_loved.feeds = vec![10];
|
||||||
|
let mut author_down = example(2, "not_for_me", -1.0, &[1.0, 0.0]);
|
||||||
|
author_down.feeds.clear();
|
||||||
|
author_down.author = Some("writer".into());
|
||||||
|
let state = PreferenceState::build(vec![feed_loved, author_down], &ranking);
|
||||||
|
let mut candidate = article(3, &[10]);
|
||||||
|
candidate.author = Some("Writer".into());
|
||||||
|
|
||||||
|
assert!((state.feed(&candidate).unwrap() - 0.5).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aggregator_only_without_author_keeps_feed_behavior_at_reduced_credit() {
|
||||||
|
let mut ranking = ranking();
|
||||||
|
ranking.feed_floor = 0;
|
||||||
|
ranking.feed_full = 1;
|
||||||
|
let mut rated = example(1, "not_for_me", -1.0, &[1.0, 0.0]);
|
||||||
|
rated.feeds = vec![10];
|
||||||
|
rated.aggregator_only = true;
|
||||||
|
let state = PreferenceState::build(vec![rated], &ranking);
|
||||||
|
|
||||||
|
assert_eq!(state.feed_credit(10), Some((0.0, 0.25)));
|
||||||
|
assert_eq!(state.author_credit(""), None);
|
||||||
|
assert!((state.feed(&article(2, &[10])).unwrap() - 1.0 / 2.25).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn feed_is_absent_when_the_gate_is_closed() {
|
fn feed_is_absent_when_the_gate_is_closed() {
|
||||||
let ranking = ranking(); // feed_floor 15
|
let ranking = ranking(); // feed_floor 15
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ pub fn routes() -> Router<AppState> {
|
|||||||
pub struct FeedCredit {
|
pub struct FeedCredit {
|
||||||
pub feed_id: FeedId,
|
pub feed_id: FeedId,
|
||||||
pub feed_title: String,
|
pub feed_title: String,
|
||||||
/// `value × decay / n` over the article's `n` direct feeds.
|
/// `value × decay / n` over the article's `n` direct feeds, reduced for an
|
||||||
|
/// aggregator-only article.
|
||||||
pub credit: f64,
|
pub credit: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +98,12 @@ pub fn contribution(
|
|||||||
let feed_credits = if rating.label == "cleared" || feeds.is_empty() {
|
let feed_credits = if rating.label == "cleared" || feeds.is_empty() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
let credit = weight / feeds.len() as f64;
|
let feed_weight = if article.is_some_and(crate::discovery::aggregator_only) {
|
||||||
|
weight * signals::AGGREGATOR_FEED_SHARE
|
||||||
|
} else {
|
||||||
|
weight
|
||||||
|
};
|
||||||
|
let credit = feed_weight / feeds.len() as f64;
|
||||||
feeds
|
feeds
|
||||||
.iter()
|
.iter()
|
||||||
.map(|feed_id| FeedCredit {
|
.map(|feed_id| FeedCredit {
|
||||||
|
|||||||
Reference in New Issue
Block a user