From 5487a1801fdb4f645699247c4f9146f455cf4efa Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Sun, 1 Feb 2026 13:04:33 -0800 Subject: [PATCH] scoring starter code, and flake to stable --- flake.lock | 8 +- flake.nix | 2 +- .../20260115003047_initial_schema.sql | 4 +- koucha/src/db.rs | 1 - koucha/src/lib.rs | 1 + koucha/src/score.rs | 89 +++++++++++++++++++ 6 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 koucha/src/score.rs diff --git a/flake.lock b/flake.lock index 2972d8a..c4fa525 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1768127708, - "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", + "lastModified": 1769933782, + "narHash": "sha256-GlZemJ2dxhXMMq6TNyt588OFv4/jIt3J1QVBO9MspBE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", + "rev": "64728753f1a42c81c5688a136a6bee173665acc9", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-25.11-small", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 79e3a0f..f48d72e 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "Koucha rust flake"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small"; }; outputs = { self, nixpkgs }: diff --git a/koucha/migrations/20260115003047_initial_schema.sql b/koucha/migrations/20260115003047_initial_schema.sql index 63d3591..ae2418e 100644 --- a/koucha/migrations/20260115003047_initial_schema.sql +++ b/koucha/migrations/20260115003047_initial_schema.sql @@ -39,7 +39,9 @@ CREATE TABLE feeds ( CREATE TABLE feed_channels ( feed_id INTEGER NOT NULL, channel_id INTEGER NOT NULL, - -- Decay settings will go here + initial_score INTEGER, + gravity INTEGER, + boost INTEGER, PRIMARY KEY (feed_id, channel_id), FOREIGN KEY (feed_id) REFERENCES feeds(id), FOREIGN KEY (channel_id) REFERENCES channels(id) diff --git a/koucha/src/db.rs b/koucha/src/db.rs index 79eaf50..2456bd9 100644 --- a/koucha/src/db.rs +++ b/koucha/src/db.rs @@ -9,7 +9,6 @@ pub use channel::Channel; mod item; pub use item::Item; - macro_rules! define_id { ($name:ident) => { #[derive(PartialEq, Debug, Copy, Clone)] diff --git a/koucha/src/lib.rs b/koucha/src/lib.rs index a9de79f..a20a9dc 100644 --- a/koucha/src/lib.rs +++ b/koucha/src/lib.rs @@ -4,6 +4,7 @@ type Result = std::result::Result>; pub mod db; pub mod fetch; +pub mod score; #[cfg(test)] pub mod test_utils; diff --git a/koucha/src/score.rs b/koucha/src/score.rs new file mode 100644 index 0000000..7c54b63 --- /dev/null +++ b/koucha/src/score.rs @@ -0,0 +1,89 @@ +use chrono::{DateTime, Utc, TimeDelta}; + +mod default { + pub const INITIAL_SCORE: i64 = 70; + pub const GRAVITY: i64 = -10; + pub const BOOST: i64 = 12; +} + +macro_rules! rich_i64 { + ($name:ident, $default:expr) => { + #[derive(PartialEq, Debug, Copy, Clone)] + pub struct $name(i64); + impl From<$name> for i64 { fn from(id: $name) -> Self { id.0 } } + impl $name { + pub fn new(value: Option) -> Self { + Self(value.unwrap_or($default)) + } + } + }; +} + +rich_i64!(Gravity, default::GRAVITY); +rich_i64!(Boost, default::BOOST); +impl Boost { + fn should_apply( + old_boost_time: Option>, new_boost_time: DateTime + ) -> bool { + old_boost_time.map_or(true, |obs| { + let time_delta = obs - &new_boost_time; + time_delta.num_days() >= 1 + }) + } +} +pub struct Score { + value: i64, + last_updated: DateTime, + last_boosted: Option>, +} + +impl Score { + pub fn new() -> Self { + Self::new_at_time(Utc::now()) + } + + pub fn new_at_time(creation_time: DateTime) -> Self { + Score { + value: default::INITIAL_SCORE, + last_updated: creation_time, + last_boosted: Some(creation_time), + } + } + + pub fn update(self) -> Self { + self.update_at_time(Utc::now()) + } + + pub fn update_at_time(self, update_time: DateTime) -> Self { + let to_elapse_from = self.last_boosted.map_or(self.last_updated, |lb| { + std::cmp::max(self.last_updated, lb + TimeDelta::days(1)) + }); + let elapsed_time = update_time.signed_duration_since(to_elapse_from); + + self + } + + pub fn boost(self, boost: Boost) -> Self { + self.boost_at_time(boost, Utc::now()) + } + + // This function does not "fast-forward" the score's value to the time of + // boost, but still updates `last_updated`. Meaning that hours of gravity may + // not be applied. + // The expectation is that a boost is a user interaction, and that the user + // should be seeing a "fresh" score. We wouldn't want a boost to end up + // applying lots of gravity. But care should be taken to ensure that only + // "fresh" scores are boosted. Or that boost_time reflects the state of the + // user experience. + pub fn boost_at_time(self, boost: Boost, boost_time: DateTime) -> Self { + if Boost::should_apply(self.last_boosted, boost_time) { + Self { + value: self.value + i64::from(boost), + last_updated: boost_time, + last_boosted: Some(boost_time), + } + } else { + self + } + } +}