~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The feed leaves out what a repository's bots say and push

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/feed-without-bots
.changeset/feed-without-bots.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: patch
---
# The feed leaves out bots
The front page's feed no longer shows what a repository's runners and triagers say or push on it, which repeated what people did and crowded them out. A runner that is the repository's owner or a collaborator still shows. A pull the merge queue landed shows as merged by whoever queued it. A bot's own profile still shows everything it did.
crates/knotless-serve/src/web/feed.rs+129−35
@@ -3,7 +3,7 @@
//! the viewer follows did.
use super::*;
use crate::index::{Among, Index, Push, cursor_at};
use crate::index::{Among, Assembled, Index, Push, cursor_at};
use crate::viewer::Viewer;
use axum::Extension;
use knotless::records::nsid;
@@ -157,7 +157,10 @@ fn owner_and_name(uri: &str) -> Option<(String, String)> {
/// and those it follows', with anything on its repositories and those it
/// starred, if named; else everyone's but their stars, which anyone can
/// write as many of as they like. Of every kind, or only `kind`: a kind
/// this server does not know is none.
/// this server does not know is none. Unless `by` is named, of each
/// repository's bots, the runners and triagers it names that are not
/// people who may push, only the pulls they merged, as whoever queued
/// each: what they say and push restates what people did.
#[derive(Clone, Copy)]
struct Whose<'a> {
by: Option<&'a str>,
@@ -191,6 +194,8 @@ async fn events(
kind,
} = whose;
let wants = |k: &str| kind.is_none_or(|kind| kind == k);
// One account's feed is all it did; the rest leave out bots.
let quiet = by.is_none();
// Pulls and issues are their records and changes of their state.
let only = match kind {
Some("pulls") => Some("pull"),
@@ -212,13 +217,13 @@ async fn events(
}
if wants("pushes") {
pushes = index
.pushes(limit, cursor, by, following)
.pushes(limit, cursor, by, following, quiet)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
}
if kind.is_none() || only.is_some() {
said = index
.recent(limit, cursor, by, following, only)
.recent(limit, cursor, by, following, only, quiet)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
}
@@ -263,6 +268,49 @@ async fn events(
stars.retain(|s| held(&s.created_at, &s.uri));
}
// Pulls and issues, as their repositories' owners let them stand, by
// title, and the repositories they are on.
let mut titles: HashMap<String, String> = HashMap::new();
let on: HashSet<(&str, &str)> = said
.iter()
.map(|(_, did, key)| (did.as_str(), key.as_str()))
.collect();
let mut on_repos: HashMap<(&str, &str), (String, RepoRecord, Arc<Assembled>)> = HashMap::new();
for (did, key) in on {
let name = index
.name(did, key)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?
.unwrap_or_default();
let record = index
.repo_of(did, key)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
let record: RepoRecord = record
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_default();
let made = index
.assembled(did, key, &record)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
for e in &made.entries {
let collection = forge::parse_uri(&e.uri).unwrap_or_default().1;
if matches!(collection, nsid::PULL | nsid::ISSUE) {
let title = e.value["title"].as_str().unwrap_or_default().to_string();
titles.insert(e.uri.clone(), title);
}
}
on_repos.insert((did, key), (name, record, made));
}
// Who each is shown as said or done by.
let authors: Vec<&str> = said
.iter()
.map(|(e, did, key)| {
let (_, record, made) = &on_repos[&(did.as_str(), key.as_str())];
shown_by(e, did, record, made)
})
.collect();
// Each account named, resolved together.
let parents: Vec<(String, String)> = repos
.iter()
@@ -272,10 +320,8 @@ async fn events(
let mut dids: HashSet<&str> = parents.iter().map(|(did, _)| did.as_str()).collect();
dids.extend(repos.iter().map(|r| r.did.as_str()));
dids.extend(pushes.iter().flat_map(|p| [p.did.as_str(), p.by.as_str()]));
dids.extend(
said.iter()
.flat_map(|(e, did, _)| [e.author.as_str(), did.as_str()]),
);
dids.extend(said.iter().map(|(_, did, _)| did.as_str()));
dids.extend(authors.iter().copied());
dids.extend(
stars
.iter()
@@ -425,34 +471,11 @@ async fn events(
events.push((last.uri.clone(), event));
}
// Pulls and issues, as their repositories' owners let them stand, by
// title.
let mut titles: HashMap<String, String> = HashMap::new();
let on: HashSet<(&str, &str)> = said
let named: HashMap<(&str, &str), RepoView> = on_repos
.iter()
.map(|(_, did, key)| (did.as_str(), key.as_str()))
.map(|(&(did, key), (name, ..))| ((did, key), repo(did, name)))
.collect();
let mut named = HashMap::new();
for (did, key) in on {
let name = index
.name(did, key)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?
.unwrap_or_default();
let entries = index
.forge(did, key)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
for e in entries {
let collection = forge::parse_uri(&e.uri).unwrap_or_default().1;
if matches!(collection, nsid::PULL | nsid::ISSUE) {
let title = e.value["title"].as_str().unwrap_or_default().to_string();
titles.insert(e.uri, title);
}
}
named.insert((did, key), repo(did, &name));
}
for (e, did, key) in &said {
for ((e, did, key), author) in said.iter().zip(&authors) {
// A pull or an issue is its own subject. One the repository's owner
// keeps out is not shown, nor what is said about it.
let subject = if titles.contains_key(&e.uri) {
@@ -480,7 +503,7 @@ async fn events(
};
let event = Event {
created_at: text("createdAt"),
actor: actor(&e.author),
actor: actor(author),
repo: named[&(did.as_str(), key.as_str())].clone(),
action,
};
@@ -501,6 +524,28 @@ async fn events(
Ok((events.into_iter().map(|(_, e)| e).collect(), next))
}
/// Who `e`, on `owner`'s repository of `record`, is shown as written by:
/// its author, or, for a pull a runner the record names marked merged
/// landing the merge queue, whoever queued it, as the pull's standing in
/// the queue says. A runner that is the owner or a collaborator is a
/// person, and keeps it.
fn shown_by<'a>(
e: &'a forge::Entry,
owner: &str,
record: &RepoRecord,
made: &'a Assembled,
) -> &'a str {
let person = e.author == owner || record.is_collaborator(&e.author);
let bot = record.is_runner(&e.author) && !person;
let merged = e.value["state"] == "merged" && bot;
let subject = e.value["subject"].as_str().unwrap_or_default();
let item = made.items.iter().find(|i| merged && i.uri == subject);
let queued = item
.and_then(|i| i.queue.as_ref())
.filter(|q| q.state == "queued");
queued.map_or(&e.author, |q| &q.by)
}
/// What a rebase or a rewrite keeps of a commit: who wrote it, when, and
/// what it says.
fn same(c: &read::Commit) -> (&str, &str, &str, &str) {
@@ -615,6 +660,55 @@ mod tests {
);
}
#[test]
fn merges_shown_as_queued() {
let pull = "at://did:o/dev.knotless.pull/p";
let queued = |state: &str| knotless::forge::Queued {
state: state.into(),
reason: None,
commit: None,
checks: vec![],
by: "did:o".into(),
at: "q".into(),
created_at: "t".into(),
};
let made = |queue| Assembled {
entries: vec![],
items: vec![knotless::forge::Item {
uri: pull.into(),
queue,
..Default::default()
}],
};
let state = |author: &str, state: &str| forge::Entry {
uri: format!("at://{author}/dev.knotless.state/s"),
author: author.into(),
value: serde_json::json!({"subject": pull, "state": state}),
};
let record: RepoRecord = serde_json::from_value(serde_json::json!({
"name": "r", "key": "k", "createdAt": "t",
"collaborators": [{"did": "did:c"}],
"runners": [{"did": "did:bot"}, {"did": "did:o"}, {"did": "did:c"}],
}))
.unwrap();
let shown = |author: &str, s: &str, made: &Assembled| {
shown_by(&state(author, s), "did:o", &record, made).to_string()
};
let queue = made(Some(queued("queued")));
// A runner's merge is whoever queued the pull's.
assert_eq!(shown("did:bot", "merged", &queue), "did:o");
// Anyone else's, a runner's that is the owner or a collaborator,
// and a runner's other changes, their own.
for who in ["did:m", "did:o", "did:c"] {
assert_eq!(shown(who, "merged", &queue), who);
}
assert_eq!(shown("did:bot", "closed", &queue), "did:bot");
// A pull no one queued, or that left the queue, is the runner's.
for made in [made(None), made(Some(queued("left")))] {
assert_eq!(shown("did:bot", "merged", &made), "did:bot");
}
}
#[test]
fn owners_and_names() {
assert_eq!(
crates/knotless-serve/src/index.rs+135−32
@@ -2806,6 +2806,18 @@ impl Index {
.optional()
}
/// The record of `did`'s repository `key`.
pub fn repo_of(&self, did: &str, key: &str) -> rusqlite::Result<Option<Value>> {
let db = self.db.lock().unwrap();
let found = db.query_row(
"SELECT value FROM repo WHERE did = ?1 AND key = ?2",
[did, key],
|row| row.get::<_, String>(0),
);
let found = found.optional()?;
Ok(found.and_then(|v| serde_json::from_str(&v).ok()))
}
/// The repository record at `uri`.
pub fn repo(&self, uri: &str) -> rusqlite::Result<Option<Value>> {
let db = self.db.lock().unwrap();
@@ -2949,7 +2961,8 @@ impl Index {
/// `author`'s alone, if named; or with `followed_by`, what it and whom
/// it follows wrote, and anything on its repositories and those it
/// starred. With `only`, `pull` or `issue`, those and changes of their
/// state alone; `comment`, comments alone.
/// state alone; `comment`, comments alone. With `quiet`, of a
/// repository's bots ([`bot_on`]) changes of state alone.
pub fn recent(
&self,
limit: usize,
@@ -2957,6 +2970,7 @@ impl Index {
author: Option<&str>,
followed_by: Option<&str>,
only: Option<&str>,
quiet: bool,
) -> rusqlite::Result<Vec<(Entry, String, String)>> {
let (at, uri) = after(cursor);
// One account's by forge_author_created, which a test of whether
@@ -2997,27 +3011,31 @@ impl Index {
AND json_extract(f.value, '$.subject') LIKE '%/dev.knotless.' || ?6 || '/%'))))
AND coalesce(json_extract(f.value, '$.subject'), '') NOT IN down
AND f.author NOT IN (SELECT subject FROM block WHERE did = f.repo_did)
AND (NOT ?7 OR f.uri LIKE '%/dev.knotless.state/%' OR NOT {BOT})
AND NOT EXISTS (SELECT 1 FROM owned o
WHERE o.repo_did = f.repo_did AND o.repo_key = f.repo_key
AND ((o.blocks AND o.subject = f.author)
OR (NOT o.blocks AND o.subject
IN (f.uri, coalesce(json_extract(f.value, '$.subject'), '')))))
ORDER BY json_extract(f.value, '$.createdAt') DESC, f.uri DESC LIMIT ?1"
.replace("{BY}", by),
.replace("{BY}", by)
.replace("{BOT}", &bot_on("f.author", "f.repo_did", "f.repo_key")),
)?;
let found = params![limit as i64, at, uri, author, followed_by, only];
let found = params![limit as i64, at, uri, author, followed_by, only, quiet];
stmt.query_map(found, recent)?.collect()
}
/// Refs that moved on repositories that stand, newest first; after
/// `cursor`, as [`cursor_at`] makes one, if given: `pusher`'s alone,
/// or those of `followed_by` and whom it follows, if named.
/// or those of `followed_by` and whom it follows, if named; with
/// `quiet`, none by the repository's bots ([`bot_on`]).
pub fn pushes(
&self,
limit: usize,
cursor: Option<&str>,
pusher: Option<&str>,
followed_by: Option<&str>,
quiet: bool,
) -> rusqlite::Result<Vec<Push>> {
let db = self.db.lock().unwrap();
let (at, uri) = after(cursor);
@@ -3028,18 +3046,22 @@ impl Index {
Some(_) => "(p.pusher = ?4 OR p.did = ?4)",
None => "?4 IS NULL",
};
// A pusher is credited only while the repository names them, so
// one it names as a bot is the one credited.
let bot = bot_on("coalesce(p.pusher, p.did)", "p.did", "p.key");
let mut stmt = db.prepare_cached(&format!(
"SELECT * FROM (
SELECT r.uri, p.did, r.name, r.fork_of, p.ref, p.before, p.after, p.created_at,
p.uri, {BY} AS by
FROM push p CROSS JOIN repo r ON r.did = p.did AND r.key = p.key
WHERE p.did NOT IN down AND r.uri NOT IN down AND {whose}
AND (p.created_at < ?2 OR (p.created_at = ?2 AND p.uri < ?3)))
AND (p.created_at < ?2 OR (p.created_at = ?2 AND p.uri < ?3))
AND (NOT ?6 OR NOT {bot}))
WHERE by NOT IN down AND (?4 IS NULL OR by = ?4)
AND (?5 IS NULL OR by = ?5 OR by IN (SELECT subject FROM follow WHERE did = ?5))
ORDER BY created_at DESC, uri DESC LIMIT ?1"
))?;
let found = params![limit as i64, at, uri, pusher, followed_by];
let found = params![limit as i64, at, uri, pusher, followed_by, quiet];
stmt.query_map(found, push)?.collect()
}
@@ -3451,6 +3473,19 @@ fn recent(row: &rusqlite::Row) -> rusqlite::Result<(Entry, String, String)> {
Ok((entry(row)?, row.get(3)?, row.get(4)?))
}
/// SQL saying whether the account `author` is a bot on the repository of
/// owner `did` and key `key`: one its record names as a runner or a
/// triager that is neither its owner nor a collaborator, who are people
/// a bot may act as.
fn bot_on(author: &str, did: &str, key: &str) -> String {
format!(
"EXISTS (SELECT 1 FROM repo b WHERE b.did = {did} AND b.key = {key} AND b.did != {author}
AND {author} IN (SELECT value ->> '$.did' FROM json_each(b.value, '$.runners')
UNION ALL SELECT value ->> '$.did' FROM json_each(b.value, '$.triagers'))
AND {author} NOT IN (SELECT value ->> '$.did' FROM json_each(b.value, '$.collaborators')))"
)
}
/// Where a page cut after the record `uri`, made at `at`, starts the next:
/// newest first, ties broken by URI, so the next starts strictly after it.
pub fn cursor_at(at: &str, uri: &str) -> String {
@@ -3746,7 +3781,10 @@ mod tests {
let mut days = BTreeMap::new();
days.insert("2026-01-02".to_string(), Day::default());
let seen = |days: &mut BTreeMap<String, Day>| {
let recent = index.recent(10, None, None, None, None).unwrap().len();
let recent = index
.recent(10, None, None, None, None, false)
.unwrap()
.len();
let heard = index
.notifications("did:a", &[], 10, None, None)
.unwrap()
@@ -4430,19 +4468,25 @@ mod tests {
assert_eq!(index.stars(10, Some(&at), None, None).unwrap(), all[1..]);
// What anyone wrote on a repository it starred, but not elsewhere
// by those it does not follow.
let said = index.recent(10, None, None, a, None).unwrap();
let said = index.recent(10, None, None, a, None, false).unwrap();
let said: Vec<_> = said.into_iter().map(|(e, ..)| e.uri).collect();
assert_eq!(said, ["at://did:c/dev.knotless.issue/5"]);
// Issues alone are both; pulls and comments, none.
let only = |kind| {
index
.recent(10, None, None, None, Some(kind))
.recent(10, None, None, None, Some(kind), false)
.unwrap()
.len()
};
assert_eq!((only("issue"), only("pull"), only("comment")), (2, 0, 0));
assert_eq!(index.recent(10, None, None, None, None).unwrap().len(), 2);
assert!(index.pushes(10, None, None, a).unwrap().is_empty());
assert_eq!(
index
.recent(10, None, None, None, None, false)
.unwrap()
.len(),
2
);
assert!(index.pushes(10, None, None, a, false).unwrap().is_empty());
}
#[test]
@@ -4466,8 +4510,8 @@ mod tests {
assert!(index.forks("did:a", "k").is_err());
assert!(index.forge("did:a", "k").is_err());
assert!(index.name("did:a", "k").is_err());
assert!(index.recent(1, None, None, None, None).is_err());
assert!(index.pushes(1, None, None, None).is_err());
assert!(index.recent(1, None, None, None, None, false).is_err());
assert!(index.pushes(1, None, None, None, false).is_err());
assert!(index.actor_days("did:a", None, "UTC").is_err());
assert!(index.actor_pushes("did:a", None, "UTC").is_err());
}
@@ -4495,7 +4539,13 @@ mod tests {
index.keep_profile("did:a", &[]).unwrap();
assert_eq!(index.profile("did:a").unwrap(), Some(vec![]));
// Nothing of it is in what the feed shows.
assert_eq!(index.recent(10, None, None, None, None).unwrap().len(), 0);
assert_eq!(
index
.recent(10, None, None, None, None, false)
.unwrap()
.len(),
0
);
}
#[test]
@@ -4683,7 +4733,8 @@ mod tests {
drop(db);
let index = Index::open(&path).unwrap();
let repo = json!({"name": "r", "key": "k", "createdAt": "2026-01-01T00:00:00Z",
"collaborators": [{"did": "did:b"}], "runners": [{"did": "did:r"}]});
"collaborators": [{"did": "did:b"}],
"runners": [{"did": "did:r"}, {"did": "did:b"}, {"did": "did:a"}]});
index.put("did:a", nsid::REPO, "r", &repo).unwrap();
// did:b made main, and moved it; did:r landed a batch on it; did:c,
// whom the repository does not name, moved it; and the owner did.
@@ -4703,19 +4754,36 @@ mod tests {
claim("k.m.3", "2026-01-05T00:00:00Z", Some("did:c"));
claim("k.m.4", "2026-01-06T00:00:00Z", None);
let by: Vec<String> = index
.pushes(10, None, None, None)
.pushes(10, None, None, None, false)
.unwrap()
.into_iter()
.map(|p| p.by)
.collect();
assert_eq!(by, ["did:a", "did:a", "did:r", "did:b", "did:b"]);
let of = |did| index.pushes(10, None, Some(did), None).unwrap().len();
// Quiet, none of the runner's: the owner and a collaborator it
// names as runners too are people.
let by: Vec<String> = index
.pushes(10, None, None, None, true)
.unwrap()
.into_iter()
.map(|p| p.by)
.collect();
assert_eq!(by, ["did:a", "did:a", "did:b", "did:b"]);
let of = |did| {
index
.pushes(10, None, Some(did), None, false)
.unwrap()
.len()
};
assert_eq!((of("did:a"), of("did:b"), of("did:c")), (2, 2, 0));
// Followers of did:b see its pushes to the owner's repository.
let follow = json!({"subject": "did:b", "createdAt": "2026-01-01T00:00:00Z"});
index.put("did:f", nsid::FOLLOW, "f", &follow).unwrap();
assert_eq!(
index.pushes(10, None, None, Some("did:f")).unwrap().len(),
index
.pushes(10, None, None, Some("did:f"), false)
.unwrap()
.len(),
2
);
// Each account's days count the pushes it made; the owner's
@@ -4738,7 +4806,7 @@ mod tests {
index
.take_down("did:web:me", "did:b", &json!({"did": "did:b"}), true)
.unwrap();
assert_eq!(index.pushes(10, None, None, None).unwrap().len(), 3);
assert_eq!(index.pushes(10, None, None, None, false).unwrap().len(), 3);
}
#[test]
@@ -4747,6 +4815,9 @@ mod tests {
let index = Index::open(&dir.path().join("i.db")).unwrap();
let repo = json!({"name": "r", "key": "k", "createdAt": "2026-01-01T00:00:00Z"});
index.put("did:a", nsid::REPO, "r", &repo).unwrap();
// Found by its key, not its record's.
assert_eq!(index.repo_of("did:a", "k").unwrap(), Some(repo.clone()));
assert_eq!(index.repo_of("did:a", "r").unwrap(), None);
let claim = |rkey: &str, from: Option<&str>, to: Option<&str>, at: &str| {
let mut c =
json!({"repo": "k", "name": "refs/heads/main", "generation": 0, "createdAt": at});
@@ -4796,7 +4867,7 @@ mod tests {
)
.unwrap();
assert_eq!(days, (Some("2026-01-03".into()), None));
let pushes = index.pushes(10, None, None, None).unwrap();
let pushes = index.pushes(10, None, None, None, false).unwrap();
assert_eq!(
pushes,
[
@@ -4836,11 +4907,14 @@ mod tests {
index.delete("did:a", nsid::CLAIM, "k.m.2").unwrap();
// Compaction drops claims; the history stays.
index.delete("did:a", nsid::CLAIM, "k.m.0").unwrap();
assert_eq!(index.pushes(10, None, None, None).unwrap().len(), 3);
assert_eq!(index.pushes(1, None, None, None).unwrap().len(), 1);
let all = index.pushes(10, None, None, None).unwrap();
assert_eq!(index.pushes(10, None, None, None, false).unwrap().len(), 3);
assert_eq!(index.pushes(1, None, None, None, false).unwrap().len(), 1);
let all = index.pushes(10, None, None, None, false).unwrap();
let at = cursor_at(&all[0].created_at, &all[0].uri);
assert_eq!(index.pushes(10, Some(&at), None, None).unwrap(), all[1..]);
assert_eq!(
index.pushes(10, Some(&at), None, None, false).unwrap(),
all[1..]
);
// The other refs' tips, a deleted one's where it was.
let side = |rkey: &str, c: Value| {
let mut c = c;
@@ -4877,7 +4951,7 @@ mod tests {
version("k.t", "refs/tags/v1", 1, "t1");
version("k.t", "refs/tags/v1", 2, "t2");
version("k.m", main, 1, "a1");
let pushes = index.pushes(10, None, None, None).unwrap();
let pushes = index.pushes(10, None, None, None, false).unwrap();
let tagged: Vec<_> = pushes
.iter()
.filter(|p| p.refname == "refs/tags/v1")
@@ -4969,22 +5043,32 @@ mod tests {
let thread = at(nsid::COMMENT, "3");
let resolved = json!({"repo": named, "subject": thread, "state": "resolved", "createdAt": "2026-01-08"});
index.put("did:b", nsid::STATE, "18", &resolved).unwrap();
let recent = index.recent(10, None, None, None, None).unwrap();
let recent = index.recent(10, None, None, None, None, false).unwrap();
let kinds: Vec<&str> = recent
.iter()
.map(|(e, _, _)| e.uri.rsplit('/').nth(1).unwrap())
.collect();
assert_eq!(kinds, [nsid::COMMENT, nsid::PULL, nsid::ISSUE, nsid::STATE]);
assert_eq!((recent[0].1.as_str(), recent[0].2.as_str()), ("did:a", "k"));
assert_eq!(index.recent(1, None, None, None, None).unwrap().len(), 1);
assert_eq!(
index
.recent(1, None, None, None, None, false)
.unwrap()
.len(),
1
);
// Paged by time and URI, the next page starting strictly after.
let first = &index.recent(1, None, None, None, None).unwrap()[0].0;
let first = &index.recent(1, None, None, None, None, false).unwrap()[0].0;
let at = cursor_at(first.value["createdAt"].as_str().unwrap(), &first.uri);
let rest = index.recent(10, Some(&at), None, None, None).unwrap();
let rest = index
.recent(10, Some(&at), None, None, None, false)
.unwrap();
assert_eq!(rest.len(), 3);
assert_eq!(rest[0].0.uri, recent[1].0.uri);
// One account's alone.
let theirs = index.recent(10, None, Some("did:b"), None, None).unwrap();
let theirs = index
.recent(10, None, Some("did:b"), None, None, false)
.unwrap();
let theirs: Vec<_> = theirs.iter().map(|(e, ..)| e.uri.as_str()).collect();
let all: Vec<_> = recent.iter().map(|(e, ..)| e.uri.as_str()).collect();
let by_b: Vec<_> = all
@@ -4993,10 +5077,29 @@ mod tests {
.collect();
assert!(!by_b.is_empty());
assert_eq!(theirs, by_b);
// Quiet, of a bot of the repository, a triager it names, changes of
// state alone; of its owner, a runner it names too, all.
let repo = json!({"name": "r", "key": "k", "createdAt": "2026-01-01T00:00:00Z",
"runners": [{"did": "did:a"}], "triagers": [{"did": "did:b"}]});
index.put("did:a", nsid::REPO, "r", &repo).unwrap();
let quiet = index.recent(10, None, None, None, None, true).unwrap();
let quiet: Vec<_> = quiet.iter().map(|(e, ..)| e.uri.as_str()).collect();
let kept: Vec<_> = recent
.iter()
.map(|(e, ..)| e.uri.as_str())
.filter(|u| !u.starts_with("at://did:b/") || u.contains("/dev.knotless.state/"))
.collect();
assert!(kept.len() < recent.len());
assert_eq!(quiet, kept);
// Gone with the repository.
index.delete("did:a", nsid::REPO, "r").unwrap();
assert!(index.pushes(10, None, None, None).unwrap().is_empty());
assert!(
index
.pushes(10, None, None, None, false)
.unwrap()
.is_empty()
);
}
#[test]
@@ -5158,7 +5261,7 @@ mod tests {
"createdAt": took, "pusher": pusher});
index.put("did:o", nsid::CLAIM, rkey, &claim).unwrap();
}
let pushes = index.pushes(10, None, None, None).unwrap();
let pushes = index.pushes(10, None, None, None, false).unwrap();
let mut by: Vec<String> = pushes.into_iter().map(|p| p.by).collect();
by.sort();
assert_eq!(by, ["did:m", "did:o"]);
crates/knotless-serve/tests/forge.rs+1−1
@@ -2784,7 +2784,7 @@ fn a_deleted_repository_leaves_nothing() {
// nor its mirror; the other repository keeps its own.
wait_until(|| said(&key) == 0 && !mirror.exists());
assert_eq!(said(&kept_key), 1);
let recent = index.recent(50, None, None, None, None).unwrap();
let recent = index.recent(50, None, None, None, None, false).unwrap();
assert!(recent.iter().all(|(_, _, k)| k != &key));
fails(&owner, &["refs", &url]);
}
docs/design/serve.md+7−2
@@ -281,8 +281,13 @@ answers](../reference/api.md#appview)), and the web pages read the same answers:
runners the repository record names now count, so naming or dropping a runner
takes effect on the next read. [runner.md](runner.md) describes checks.
- `dev.knotless.getFeed`: the front page's recent activity, everyone's or what
the viewer and those it follows did, or one account's. The page puts it
together further: one account's pushes to a repository within half an hour
the viewer and those it follows did, or one account's. Everyone's and the
followed hold nothing a repository's bots said or pushed on it: the
runners and triagers its record names, but its owner and collaborators,
who are people a bot may act as. What a bot says restates what people did
and would crowd them out. A pull a runner merged landing the merge queue
shows as merged by whoever queued it. A bot's own feed holds it all. The
page puts it together further: one account's pushes to a repository within half an hour
are one line, a merged pull takes in the push that landed it, and stars in
a row of one repository, or by one account, are one line.
- `dev.knotless.getActorActivity`: how much one account did each day, and when
lexicons/dev/knotless/getFeed.json+1−1
@@ -5,7 +5,7 @@
"defs": {
"main": {
"type": "query",
"description": "What happened on the public repositories an appview has indexed, newest first: repositories created and forked, refs pushed and deleted, pulls and issues opened, discussed and moved between states, and repositories starred. With filter `following`, what the account asking and those it follows did, and anything done on its repositories and those it starred; without, the same for anyone, but no stars.",
"description": "What happened on the public repositories an appview has indexed, newest first: repositories created and forked, refs pushed and deleted, pulls and issues opened, discussed and moved between states, and repositories starred. With filter `following`, what the account asking and those it follows did, and anything done on its repositories and those it starred; without, the same for anyone, but no stars. Without `actor`, what a repository's bots said or pushed on it is left out: the runners and triagers its record names, but its owner and collaborators. A pull a runner merged landing the merge queue is shown as merged by whoever queued it.",
"parameters": {
"type": "params",
"properties": {