chadtmiller.com/knotless

Git hosted in your atproto account, on any stock PDS, fast enough for agents.

git clone knotless://chadtmiller.com/knotless

needs the knotless git helper. set it up

Feed: pushes to a branch within an hour shown as one, and each commit once

A run of pushes by one account to one ref, each within an hour of the
last, is one event. Each commit is listed once on a page, by the earliest
event that brought it, and only when no other ref of its repository had
it before: a push to main of what a branch already held counts those
commits ("and 1 on another branch") rather than listing them again. A
pushed event says how many of its commits are new (new), beside count.

The screenshots' world has a branch that main then moved past, to show
it. Each checkout's shots have three ports of their own, from its path,
so two sessions' runs no longer take each other's; the commands on the
getting started page that name this server are covered in its shots, as
they name the port. And a file's raw answer at a commit hash, cached for
a year, is tested again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chad Miller and Claude Opus 5.5 committed on Oct 21, 2026 parent 5167b3e commit c2f591c
Showing 132 changed files with 388 additions and 165 deletions
CLAUDE.md+2−3
@@ -21,9 +21,8 @@ work is done.
no PDS. Look at the PNGs it writes under
`harness/shots/baseline/<platform>/`. After a change to what a page's
answer holds, `just shots-up` in the background once, then `just
shots-record`. If another checkout's shots hold the ports (2593 to
2595), set `SHOTS_PORT` to another base for all of these, though
`start` shows its port and will not match on another.
shots-record`. Each checkout's shots have three ports of their own,
from its path; `SHOTS_PORT` sets another base for all of these.
- `shots-check` and `shots-live` take only pages whose HTML or browser
changed since their baselines, as `shots` does; `SHOTS_ALL=1` takes
every one. Keep `just shots-up` running in the background while
crates/knotless-serve/src/index.rs+53−0
@@ -515,6 +515,30 @@ impl Index {
Ok(tip.flatten())
}
/// Where each of the repository at `repo`'s refs but `refname` was
/// just before the push `uri` at `at`, as far as the index saw; a ref
/// deleted by then, where it was when it was.
pub fn tips_before(
&self,
repo: &str,
refname: &str,
at: &str,
uri: &str,
) -> rusqlite::Result<Vec<String>> {
let db = self.db.lock().unwrap();
let mut stmt = db.prepare_cached(
"SELECT COALESCE(p.after, p.before), MAX(p.created_at || ' ' || p.uri)
FROM push p JOIN repo r ON r.did = p.did AND r.key = p.key
WHERE r.uri = ?1 AND p.ref != ?2
AND (p.created_at < ?3 OR (p.created_at = ?3 AND p.uri < ?4))
GROUP BY p.ref",
)?;
let tips: Vec<Option<String>> = stmt
.query_map([repo, refname, at, uri], tip)?
.collect::<rusqlite::Result<_>>()?;
Ok(tips.into_iter().flatten().collect())
}
/// Forks of `did`'s repository `key`.
pub fn forks(&self, did: &str, key: &str) -> rusqlite::Result<Vec<Listed>> {
let db = self.db.lock().unwrap();
@@ -565,6 +589,10 @@ fn push(row: &rusqlite::Row) -> rusqlite::Result<Push> {
})
}
fn tip(row: &rusqlite::Row) -> rusqlite::Result<Option<String>> {
row.get(0)
}
fn recent(row: &rusqlite::Row) -> rusqlite::Result<(Entry, String, String)> {
Ok((entry(row)?, row.get(3)?, row.get(4)?))
}
@@ -847,6 +875,31 @@ mod tests {
let all = index.pushes(10, None).unwrap();
let at = cursor_at(&all[0].created_at, &all[0].uri);
assert_eq!(index.pushes(10, Some(&at)).unwrap(), all[1..]);
// The other refs' tips, a deleted one's where it was.
let side = |rkey: &str, c: Value| {
let mut c = c;
c["repo"] = json!("k");
c["name"] = json!("refs/heads/side");
index.put("did:a", nsid::CLAIM, rkey, &c).unwrap();
};
side(
"k.s.0",
json!({"to": "s1", "createdAt": "2026-01-02T00:00:00Z"}),
);
let others = |at| index.tips_before(uri, main, at, "z").unwrap();
assert_eq!(others("2026-01-02T00:00:00Z"), ["s1"]);
assert!(others("2026-01-01T00:00:00Z").is_empty());
assert!(
index
.tips_before(uri, "refs/heads/side", "2026-01-02T00:00:00Z", "a")
.unwrap()
.is_empty()
);
side(
"k.s.1",
json!({"from": "s1", "createdAt": "2026-01-03T06:00:00Z"}),
);
assert_eq!(others("2026-01-04T00:00:00Z"), ["s1"]);
// Pulls, issues and what was said, newest first, without what the
// owner hides or blocks, or those records themselves.
crates/knotless-serve/src/web/feed.rs+142−23
@@ -2,7 +2,7 @@
//! knows, newest first. The front page shows it.
use super::*;
use crate::index::{Index, cursor_at};
use crate::index::{Index, Push, cursor_at};
use knotless::records::nsid;
use std::collections::HashSet;
@@ -13,6 +13,8 @@ const LIMIT: usize = 40;
const WALKED: usize = 100;
/// Commits a push lists; the rest are counted.
const LISTED: usize = 10;
/// Seconds between pushes to a ref that are shown as one.
const RUN: i64 = 60 * 60;
#[derive(Serialize, TS)]
#[ts(export, rename = "Feed")]
@@ -72,13 +74,15 @@ pub enum Action {
#[ts(optional)]
fork_of: Option<RepoView>,
},
/// A ref made or moved, and the newest of the commits that brought.
/// A ref made or moved, and the newest of the commits that brought
/// which no other ref had first.
#[serde(rename = "dev.knotless.getFeed#pushed")]
Pushed {
#[serde(rename = "ref")]
refname: String,
created: bool,
count: usize,
new: usize,
commits: Vec<CommitView>,
},
#[serde(rename = "dev.knotless.getFeed#deleted")]
@@ -257,11 +261,15 @@ async fn events(
.into_iter()
.filter_map(|(at, repo)| Some((at, repo.ok()?)))
.collect();
for p in &pushes {
let mirror = mirrors.get(&(p.did.clone(), p.name.clone()));
let action = match &p.to {
// Each commit is listed once, by the earliest run on the page to bring
// it that no other ref of its repository had it before.
let mut listed: HashSet<(&str, String)> = HashSet::new();
for run in runs(&pushes).iter().rev() {
let (last, first) = (run[0], run[run.len() - 1]);
let mirror = mirrors.get(&(last.did.clone(), last.name.clone()));
let action = match &last.to {
None => Action::Deleted {
refname: p.refname.clone(),
refname: last.refname.clone(),
},
Some(to) => {
// A ref that moved brought what it did not reach before; a
@@ -270,12 +278,12 @@ async fn events(
// a fork's is named for.
let head = mirror.map(|r| r.head.as_str()).unwrap_or_default();
let branch = format!("refs/heads/{head}");
let before = |at: &str| index.tip_before(at, &branch, &p.created_at);
let known: Vec<String> = match &p.from {
let before = |at: &str| index.tip_before(at, &branch, &first.created_at);
let known: Vec<String> = match &first.from {
Some(from) => vec![from.clone()],
None => [
(branch != p.refname).then_some(p.repo.as_str()),
p.fork_of.as_deref(),
(branch != first.refname).then_some(first.repo.as_str()),
first.fork_of.as_deref(),
]
.into_iter()
.flatten()
@@ -287,20 +295,33 @@ async fn events(
.flatten()
.collect(),
};
let known: Vec<&str> = known.iter().map(String::as_str).collect();
let brought = mirror
.and_then(|r| r.objects().ok())
.and_then(|o| o.brought(to, &known, WALKED).ok())
.unwrap_or_default();
let others = index
.tips_before(&first.repo, &first.refname, &first.created_at, &first.uri)
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
let walk = |known: &[&str]| {
mirror
.and_then(|r| r.objects().ok())
.and_then(|o| o.brought(to, known, WALKED).ok())
.unwrap_or_default()
};
let mut known: Vec<&str> = known.iter().map(String::as_str).collect();
let brought = walk(&known);
// A fork's copy of its source's refs is the fork itself.
if p.fork_of.is_some() && p.from.is_none() && brought.is_empty() {
if first.fork_of.is_some() && first.from.is_none() && brought.is_empty() {
continue;
}
known.extend(others.iter().map(String::as_str));
let new: Vec<_> = walk(&known)
.into_iter()
.filter(|c| listed.insert((last.repo.as_str(), c.sha.clone())))
.collect();
Action::Pushed {
refname: p.refname.clone(),
created: p.from.is_none(),
refname: last.refname.clone(),
created: first.from.is_none(),
count: brought.len(),
commits: brought
new: new.len(),
commits: new
.into_iter()
.take(LISTED)
.map(|c| CommitView {
@@ -312,12 +333,12 @@ async fn events(
}
};
let event = Event {
created_at: p.created_at.clone(),
actor: actor(&p.did),
repo: repo(&p.did, &p.name),
created_at: last.created_at.clone(),
actor: actor(&last.did),
repo: repo(&last.did, &last.name),
action,
};
events.push((p.uri.clone(), event));
events.push((last.uri.clone(), event));
}
// Pulls and issues, as their repositories' owners let them stand, by
@@ -385,10 +406,108 @@ async fn events(
Ok((events.into_iter().map(|(_, e)| e).collect(), next))
}
/// `pushes`, newest first, in runs shown as one, newest first, each newest
/// first: the pushes to a ref, each within an hour of the one after it.
/// A deletion is a run of its own, which ends the run after it.
fn runs(pushes: &[Push]) -> Vec<Vec<&Push>> {
let mut runs: Vec<Vec<&Push>> = Vec::new();
let mut open: HashMap<(&str, &str), usize> = HashMap::new();
for p in pushes {
let key = (p.repo.as_str(), p.refname.as_str());
let after = open.get(&key).filter(|&&i| {
let next = &runs[i][runs[i].len() - 1].created_at;
p.to.is_some() && within(&p.created_at, next)
});
match after {
Some(&i) => runs[i].push(p),
None if p.to.is_none() => {
open.remove(&key);
runs.push(vec![p]);
}
None => {
open.insert(key, runs.len());
runs.push(vec![p]);
}
}
}
runs
}
/// Whether `next` came within a run's gap of `at`.
fn within(at: &str, next: &str) -> bool {
seconds(at)
.zip(seconds(next))
.is_some_and(|(at, next)| next - at <= RUN)
}
/// Seconds since 1970 at `at`, a UTC datetime such as
/// `2026-10-21T22:50:00Z`, by Howard Hinnant's days from civil.
fn seconds(at: &str) -> Option<i64> {
let n = |from: usize, to: usize| at.get(from..to)?.parse::<i64>().ok();
let (y, m, d) = (n(0, 4)?, n(5, 7)?, n(8, 10)?);
let (h, min, s) = (n(11, 13)?, n(14, 16)?, n(17, 19)?);
let y = if m <= 2 { y - 1 } else { y };
let era = y.div_euclid(400);
let yoe = y - era * 400;
let doy = (153 * ((m + 9) % 12) + 2) / 5 + d - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
Some((era * 146097 + doe - 719468) * 86400 + h * 3600 + min * 60 + s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn times() {
assert_eq!(seconds("1970-01-01T00:00:00Z"), Some(0));
assert_eq!(seconds("2026-10-21T22:50:07.123Z"), Some(1792623007));
assert_eq!(seconds("2000-02-29T00:00:00Z"), Some(951782400));
assert_eq!(seconds("t"), None);
assert!(within("2026-01-01T00:00:00Z", "2026-01-01T01:00:00Z"));
assert!(!within("2026-01-01T00:00:00Z", "2026-01-01T01:00:01Z"));
assert!(!within("t", "2026-01-01T01:00:00Z"));
}
#[test]
fn pushes_in_runs() {
let push = |r: &str, at: &str, to: Option<&str>| Push {
uri: format!("at://did:a/dev.knotless.claim/{at}"),
repo: "at://did:a/dev.knotless.repo/r".into(),
did: "did:a".into(),
name: "r".into(),
fork_of: None,
refname: format!("refs/heads/{r}"),
from: None,
to: to.map(str::to_string),
created_at: format!("2026-01-01T{at}:00Z"),
};
// Newest first: main moved at five, four and three, a gap, then at
// one; side made at three thirty; main deleted at half past four.
let pushes = [
push("main", "05:00", Some("e")),
push("main", "04:30", None),
push("main", "04:00", Some("d")),
push("side", "03:30", Some("s")),
push("main", "03:00", Some("c")),
push("main", "01:00", Some("a")),
];
let runs: Vec<Vec<&str>> = runs(&pushes)
.into_iter()
.map(|run| run.into_iter().map(|p| &p.created_at[11..16]).collect())
.collect();
assert_eq!(
runs,
[
vec!["05:00"],
vec!["04:30"],
vec!["04:00", "03:00"],
vec!["03:30"],
vec!["01:00"]
]
);
}
#[test]
fn owners_and_names() {
assert_eq!(
crates/knotless-serve/tests/web.rs+37−13
@@ -315,6 +315,12 @@ fn browsing_a_repository() {
.starts_with("sandbox")
);
}
// A file at a commit's hash is the same forever; at a branch, it can move.
let cached = |rev: &str| {
site.get(&format!("{repo}/raw/{rev}/src/main.rs")).headers()["cache-control"].clone()
};
assert_eq!(cached(tip.trim()), "public, max-age=31536000, immutable");
assert_eq!(cached("main"), "no-cache");
let log = site.data(&format!("{repo}/commits/main"));
assert_eq!(log["commits"].as_array().unwrap().len(), 50);
@@ -834,8 +840,9 @@ fn the_feed() {
);
let work = owner.work("w", 1);
owner.git(&work, &["push", "-q", &url, "main"]);
// A push of several commits lists them, newest first; one back to
// where it was brings nothing.
// Pushes to a ref close together are one, from where the first found
// it to where the last left it, so one back to where it was counts
// only what stayed.
for i in 0..6 {
owner.commit_file(&work, "f", &format!("{i}\n"));
}
@@ -844,8 +851,11 @@ fn the_feed() {
owner.git(&work, &["checkout", "-q", "-b", "side"]);
owner.commit_file(&work, "s", "s\n");
owner.git(&work, &["push", "-q", &url, "side"]);
// A commit another ref had first is counted, not listed again; nor is
// one listed by an earlier event, though main was pushed first.
owner.git(&work, &["tag", "v1"]);
owner.git(&work, &["push", "-q", &url, "v1"]);
owner.git(&work, &["push", "-q", &url, "side:main"]);
owner.git(&work, &["push", "-q", &url, ":side"]);
let feed = shows(
o,
@@ -854,26 +864,40 @@ fn the_feed() {
("created", json!({"description": "Fed <to> the feed"})),
(
"pushed",
json!({"ref": "refs/heads/main", "created": false, "count": 6}),
json!({"ref": "refs/heads/main", "created": true, "count": 8, "new": 5}),
),
(
"pushed",
json!({"ref": "refs/heads/main", "created": false, "count": 0, "commits": []}),
json!({"ref": "refs/heads/side", "created": true, "count": 3, "new": 3}),
),
(
"pushed",
json!({"ref": "refs/heads/side", "created": true, "count": 3}),
json!({"ref": "refs/tags/v1", "created": true, "count": 3, "new": 0, "commits": []}),
),
("pushed", json!({"ref": "refs/tags/v1", "created": true})),
],
);
let six = about(&feed, o, "fed")
.into_iter()
.find(|e| e["action"]["count"] == 6)
.unwrap();
assert_eq!(six["action"]["commits"].as_array().unwrap().len(), 6);
assert_eq!(six["actor"]["handle"], o.as_str());
assert_eq!(six["actor"]["did"], owner.account.did.as_str());
let on = |r: &str| {
about(&feed, o, "fed")
.into_iter()
.filter(|e| e["action"]["ref"] == r)
.collect::<Vec<_>>()
};
let (main, side) = (on("refs/heads/main"), on("refs/heads/side"));
assert_eq!((main.len(), side.len()), (1, 2));
// None of what side listed.
let shas = |e: &Value| {
e["action"]["commits"]
.as_array()
.unwrap()
.iter()
.map(|c| c["sha"].as_str().unwrap().to_string())
.collect::<Vec<_>>()
};
let (main_shas, side_shas) = (shas(main[0]), shas(side[1]));
assert_eq!((main_shas.len(), side_shas.len()), (5, 3));
assert!(main_shas.iter().all(|sha| !side_shas.contains(sha)));
assert_eq!(main[0]["actor"]["handle"], o.as_str());
assert_eq!(main[0]["actor"]["did"], owner.account.did.as_str());
let fork = ok(&other, &["fork", &url]);
let clone = other.path("clone");
harness/shots/baseline/darwin/commit-desktop-dark.sha256+1−1
@@ -1 +1 @@
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
55523575b08edca3dd0533f9b61f8b8d676c6a6d8613d5831a5eb41da7d46fcd
\ No newline at end of file
harness/shots/baseline/darwin/commit-desktop-light.sha256+1−1
@@ -1 +1 @@
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
55523575b08edca3dd0533f9b61f8b8d676c6a6d8613d5831a5eb41da7d46fcd
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-dark.sha256+1−1
@@ -1 +1 @@
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
55523575b08edca3dd0533f9b61f8b8d676c6a6d8613d5831a5eb41da7d46fcd
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-light.sha256+1−1
@@ -1 +1 @@
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
55523575b08edca3dd0533f9b61f8b8d676c6a6d8613d5831a5eb41da7d46fcd
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-dark.sha256+1−1
@@ -1 +1 @@
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
c910b813a20a4915611565155ab84e92563129dd13d6d8ea825fc2f0fcfc67a4
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-light.sha256+1−1
@@ -1 +1 @@
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
c910b813a20a4915611565155ab84e92563129dd13d6d8ea825fc2f0fcfc67a4
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-dark.sha256+1−1
@@ -1 +1 @@
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
c910b813a20a4915611565155ab84e92563129dd13d6d8ea825fc2f0fcfc67a4
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-light.sha256+1−1
@@ -1 +1 @@
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
c910b813a20a4915611565155ab84e92563129dd13d6d8ea825fc2f0fcfc67a4
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-dark.sha256+1−1
@@ -1 +1 @@
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
9be5b93f7d33ce2b0ffec21715efe83f6e9cd3625c5ae144336ea55b7de65af4
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-light.sha256+1−1
@@ -1 +1 @@
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
9be5b93f7d33ce2b0ffec21715efe83f6e9cd3625c5ae144336ea55b7de65af4
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-dark.sha256+1−1
@@ -1 +1 @@
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
9be5b93f7d33ce2b0ffec21715efe83f6e9cd3625c5ae144336ea55b7de65af4
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-light.sha256+1−1
@@ -1 +1 @@
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
9be5b93f7d33ce2b0ffec21715efe83f6e9cd3625c5ae144336ea55b7de65af4
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-dark.sha256+1−1
@@ -1 +1 @@
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
c47fb4a9f08c24ac94baf8d3fa18447cb95a6f44a65832e87f919691c372cb16
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-light.sha256+1−1
@@ -1 +1 @@
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
c47fb4a9f08c24ac94baf8d3fa18447cb95a6f44a65832e87f919691c372cb16
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-dark.sha256+1−1
@@ -1 +1 @@
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
c47fb4a9f08c24ac94baf8d3fa18447cb95a6f44a65832e87f919691c372cb16
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-light.sha256+1−1
@@ -1 +1 @@
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
c47fb4a9f08c24ac94baf8d3fa18447cb95a6f44a65832e87f919691c372cb16
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-dark.sha256+1−1
@@ -1 +1 @@
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
946bd65188f6e54fe6cc1f2122f129a8f6b39ac5c0f6b6e1e6d378af4f85ae1d
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-light.sha256+1−1
@@ -1 +1 @@
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
946bd65188f6e54fe6cc1f2122f129a8f6b39ac5c0f6b6e1e6d378af4f85ae1d
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-dark.sha256+1−1
@@ -1 +1 @@
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
946bd65188f6e54fe6cc1f2122f129a8f6b39ac5c0f6b6e1e6d378af4f85ae1d
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-light.sha256+1−1
@@ -1 +1 @@
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
946bd65188f6e54fe6cc1f2122f129a8f6b39ac5c0f6b6e1e6d378af4f85ae1d
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
7a1853cb11a21d4246888ba642feab7a7c22d9566a5a87feee4afa45b05f9bdd
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-light.sha256+1−1
@@ -1 +1 @@
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
7a1853cb11a21d4246888ba642feab7a7c22d9566a5a87feee4afa45b05f9bdd
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-dark.sha256+1−1
@@ -1 +1 @@
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
7a1853cb11a21d4246888ba642feab7a7c22d9566a5a87feee4afa45b05f9bdd
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-light.sha256+1−1
@@ -1 +1 @@
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
7a1853cb11a21d4246888ba642feab7a7c22d9566a5a87feee4afa45b05f9bdd
\ No newline at end of file
harness/shots/baseline/darwin/home-desktop-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/home-desktop-dark.sha256+1−1
@@ -1 +1 @@
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
c0847961f51639a5b84b40e8af596b81e704161fbcf04793258db593a835f218
\ No newline at end of file
harness/shots/baseline/darwin/home-desktop-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/home-desktop-light.sha256+1−1
@@ -1 +1 @@
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
c0847961f51639a5b84b40e8af596b81e704161fbcf04793258db593a835f218
\ No newline at end of file
harness/shots/baseline/darwin/home-phone-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/home-phone-dark.sha256+1−1
@@ -1 +1 @@
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
c0847961f51639a5b84b40e8af596b81e704161fbcf04793258db593a835f218
\ No newline at end of file
harness/shots/baseline/darwin/home-phone-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/home-phone-light.sha256+1−1
@@ -1 +1 @@
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
c0847961f51639a5b84b40e8af596b81e704161fbcf04793258db593a835f218
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-dark.sha256+1−1
@@ -1 +1 @@
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
ed1a97810f3a60b19cb97bfd628f0fd58d8813db8869c689567c815dee14a712
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-light.sha256+1−1
@@ -1 +1 @@
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
ed1a97810f3a60b19cb97bfd628f0fd58d8813db8869c689567c815dee14a712
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-dark.sha256+1−1
@@ -1 +1 @@
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
ed1a97810f3a60b19cb97bfd628f0fd58d8813db8869c689567c815dee14a712
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-light.sha256+1−1
@@ -1 +1 @@
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
ed1a97810f3a60b19cb97bfd628f0fd58d8813db8869c689567c815dee14a712
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
a73b55b364c8514d91fab15c5598e8f730ec39d6c46fc8bed91b7211440bc1dd
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
a73b55b364c8514d91fab15c5598e8f730ec39d6c46fc8bed91b7211440bc1dd
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
a73b55b364c8514d91fab15c5598e8f730ec39d6c46fc8bed91b7211440bc1dd
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-light.sha256+1−1
@@ -1 +1 @@
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
a73b55b364c8514d91fab15c5598e8f730ec39d6c46fc8bed91b7211440bc1dd
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issue-desktop-dark.sha256+1−1
@@ -1 +1 @@
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
1ba98580965f7cc89bc716ce5753536ab7654aa1af1dc311c462a41f16a8507e
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issue-desktop-light.sha256+1−1
@@ -1 +1 @@
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
1ba98580965f7cc89bc716ce5753536ab7654aa1af1dc311c462a41f16a8507e
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issue-phone-dark.sha256+1−1
@@ -1 +1 @@
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
1ba98580965f7cc89bc716ce5753536ab7654aa1af1dc311c462a41f16a8507e
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issue-phone-light.sha256+1−1
@@ -1 +1 @@
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
1ba98580965f7cc89bc716ce5753536ab7654aa1af1dc311c462a41f16a8507e
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
3d230b1ad31a5d0fd3d64e3fefdb583ca57411d4027510015616637138fdfe89
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
3d230b1ad31a5d0fd3d64e3fefdb583ca57411d4027510015616637138fdfe89
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
3d230b1ad31a5d0fd3d64e3fefdb583ca57411d4027510015616637138fdfe89
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-light.sha256+1−1
@@ -1 +1 @@
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
3d230b1ad31a5d0fd3d64e3fefdb583ca57411d4027510015616637138fdfe89
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issues-desktop-dark.sha256+1−1
@@ -1 +1 @@
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
dc07d6d77285f6910fa1d6620bb0e9ba9b70ead8201015d95e9b119f1e8f8f77
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issues-desktop-light.sha256+1−1
@@ -1 +1 @@
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
dc07d6d77285f6910fa1d6620bb0e9ba9b70ead8201015d95e9b119f1e8f8f77
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issues-phone-dark.sha256+1−1
@@ -1 +1 @@
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
dc07d6d77285f6910fa1d6620bb0e9ba9b70ead8201015d95e9b119f1e8f8f77
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/issues-phone-light.sha256+1−1
@@ -1 +1 @@
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
dc07d6d77285f6910fa1d6620bb0e9ba9b70ead8201015d95e9b119f1e8f8f77
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-dark.sha256+1−1
@@ -1 +1 @@
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
dd7b363b46884884e156bfc9f823e7b411c8554f41b3a5e5a39c4cf8e49e95c6
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-light.sha256+1−1
@@ -1 +1 @@
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
dd7b363b46884884e156bfc9f823e7b411c8554f41b3a5e5a39c4cf8e49e95c6
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-dark.sha256+1−1
@@ -1 +1 @@
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
dd7b363b46884884e156bfc9f823e7b411c8554f41b3a5e5a39c4cf8e49e95c6
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-light.sha256+1−1
@@ -1 +1 @@
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
dd7b363b46884884e156bfc9f823e7b411c8554f41b3a5e5a39c4cf8e49e95c6
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-dark.sha256+1−1
@@ -1 +1 @@
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
9872ccfd82fe0e00914101bce6cee132af07c0bd50c14ad62deb0ffb7fbbe81b
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-light.sha256+1−1
@@ -1 +1 @@
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
9872ccfd82fe0e00914101bce6cee132af07c0bd50c14ad62deb0ffb7fbbe81b
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-dark.sha256+1−1
@@ -1 +1 @@
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
9872ccfd82fe0e00914101bce6cee132af07c0bd50c14ad62deb0ffb7fbbe81b
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-light.sha256+1−1
@@ -1 +1 @@
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
9872ccfd82fe0e00914101bce6cee132af07c0bd50c14ad62deb0ffb7fbbe81b
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-dark.sha256+1−1
@@ -1 +1 @@
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
d16cf0c2b2394a5f10a42cd322002f1d21644793a422d1a7bd7d31e401a1bb99
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-light.sha256+1−1
@@ -1 +1 @@
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
d16cf0c2b2394a5f10a42cd322002f1d21644793a422d1a7bd7d31e401a1bb99
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/profile-fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
a9b12b3f0dc154daa17bfbcb0cabc299c18c50437eae6655186cf5dc16303fd4
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-light.sha256+1−1
@@ -1 +1 @@
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
a9b12b3f0dc154daa17bfbcb0cabc299c18c50437eae6655186cf5dc16303fd4
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/profile-fork-phone-dark.sha256+1−1
@@ -1 +1 @@
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
a9b12b3f0dc154daa17bfbcb0cabc299c18c50437eae6655186cf5dc16303fd4
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-light.sha256+1−1
@@ -1 +1 @@
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
a9b12b3f0dc154daa17bfbcb0cabc299c18c50437eae6655186cf5dc16303fd4
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-dark.sha256+1−1
@@ -1 +1 @@
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
d16cf0c2b2394a5f10a42cd322002f1d21644793a422d1a7bd7d31e401a1bb99
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-light.sha256+1−1
@@ -1 +1 @@
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
d16cf0c2b2394a5f10a42cd322002f1d21644793a422d1a7bd7d31e401a1bb99
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-dark.sha256+1−1
@@ -1 +1 @@
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
747a24ce632a28e4192d5e3ffb1842c958ebc5b16d2350ddff5f3c3ab9af11cd
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-light.sha256+1−1
@@ -1 +1 @@
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
747a24ce632a28e4192d5e3ffb1842c958ebc5b16d2350ddff5f3c3ab9af11cd
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pull-open-desktop-dark.sha256+1−1
@@ -1 +1 @@
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
b12cb9cd3fecb1bcf020ee4684440bbccfd631997616928e30f4434ce1f98036
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pull-open-desktop-light.sha256+1−1
@@ -1 +1 @@
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
b12cb9cd3fecb1bcf020ee4684440bbccfd631997616928e30f4434ce1f98036
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pull-open-phone-dark.sha256+1−1
@@ -1 +1 @@
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
b12cb9cd3fecb1bcf020ee4684440bbccfd631997616928e30f4434ce1f98036
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pull-open-phone-light.sha256+1−1
@@ -1 +1 @@
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
b12cb9cd3fecb1bcf020ee4684440bbccfd631997616928e30f4434ce1f98036
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-dark.sha256+1−1
@@ -1 +1 @@
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
747a24ce632a28e4192d5e3ffb1842c958ebc5b16d2350ddff5f3c3ab9af11cd
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-light.sha256+1−1
@@ -1 +1 @@
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
747a24ce632a28e4192d5e3ffb1842c958ebc5b16d2350ddff5f3c3ab9af11cd
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
49f57b2917357ae0f3cf82e42b037fe2951f3a1e9cc92a4febc249b6c2531270
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
49f57b2917357ae0f3cf82e42b037fe2951f3a1e9cc92a4febc249b6c2531270
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
49f57b2917357ae0f3cf82e42b037fe2951f3a1e9cc92a4febc249b6c2531270
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-light.sha256+1−1
@@ -1 +1 @@
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
49f57b2917357ae0f3cf82e42b037fe2951f3a1e9cc92a4febc249b6c2531270
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pulls-desktop-dark.sha256+1−1
@@ -1 +1 @@
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
23463e7393c3d7e700b0453d9924466d72ab0b1e5201808e4d32718cd96ef3a7
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pulls-desktop-light.sha256+1−1
@@ -1 +1 @@
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
23463e7393c3d7e700b0453d9924466d72ab0b1e5201808e4d32718cd96ef3a7
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pulls-phone-dark.sha256+1−1
@@ -1 +1 @@
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
23463e7393c3d7e700b0453d9924466d72ab0b1e5201808e4d32718cd96ef3a7
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/pulls-phone-light.sha256+1−1
@@ -1 +1 @@
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
23463e7393c3d7e700b0453d9924466d72ab0b1e5201808e4d32718cd96ef3a7
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-dark.sha256+1−1
@@ -1 +1 @@
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
29422ff9c0c91e3b9baeb6ce66555c8df73ac0c39c0ee4e5a05412871e8dfa04
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-light.sha256+1−1
@@ -1 +1 @@
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
29422ff9c0c91e3b9baeb6ce66555c8df73ac0c39c0ee4e5a05412871e8dfa04
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-dark.sha256+1−1
@@ -1 +1 @@
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
29422ff9c0c91e3b9baeb6ce66555c8df73ac0c39c0ee4e5a05412871e8dfa04
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-light.sha256+1−1
@@ -1 +1 @@
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
29422ff9c0c91e3b9baeb6ce66555c8df73ac0c39c0ee4e5a05412871e8dfa04
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-dark.sha256+1−1
@@ -1 +1 @@
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
05d96d25012fd62b3b0cce0ad6034f1fe854810f691981dde52da3931f22c877
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-light.sha256+1−1
@@ -1 +1 @@
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
05d96d25012fd62b3b0cce0ad6034f1fe854810f691981dde52da3931f22c877
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-dark.sha256+1−1
@@ -1 +1 @@
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
05d96d25012fd62b3b0cce0ad6034f1fe854810f691981dde52da3931f22c877
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-light.sha256+1−1
@@ -1 +1 @@
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
05d96d25012fd62b3b0cce0ad6034f1fe854810f691981dde52da3931f22c877
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/start-desktop-dark.sha256+1−1
@@ -1 +1 @@
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
1f23dd671d3dbeee1455f45d21ac82100dfa8d33b9b953b97d3570ee65e26e1e
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/start-desktop-light.sha256+1−1
@@ -1 +1 @@
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
1f23dd671d3dbeee1455f45d21ac82100dfa8d33b9b953b97d3570ee65e26e1e
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-dark.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/start-phone-dark.sha256+1−1
@@ -1 +1 @@
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
1f23dd671d3dbeee1455f45d21ac82100dfa8d33b9b953b97d3570ee65e26e1e
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-light.png+0−0
A binary file, not shown.
harness/shots/baseline/darwin/start-phone-light.sha256+1−1
@@ -1 +1 @@
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
1f23dd671d3dbeee1455f45d21ac82100dfa8d33b9b953b97d3570ee65e26e1e
\ No newline at end of file
harness/shots/fixture/reading/more.mdnew+5−0
@@ -0,0 +1,5 @@
# Reading
- *A Philosophy of Software Design*, John Ousterhout
- *The Mythical Man-Month*, Fred Brooks
- *Working in Public*, Nadia Eghbal
harness/shots/fixture/reading/reading.mdnew+4−0
@@ -0,0 +1,4 @@
# Reading
- *A Philosophy of Software Design*, John Ousterhout
- *The Mythical Man-Month*, Fred Brooks
harness/shots/fixtures.json+20−20
@@ -14,12 +14,12 @@
"commit": "/alice.test/tally/commit/97727c2c19d5d8b3849b2ed9ad47130cee9798f5",
"pulls": "/alice.test/tally/pulls",
"pulls-closed": "/alice.test/tally/pulls?state=closed",
"pull": "/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g",
"pull-open": "/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi",
"pull": "/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr62puktrn",
"pull-open": "/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67mpz2lh",
"issues": "/alice.test/tally/issues",
"issues-closed": "/alice.test/tally/issues?state=closed",
"issue": "/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs",
"issue-closed": "/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m",
"issue": "/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67pi5ynj",
"issue-closed": "/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67tjxsqp",
"fork": "/bob.test/tally",
"empty": "/alice.test/scratch",
"missing": "/alice.test/tally/blob/main/nowhere"
@@ -31,7 +31,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"Alice Moreau\",\"bio\":\"Small tools for text. Writes tally.\",\"avatar\":null,\"about\":\"Small tools for text. Writes tally.\",\"repos\":[{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"notes\",\"description\":\"Scratch notes 📝\",\"forkOf\":null,\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"private\":false}]}"
"body": "{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"Alice Moreau\",\"bio\":\"Small tools for text. Writes tally.\",\"avatar\":null,\"about\":\"Small tools for text. Writes tally.\",\"repos\":[{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"createdAt\":\"2026-10-09T14:56:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"notes\",\"description\":\"Scratch notes 📝\",\"forkOf\":null,\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"private\":false}]}"
},
"/_data/alice.test/scratch": {
"status": 200,
@@ -97,21 +97,21 @@
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"rev\":\"main\",\"path\":\"\",\"page\":0,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"parents\":[\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\"],\"author\":{\"name\":\"Bob Tanaka\",\"account\":\"bob.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-09T09:31:00+00:00\",\"message\":\"Count no lines in empty input\"},{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},{\"sha\":\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\",\"parents\":[\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-06T10:02:00+00:00\",\"message\":\"Document usage and output\"},{\"sha\":\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\",\"parents\":[\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-05T17:21:00+00:00\",\"message\":\"Add a logo\"},{\"sha\":\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\",\"parents\":[\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-03T08:05:00+00:00\",\"message\":\"Read from stdin when no file is given\\n\\nSo tally works at the end of a pipe, as wc does. The name shown\\nfor stdin is \\\"-\\\".\"},{\"sha\":\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\",\"parents\":[\"cac02c464843db4d55fecfa69d327ef7b12702c0\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-02T14:40:00+00:00\",\"message\":\"Count words, lines and bytes\"},{\"sha\":\"cac02c464843db4d55fecfa69d327ef7b12702c0\",\"parents\":[],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-01T09:12:00+00:00\",\"message\":\"Start tally\"}],\"more\":false}"
},
"/_data/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs": {
"/_data/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67pi5ynj": {
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"title\":\"Fails on files that are not UTF-8\",\"state\":\"open\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T23:34:00.000Z\",\"base\":null,\"from\":null,\"branch\":null,\"body\":\"<p>Running <code>tally image.png</code> panics:</p>\\n<pre><code>thread 'main' panicked at src/main.rs:8:56:\\nstream did not contain valid UTF-8\\n</code></pre>\\n<p>wc counts bytes for any file.</p>\\n\",\"comments\":[{\"author\":\"alice.test\",\"avatar\":null,\"createdAt\":\"2026-10-10T00:11:00.000Z\",\"html\":\"<p>Confirmed. Reading bytes and counting words lossily would fix it.</p>\\n\",\"verdict\":null}],\"merged\":null,\"about\":\"Running `tally image.png` panics: ``` thread 'main' panicked at src/main.rs:8:56: stream did not contain valid UTF-8 ``` wc counts bytes for any file.\",\"card\":\"/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs/card.png?v=160a3e2db8db2127\"}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"title\":\"Fails on files that are not UTF-8\",\"state\":\"open\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-10T02:02:00.000Z\",\"base\":null,\"from\":null,\"branch\":null,\"body\":\"<p>Running <code>tally image.png</code> panics:</p>\\n<pre><code>thread 'main' panicked at src/main.rs:8:56:\\nstream did not contain valid UTF-8\\n</code></pre>\\n<p>wc counts bytes for any file.</p>\\n\",\"comments\":[{\"author\":\"alice.test\",\"avatar\":null,\"createdAt\":\"2026-10-10T02:39:00.000Z\",\"html\":\"<p>Confirmed. Reading bytes and counting words lossily would fix it.</p>\\n\",\"verdict\":null}],\"merged\":null,\"about\":\"Running `tally image.png` panics: ``` thread 'main' panicked at src/main.rs:8:56: stream did not contain valid UTF-8 ``` wc counts bytes for any file.\",\"card\":\"/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67pi5ynj/card.png?v=160a3e2db8db2127\"}"
},
"/_data/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m": {
"/_data/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67tjxsqp": {
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"title\":\"Typo in the README ✏️\",\"state\":\"closed\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"base\":null,\"from\":null,\"branch\":null,\"body\":null,\"comments\":[],\"merged\":null,\"about\":\"An issue on alice.test/tally by bob.test.\",\"card\":\"/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m/card.png?v=72fe47df61de13f9\"}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"title\":\"Typo in the README ✏️\",\"state\":\"closed\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-10T03:16:00.000Z\",\"base\":null,\"from\":null,\"branch\":null,\"body\":null,\"comments\":[],\"merged\":null,\"about\":\"An issue on alice.test/tally by bob.test.\",\"card\":\"/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67tjxsqp/card.png?v=72fe47df61de13f9\"}"
},
"/_data/alice.test/tally/issues": {
"status": 200,
@@ -119,7 +119,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"open\":true,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Fails on files that are not UTF-8\",\"state\":\"open\",\"href\":\"/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T23:34:00.000Z\",\"comments\":1}]}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"open\":true,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Fails on files that are not UTF-8\",\"state\":\"open\",\"href\":\"/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67pi5ynj\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-10T02:02:00.000Z\",\"comments\":1}]}"
},
"/_data/alice.test/tally/issues?state=closed": {
"status": 200,
@@ -127,23 +127,23 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"open\":false,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Typo in the README ✏️\",\"state\":\"closed\",\"href\":\"/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"comments\":0}]}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"issue\",\"open\":false,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Typo in the README ✏️\",\"state\":\"closed\",\"href\":\"/alice.test/tally/issue/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67tjxsqp\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-10T03:16:00.000Z\",\"comments\":0}]}"
},
"/_data/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g": {
"/_data/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr62puktrn": {
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"title\":\"Count no lines in empty input\",\"state\":\"merged\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T18:38:00.000Z\",\"base\":\"main\",\"from\":\"bob.test/tally\",\"branch\":\"empty-input\",\"body\":\"<p><code>tally</code> printed one line for an empty file. <code>lines</code> now yields nothing when the text is empty, and a test covers it.</p>\\n\",\"comments\":[{\"author\":\"alice.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T19:15:00.000Z\",\"html\":\"<p>Could <code>lines</code> check for emptiness once, rather than in the filter?</p>\\n\",\"verdict\":\"changes\"},{\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T19:52:00.000Z\",\"html\":\"<p>It only runs per line, so the cost is one comparison each. Happy to change it if you prefer.</p>\\n\",\"verdict\":null},{\"author\":\"alice.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T20:29:00.000Z\",\"html\":\"<p>Fair enough. Thanks!</p>\\n\",\"verdict\":\"approve\"}],\"merged\":\"686a36205b82620af07dd99997c570f04f461df9\",\"about\":\"`tally` printed one line for an empty file. `lines` now yields nothing when the text is empty, and a test covers it.\",\"card\":\"/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g/card.png?v=7f555a00efc7106b\"}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"title\":\"Count no lines in empty input\",\"state\":\"merged\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T21:06:00.000Z\",\"base\":\"main\",\"from\":\"bob.test/tally\",\"branch\":\"empty-input\",\"body\":\"<p><code>tally</code> printed one line for an empty file. <code>lines</code> now yields nothing when the text is empty, and a test covers it.</p>\\n\",\"comments\":[{\"author\":\"alice.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T21:43:00.000Z\",\"html\":\"<p>Could <code>lines</code> check for emptiness once, rather than in the filter?</p>\\n\",\"verdict\":\"changes\"},{\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T22:20:00.000Z\",\"html\":\"<p>It only runs per line, so the cost is one comparison each. Happy to change it if you prefer.</p>\\n\",\"verdict\":null},{\"author\":\"alice.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"html\":\"<p>Fair enough. Thanks!</p>\\n\",\"verdict\":\"approve\"}],\"merged\":\"686a36205b82620af07dd99997c570f04f461df9\",\"about\":\"`tally` printed one line for an empty file. `lines` now yields nothing when the text is empty, and a test covers it.\",\"card\":\"/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr62puktrn/card.png?v=7f555a00efc7106b\"}"
},
"/_data/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi": {
"/_data/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67mpz2lh": {
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"title\":\"Print counts as JSON with --json\",\"state\":\"open\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"base\":\"main\",\"from\":\"bob.test/tally\",\"branch\":\"json\",\"body\":\"<p>For scripts. The object has <code>lines</code>, <code>words</code>, <code>bytes</code> and <code>name</code>.</p>\\n\",\"comments\":[],\"merged\":null,\"about\":\"For scripts. The object has `lines`, `words`, `bytes` and `name`.\",\"card\":\"/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi/card.png?v=029fd2cd417c8b94\"}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"title\":\"Print counts as JSON with --json\",\"state\":\"open\",\"author\":\"bob.test\",\"avatar\":null,\"createdAt\":\"2026-10-10T01:25:00.000Z\",\"base\":\"main\",\"from\":\"bob.test/tally\",\"branch\":\"json\",\"body\":\"<p>For scripts. The object has <code>lines</code>, <code>words</code>, <code>bytes</code> and <code>name</code>.</p>\\n\",\"comments\":[],\"merged\":null,\"about\":\"For scripts. The object has `lines`, `words`, `bytes` and `name`.\",\"card\":\"/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67mpz2lh/card.png?v=029fd2cd417c8b94\"}"
},
"/_data/alice.test/tally/pulls": {
"status": 200,
@@ -151,7 +151,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"open\":true,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Print counts as JSON with --json\",\"state\":\"open\",\"href\":\"/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"comments\":0}]}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"open\":true,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Print counts as JSON with --json\",\"state\":\"open\",\"href\":\"/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr67mpz2lh\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-10T01:25:00.000Z\",\"comments\":0}]}"
},
"/_data/alice.test/tally/pulls?state=closed": {
"status": 200,
@@ -159,7 +159,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"open\":false,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Count no lines in empty input\",\"state\":\"merged\",\"href\":\"/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T18:38:00.000Z\",\"comments\":3}]}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"kind\":\"pull\",\"open\":false,\"opened\":1,\"closed\":1,\"items\":[{\"title\":\"Count no lines in empty input\",\"state\":\"merged\",\"href\":\"/alice.test/tally/pull/did:plc:7p6ytxf46wo2lwsehtykseck/3mygr62puktrn\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T21:06:00.000Z\",\"comments\":3}]}"
},
"/_data/alice.test/tally/tree/main/src": {
"status": 200,
@@ -175,7 +175,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":null,\"bio\":null,\"avatar\":null,\"about\":null,\"repos\":[{\"owner\":\"bob.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":{\"shown\":\"alice.test/tally\",\"href\":\"/alice.test/tally\"},\"createdAt\":\"2026-10-09T14:19:00.000Z\",\"private\":false}]}"
"body": "{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\",\"name\":null,\"bio\":null,\"avatar\":null,\"about\":null,\"repos\":[{\"owner\":\"bob.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":{\"shown\":\"alice.test/tally\",\"href\":\"/alice.test/tally\"},\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"private\":false}]}"
},
"/_data/bob.test/tally": {
"status": 200,
@@ -191,7 +191,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"query\":\"t\",\"repos\":[{\"owner\":\"bob.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"createdAt\":\"2026-10-09T14:19:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"notes\",\"description\":\"Scratch notes 📝\",\"forkOf\":null,\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"private\":false}]}"
"body": "{\"query\":\"t\",\"repos\":[{\"owner\":\"bob.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"createdAt\":\"2026-10-09T14:56:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"notes\",\"description\":\"Scratch notes 📝\",\"forkOf\":null,\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"private\":false}]}"
},
"/alice.test/tally/raw/main/assets/logo.svg": {
"status": 200,
@@ -216,7 +216,7 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"events\":[{\"createdAt\":\"2026-10-10T01:25:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokcei6c5m\",\"title\":\"Typo in the README ✏️\",\"state\":\"closed\"}},{\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokcei6c5m\",\"title\":\"Typo in the README ✏️\"}},{\"createdAt\":\"2026-10-10T00:11:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokbyh7bvs\",\"title\":\"Fails on files that are not UTF-8\",\"text\":\"Confirmed. Reading bytes and counting words lossily would fix it.\"}},{\"createdAt\":\"2026-10-09T23:34:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokbyh7bvs\",\"title\":\"Fails on files that are not UTF-8\"}},{\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygokbv5rksi\",\"title\":\"Print counts as JSON with --json\"}},{\"createdAt\":\"2026-10-09T22:20:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"state\":\"merged\"}},{\"createdAt\":\"2026-10-09T21:06:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":false,\"count\":1,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"message\":\"Count no lines in empty input\"}]}},{\"createdAt\":\"2026-10-09T20:29:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"text\":\"Fair enough. Thanks!\",\"verdict\":\"approve\"}},{\"createdAt\":\"2026-10-09T19:52:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"text\":\"It only runs per line, so the cost is one comparison each. Happy to change it if you prefer.\"}},{\"createdAt\":\"2026-10-09T19:15:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"text\":\"Could `lines` check for emptiness once, rather than in the filter?\",\"verdict\":\"changes\"}},{\"createdAt\":\"2026-10-09T18:38:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\"}},{\"createdAt\":\"2026-10-09T17:24:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/json\",\"created\":true,\"count\":1,\"commits\":[{\"sha\":\"2063dabcda7417d5831f3b6bfd8ffb18da82fb8e\",\"message\":\"Print counts as JSON with --json\"}]}},{\"createdAt\":\"2026-10-09T16:10:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/empty-input\",\"created\":true,\"count\":1,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"message\":\"Count no lines in empty input\"}]}},{\"createdAt\":\"2026-10-09T14:19:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"}}},{\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"scratch\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\"}},{\"createdAt\":\"2026-10-09T12:28:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":true,\"count\":1,\"commits\":[{\"sha\":\"e78543abf0f799b7eb5713ccdf8d88b84d0f8ec0\",\"message\":\"Begin\"}]}},{\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Scratch notes 📝\"}},{\"createdAt\":\"2026-10-09T10:37:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":true,\"count\":6,\"commits\":[{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"message\":\"Split parsing into its own module\"},{\"sha\":\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\",\"message\":\"Document usage and output\"},{\"sha\":\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\",\"message\":\"Add a logo\"},{\"sha\":\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\",\"message\":\"Read from stdin when no file is given\\n\\nSo tally works at the end of a pipe, as wc does. The name shown\\nfor stdin is \\\"-\\\".\"},{\"sha\":\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\",\"message\":\"Count words, lines and bytes\"},{\"sha\":\"cac02c464843db4d55fecfa69d327ef7b12702c0\",\"message\":\"Start tally\"}]}},{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Counts lines, words and bytes, with output that lines up\"}}]}"
"body": "{\"events\":[{\"createdAt\":\"2026-10-10T03:53:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.issue/3mygr67tjxsqp\",\"title\":\"Typo in the README ✏️\",\"state\":\"closed\"}},{\"createdAt\":\"2026-10-10T03:16:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.issue/3mygr67tjxsqp\",\"title\":\"Typo in the README ✏️\"}},{\"createdAt\":\"2026-10-10T02:39:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.issue/3mygr67pi5ynj\",\"title\":\"Fails on files that are not UTF-8\",\"text\":\"Confirmed. Reading bytes and counting words lossily would fix it.\"}},{\"createdAt\":\"2026-10-10T02:02:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.issue/3mygr67pi5ynj\",\"title\":\"Fails on files that are not UTF-8\"}},{\"createdAt\":\"2026-10-10T01:25:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.pull/3mygr67mpz2lh\",\"title\":\"Print counts as JSON with --json\"}},{\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.pull/3mygr62puktrn\",\"title\":\"Count no lines in empty input\",\"state\":\"merged\"}},{\"createdAt\":\"2026-10-09T23:34:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":false,\"count\":1,\"new\":1,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"message\":\"Count no lines in empty input\"}]}},{\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.pull/3mygr62puktrn\",\"title\":\"Count no lines in empty input\",\"text\":\"Fair enough. Thanks!\",\"verdict\":\"approve\"}},{\"createdAt\":\"2026-10-09T22:20:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.pull/3mygr62puktrn\",\"title\":\"Count no lines in empty input\",\"text\":\"It only runs per line, so the cost is one comparison each. Happy to change it if you prefer.\"}},{\"createdAt\":\"2026-10-09T21:43:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.pull/3mygr62puktrn\",\"title\":\"Count no lines in empty input\",\"text\":\"Could `lines` check for emptiness once, rather than in the filter?\",\"verdict\":\"changes\"}},{\"createdAt\":\"2026-10-09T21:06:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:7p6ytxf46wo2lwsehtykseck/dev.knotless.pull/3mygr62puktrn\",\"title\":\"Count no lines in empty input\"}},{\"createdAt\":\"2026-10-09T19:52:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/json\",\"created\":true,\"count\":1,\"new\":1,\"commits\":[{\"sha\":\"2063dabcda7417d5831f3b6bfd8ffb18da82fb8e\",\"message\":\"Print counts as JSON with --json\"}]}},{\"createdAt\":\"2026-10-09T18:38:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/empty-input\",\"created\":true,\"count\":1,\"new\":1,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"message\":\"Count no lines in empty input\"}]}},{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"actor\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:7p6ytxf46wo2lwsehtykseck\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"}}},{\"createdAt\":\"2026-10-09T15:33:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":false,\"count\":2,\"new\":1,\"commits\":[{\"sha\":\"661c67a96bf920012bc578c6c6895e4103128dfe\",\"message\":\"Add Working in Public\"}]}},{\"createdAt\":\"2026-10-09T14:56:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"scratch\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\"}},{\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/reading\",\"created\":true,\"count\":1,\"new\":1,\"commits\":[{\"sha\":\"7b66c6ffd4369e246fc00c2bcb3e16d0ed1846d1\",\"message\":\"Start a reading list\"}]}},{\"createdAt\":\"2026-10-09T12:28:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":true,\"count\":1,\"new\":1,\"commits\":[{\"sha\":\"e78543abf0f799b7eb5713ccdf8d88b84d0f8ec0\",\"message\":\"Begin\"}]}},{\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Scratch notes 📝\"}},{\"createdAt\":\"2026-10-09T10:37:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":true,\"count\":6,\"new\":6,\"commits\":[{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"message\":\"Split parsing into its own module\"},{\"sha\":\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\",\"message\":\"Document usage and output\"},{\"sha\":\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\",\"message\":\"Add a logo\"},{\"sha\":\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\",\"message\":\"Read from stdin when no file is given\\n\\nSo tally works at the end of a pipe, as wc does. The name shown\\nfor stdin is \\\"-\\\".\"},{\"sha\":\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\",\"message\":\"Count words, lines and bytes\"},{\"sha\":\"cac02c464843db4d55fecfa69d327ef7b12702c0\",\"message\":\"Start tally\"}]}},{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"actor\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:d7crjayi6h6l3ek2wvnvl7qa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Counts lines, words and bytes, with output that lines up\"}}]}"
}
}
}
harness/shots/views.spec.mjs+5−3
@@ -33,9 +33,11 @@ for (const name of names) {
await page.goto(path, { waitUntil: 'load' })
// The pages paint their highlighting once they are running.
await page.locator('html[data-ready]').waitFor({ state: 'attached' })
// A DID is new each run, as the PLC directory makes one per account:
// it is covered, so the rest can match to the pixel.
await expect(page).toHaveScreenshot(`${name}.png`, { fullPage: true, mask: [page.locator('.did')], maskColor: '#80808a' })
// A DID is new each run, as the PLC directory makes one per account,
// and a command naming this server names the port it is on: they are
// covered, so the rest can match to the pixel.
const mask = [page.locator('.did'), page.locator('.cmd', { hasText: /127\.0\.0\.1:\d+/ })]
await expect(page).toHaveScreenshot(`${name}.png`, { fullPage: true, mask, maskColor: '#80808a' })
writeFileSync(info.snapshotPath(`${name}.sha256`), sums[name])
})
})
harness/shots/world.mjs+14−2
@@ -10,6 +10,7 @@
// ctrl-c; written to target/shots/world.json
import { spawn, execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
@@ -21,8 +22,10 @@ export const root = resolve(here, '../..')
const fixture = join(here, 'fixture')
const bin = join(root, 'target/debug')
// Where the world answers, and the pages behind it on the next two ports:
// SHOTS_PORT moves them, for a second checkout's shots beside a first's.
export const port = Number(process.env.SHOTS_PORT ?? 2593)
// three of their own for each checkout, from its path, so the shots of two
// side by side do not meet; SHOTS_PORT moves them.
const own = 20000 + 3 * (createHash('sha256').update(root).digest().readUInt16BE(0) % 3000)
export const port = Number(process.env.SHOTS_PORT ?? own)
export const base = `http://127.0.0.1:${port}`
export const saved = join(root, 'target/shots/world.json')
@@ -196,7 +199,16 @@ export async function create() {
git('alice', n, null, 'init', '-q', '-b', 'main')
commit('alice', n, '2026-09-20T19:30:00Z', 'Begin', { 'notes/README.md': 'README.md' })
git('alice', n, null, 'push', '-q', notes, 'main')
// A branch, then main moved past it: the feed lists only what main
// brought that the branch had not.
git('alice', n, null, 'checkout', '-q', '-b', 'reading')
commit('alice', n, '2026-09-22T08:15:00Z', 'Start a reading list', { 'reading/reading.md': 'reading.md' })
git('alice', n, null, 'push', '-q', notes, 'reading')
git('alice', n, null, 'checkout', '-q', 'main')
git('alice', n, null, 'merge', '-q', '--ff-only', 'reading')
commit('alice', n, '2026-09-23T21:40:00Z', 'Add Working in Public', { 'reading/more.md': 'reading.md' })
knotless('alice', 'create', 'knotless://alice.test/scratch')
git('alice', n, null, 'push', '-q', notes, 'main')
const fork = knotless('bob', 'fork', tally)
const b = join(work, 'bob/tally')
lexicons/dev/knotless/getFeed.json+4−3
@@ -67,13 +67,14 @@
},
"pushed": {
"type": "object",
"description": "A ref created or moved. A new ref brings what the repository's default branch did not reach just before, nor its fork source's; a fork's copies of its source's refs are not events.",
"required": ["ref", "created", "count", "commits"],
"description": "A ref created or moved, or moved again and again by one account, each move within an hour of the last, as one. A new ref brings what the repository's default branch did not reach just before, nor its fork source's; a fork's copies of its source's refs are not events.",
"required": ["ref", "created", "count", "new", "commits"],
"properties": {
"ref": { "type": "string", "description": "The full ref name, such as refs/heads/main." },
"created": { "type": "boolean", "description": "Whether the ref was new." },
"count": { "type": "integer", "minimum": 0, "maximum": 100, "description": "Commits the push brought; 100 means at least that many." },
"commits": { "type": "array", "maxLength": 10, "items": { "type": "ref", "ref": "#commitView" }, "description": "The newest of them, newest first." }
"new": { "type": "integer", "minimum": 0, "maximum": 100, "description": "Of those, the commits no other ref of the repository had before, and no event before it on the same page lists; the rest are only counted." },
"commits": { "type": "array", "maxLength": 10, "items": { "type": "ref", "ref": "#commitView" }, "description": "The newest of the new ones, newest first." }
}
},
"deleted": {
web/src/app.css+1−1
@@ -211,6 +211,6 @@ main.wrap{padding-top:1.25rem}
.start .cmd{background:var(--panel)}
.results h1{font-size:18px;font-weight:600;margin:1rem 0}
.results .find{margin-bottom:1.25rem}
@media (max-width:640px){.feed>li{flex-direction:column;gap:.1rem}.feed time{width:auto}}
@media (max-width:640px){.feed>li{flex-direction:column;align-items:stretch;gap:.1rem}.feed time{width:auto}}
::highlight(comment){color:var(--comment)}::highlight(string){color:var(--string)}::highlight(keyword){color:var(--keyword)}::highlight(number){color:var(--number)}::highlight(function){color:var(--function)}::highlight(type){color:var(--type)}::highlight(tag){color:var(--tag)}::highlight(attr){color:var(--attr)}
::highlight(xins){background-color:var(--ins-x)}::highlight(xdel){background-color:var(--del-x)}::highlight(target){background-color:var(--target)}
web/src/lib/api/FeedAction.ts+1−1
@@ -2,4 +2,4 @@
import type { FeedCommit } from "./FeedCommit";
import type { FeedRepo } from "./FeedRepo";
export type FeedAction = { "$type": "dev.knotless.getFeed#created", description?: string, forkOf?: FeedRepo, } | { "$type": "dev.knotless.getFeed#pushed", ref: string, created: boolean, count: number, commits: Array<FeedCommit>, } | { "$type": "dev.knotless.getFeed#deleted", ref: string, } | { "$type": "dev.knotless.getFeed#opened", subject: string, title: string, } | { "$type": "dev.knotless.getFeed#commented", subject: string, title: string, text: string, verdict?: string, } | { "$type": "dev.knotless.getFeed#changed", subject: string, title: string, state: string, };
export type FeedAction = { "$type": "dev.knotless.getFeed#created", description?: string, forkOf?: FeedRepo, } | { "$type": "dev.knotless.getFeed#pushed", ref: string, created: boolean, count: number, new: number, commits: Array<FeedCommit>, } | { "$type": "dev.knotless.getFeed#deleted", ref: string, } | { "$type": "dev.knotless.getFeed#opened", subject: string, title: string, } | { "$type": "dev.knotless.getFeed#commented", subject: string, title: string, text: string, verdict?: string, } | { "$type": "dev.knotless.getFeed#changed", subject: string, title: string, state: string, };
web/src/lib/molecules/CommitList.svelte+7−3
@@ -1,9 +1,13 @@
<script lang="ts">
import type { FeedCommit } from '#lib/api/FeedCommit.ts';
import { subject } from '#lib/format.ts';
/** The commits a push brought, the first few listed and the rest counted. */
let { commits, count, repo }: { commits: FeedCommit[]; count: number; repo: string } = $props();
/**
* The commits a push brought, the first few listed and the rest counted;
* and those `elsewhere`, which other branches had first, counted only.
*/
let { commits, count, elsewhere, repo }: { commits: FeedCommit[]; count: number; elsewhere: number; repo: string } = $props();
const LISTED = 5;
const other = $derived(elsewhere === 1 ? 'on another branch' : 'on other branches');
</script>
{#if commits.length}<ul class="commits">{#each commits.slice(0, LISTED) as c (c.sha)}<li><a class="hash" href="{repo}/commit/{c.sha}">{c.sha.slice(0, 7)}</a><span class="clip">{subject(c.message)}</span></li>{/each}{#if count > LISTED}<li class="rest">and {count - LISTED} more</li>{/if}</ul>{/if}
{#if commits.length || elsewhere}<ul class="commits">{#each commits.slice(0, LISTED) as c (c.sha)}<li><a class="hash" href="{repo}/commit/{c.sha}">{c.sha.slice(0, 7)}</a><span class="clip">{subject(c.message)}</span></li>{/each}{#if count > LISTED}<li class="rest">and {count - LISTED} more</li>{/if}{#if elsewhere}<li class="rest">{commits.length ? `and ${elsewhere} ${other}` : `${elsewhere} ${other}`}</li>{/if}</ul>{/if}
web/src/lib/molecules/FeedItem.svelte+1−1
@@ -41,7 +41,7 @@
{#if a.$type === 'dev.knotless.getFeed#created' && a.description}
<p class="feed-note">{a.description}</p>
{:else if a.$type === 'dev.knotless.getFeed#pushed'}
<CommitList commits={a.commits} count={a.count} {repo} />
<CommitList commits={a.commits} count={a.new} elsewhere={a.count - a.new} {repo} />
{:else if a.$type === 'dev.knotless.getFeed#commented'}
<p class="feed-note">“{excerpt(a.text, 120)}”</p>
{/if}