Signals judge one subject, with a sentence of your own, not any one word (#259)
Jev was asked "Is this item about any of these: ai, development, programming?", so any one broad word was enough and the Signal's name, which says what is meant, was never sent. AI Development held 54 items: Sega pulling a game remaster (0.90), a Surface Laptop review (0.95), a C++ tutorial (0.99). It is now asked about one subject, called by the Signal's name, described by its words and, when those are not enough, its owner's sentence. Scored against TypeSafe over those 54 with "AI tools for writing software: coding agents, AI dev tools, vibe coding, AI in code review.": 5 stay, Linux kernel AI code review highest at 0.94; the remaster 0.06, the C++ tutorial 0.02. signals.description is new, and its first start deletes every verdict, which answered the old question, so everything in the window is asked again. Any change to a Signal's name, words or sentence drops its verdicts, since each is part of the question. The queue query names the Signal's sentence apart from the item's description, which share a row; the test caught the item's winning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,13 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
||||
let sql = "ALTER TABLE users ADD COLUMN typesafe_key TEXT";
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
}
|
||||
if orm.execute_unprepared("SELECT description FROM signals LIMIT 0").await.is_err() {
|
||||
// Every verdict so far answered the old question, any one keyword on its own, which put
|
||||
// a game's remaster in AI Development (#259): all of them are asked again.
|
||||
for sql in ["ALTER TABLE signals ADD COLUMN description TEXT", "DELETE FROM signal_verdicts"] {
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
}
|
||||
}
|
||||
if orm.execute_unprepared("SELECT pause_ms FROM deadair_prefs LIMIT 0").await.is_err() {
|
||||
let sql = "ALTER TABLE deadair_prefs ADD COLUMN pause_ms BIGINT";
|
||||
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
|
||||
@@ -2539,9 +2546,19 @@ pub struct SignalRow {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub keywords: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub unread: i64,
|
||||
}
|
||||
|
||||
/// One Signal's question about an item: what it is called, its words, and its owner's sentence.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ask {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub keywords: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// An item still to be put to Jev for someone, with each of their Signals that has not been
|
||||
/// asked about it.
|
||||
#[derive(Debug)]
|
||||
@@ -2553,7 +2570,7 @@ pub struct Unjudged {
|
||||
pub feed_title: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub signals: Vec<(i64, Vec<String>)>,
|
||||
pub signals: Vec<Ask>,
|
||||
}
|
||||
|
||||
/// How far back a Signal looks when it is made: a month of items, not every back catalogue.
|
||||
@@ -2574,7 +2591,7 @@ impl Db {
|
||||
let user = a.p(user_id);
|
||||
let one = id.map(|i| format!("AND g.id = {}", a.p(i))).unwrap_or_default();
|
||||
let sql = format!(
|
||||
"SELECT g.id, g.name, g.keywords,
|
||||
"SELECT g.id, g.name, g.keywords, g.description,
|
||||
(SELECT count(*) FROM signal_verdicts v
|
||||
JOIN entries e ON e.feed_id = v.feed_id AND e.guid = v.guid
|
||||
JOIN subscriptions s ON s.user_id = g.user_id AND s.feed_id = v.feed_id
|
||||
@@ -2591,17 +2608,19 @@ impl Db {
|
||||
id: r.try_get("", "id")?,
|
||||
name: r.try_get("", "name")?,
|
||||
keywords: keywords(r.try_get("", "keywords")?).unwrap_or_default(),
|
||||
description: r.try_get("", "description")?,
|
||||
unread: r.try_get("", "unread")?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn add_signal(&self, user_id: i64, name: &str, words: &[String]) -> Result<i64> {
|
||||
pub async fn add_signal(&self, user_id: i64, name: &str, words: &[String], description: Option<&str>) -> Result<i64> {
|
||||
let m = signals::ActiveModel {
|
||||
user_id: Set(user_id),
|
||||
name: Set(name.to_owned()),
|
||||
keywords: Set(serde_json::to_string(words)?),
|
||||
description: Set(description.map(str::to_owned)),
|
||||
created: Set(now()),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -2610,9 +2629,17 @@ impl Db {
|
||||
Ok(m.id)
|
||||
}
|
||||
|
||||
/// Renames a Signal or gives it new keywords. New keywords put every item to Jev again, since
|
||||
/// what it said was about the old ones. False for a Signal that is not this person's.
|
||||
pub async fn set_signal(&self, user_id: i64, id: i64, name: Option<&str>, words: Option<&[String]>) -> Result<bool> {
|
||||
/// Renames a Signal, or gives it new words or a new sentence. Either of those puts every item
|
||||
/// to Jev again, since what it said was about the old ones; a new name does too, since the
|
||||
/// question is about the name (#259). False for a Signal that is not this person's.
|
||||
pub async fn set_signal(
|
||||
&self,
|
||||
user_id: i64,
|
||||
id: i64,
|
||||
name: Option<&str>,
|
||||
words: Option<&[String]>,
|
||||
description: Option<Option<&str>>,
|
||||
) -> Result<bool> {
|
||||
if self.signal(user_id, id).await?.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -2622,9 +2649,12 @@ impl Db {
|
||||
}
|
||||
if let Some(w) = words {
|
||||
m.keywords = Set(serde_json::to_string(w)?);
|
||||
signal_verdicts::Entity::delete_many().filter(signal_verdicts::Column::SignalId.eq(id)).exec(&self.orm).await?;
|
||||
}
|
||||
if name.is_some() || words.is_some() {
|
||||
if let Some(d) = description {
|
||||
m.description = Set(d.map(str::to_owned));
|
||||
}
|
||||
if name.is_some() || words.is_some() || description.is_some() {
|
||||
signal_verdicts::Entity::delete_many().filter(signal_verdicts::Column::SignalId.eq(id)).exec(&self.orm).await?;
|
||||
m.update(&self.orm).await?;
|
||||
}
|
||||
Ok(true)
|
||||
@@ -2662,7 +2692,7 @@ impl Db {
|
||||
};
|
||||
let limit = a.p(limit);
|
||||
let sql = format!(
|
||||
"SELECT g.id AS signal_id, g.keywords, g.user_id, u.typesafe_key, e.feed_id, e.guid, e.title,
|
||||
"SELECT g.id AS signal_id, g.name, g.keywords, g.description AS signal_description, g.user_id, u.typesafe_key, e.feed_id, e.guid, e.title,
|
||||
e.description, f.title AS feed_title
|
||||
FROM signals g
|
||||
JOIN users u ON u.id = g.user_id
|
||||
@@ -2682,7 +2712,13 @@ impl Db {
|
||||
for r in self.rows(&sql, a.0).await? {
|
||||
let (user_id, feed_id, guid): (i64, String, String) =
|
||||
(r.try_get("", "user_id")?, r.try_get("", "feed_id")?, r.try_get("", "guid")?);
|
||||
let signal = (r.try_get("", "signal_id")?, keywords(r.try_get("", "keywords")?).unwrap_or_default());
|
||||
let signal = Ask {
|
||||
id: r.try_get("", "signal_id")?,
|
||||
name: r.try_get("", "name")?,
|
||||
keywords: keywords(r.try_get("", "keywords")?).unwrap_or_default(),
|
||||
// Named apart from the item's own description, which is in the same row.
|
||||
description: r.try_get("", "signal_description")?,
|
||||
};
|
||||
// Rows of one item for one person come together, by the ORDER BY.
|
||||
match out.last_mut() {
|
||||
Some(u) if (u.user_id, &u.feed_id, &u.guid) == (user_id, &feed_id, &guid) => u.signals.push(signal),
|
||||
@@ -3331,9 +3367,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
let words = |w: &[&str]| w.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let f1 = db.add_signal(1, "F1", &words(&["formula 1"])).await.unwrap();
|
||||
let food = db.add_signal(1, "Food", &words(&["baking"])).await.unwrap();
|
||||
db.add_signal(2, "Theirs", &words(&["x"])).await.unwrap();
|
||||
let f1 = db.add_signal(1, "F1", &words(&["formula 1"]), None).await.unwrap();
|
||||
let food = db.add_signal(1, "Food", &words(&["baking"]), Some("Bread and cakes")).await.unwrap();
|
||||
db.add_signal(2, "Theirs", &words(&["x"]), None).await.unwrap();
|
||||
|
||||
assert!(db.unjudged(&[], 50).await.unwrap().is_empty(), "nobody has a key yet");
|
||||
db.set_typesafe_key(1, Some("k")).await.unwrap();
|
||||
@@ -3341,6 +3377,8 @@ mod tests {
|
||||
let mut seen: Vec<_> = queue.iter().map(|u| (u.guid.as_str(), u.signals.len())).collect();
|
||||
seen.sort();
|
||||
assert_eq!(seen, [("x", 2), ("y", 2)], "each recent subscribed item once, with both Signals; old and unsubscribed left out");
|
||||
let ask = queue[0].signals.iter().find(|a| a.id == food).unwrap();
|
||||
assert_eq!((ask.name.as_str(), ask.description.as_deref()), ("Food", Some("Bread and cakes")), "Jev is asked about the name and the sentence");
|
||||
assert!(db.unjudged(&[1], 50).await.unwrap().is_empty(), "a refused key is passed over");
|
||||
|
||||
db.record_verdicts("f", "x", &[(f1, true), (food, false)]).await.unwrap();
|
||||
@@ -3358,11 +3396,15 @@ mod tests {
|
||||
assert_eq!(db.signal(1, f1).await.unwrap().unwrap().unread, 0, "read in its feed is read in the Signal");
|
||||
|
||||
assert!(db.signal(2, f1).await.unwrap().is_none(), "someone else's Signal is not found");
|
||||
assert!(!db.set_signal(2, f1, Some("mine"), None).await.unwrap());
|
||||
assert!(!db.set_signal(2, f1, Some("mine"), None, None).await.unwrap());
|
||||
assert!(!db.remove_signal(2, f1).await.unwrap());
|
||||
|
||||
db.set_signal(1, f1, None, Some(&words(&["f1"]))).await.unwrap();
|
||||
db.set_signal(1, f1, None, Some(&words(&["f1"])), None).await.unwrap();
|
||||
assert_eq!(db.signal_count(1, f1, Filter::All, None).await.unwrap(), 0, "new keywords are judged afresh");
|
||||
db.record_verdicts("f", "x", &[(f1, true)]).await.unwrap();
|
||||
db.set_signal(1, f1, None, None, Some(Some("Grand prix racing"))).await.unwrap();
|
||||
assert_eq!(db.signal_count(1, f1, Filter::All, None).await.unwrap(), 0, "so is a new sentence");
|
||||
assert_eq!(db.signal(1, f1).await.unwrap().unwrap().description.as_deref(), Some("Grand prix racing"));
|
||||
assert!(db.remove_signal(1, food).await.unwrap());
|
||||
db.record_verdicts("f", "x", &[(food, true)]).await.unwrap();
|
||||
assert_eq!(db.signals(1).await.unwrap().len(), 1, "a verdict for a removed Signal is not kept");
|
||||
|
||||
Reference in New Issue
Block a user