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

Merged pulls vouch for their authors' commits

A pull's author's claims are believed in the commits the pull brought
into a repository, which the merge, made only by its owner or a
collaborator, vouched for; the owner and collaborators are matched first.
A merge's state record now says where the base was before it (from), so
from..commit is what it brought in; one recorded without it counts a merge
commit's second parent less its first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chad Miller and Claude Opus 5.5 committed on Oct 21, 2026 parent f81f6d6 commit 4e31bb7
Showing 105 changed files with 264 additions and 141 deletions
README.md+1−1
@@ -182,7 +182,7 @@ KNOTLESS_ADMIN_PASSWORD=… knotless admin takedown troll.test
Pages show a commit as its author's account, with its Bluesky picture,
when the address it was authored as is one the repository's owner or a
collaborator claims. Your first push claims the address git authors your
collaborator claims, or the author of the merged pull that brought it in. Your first push claims the address git authors your
commits as, and says so; claims are kept hashed, so they name no address:
```sh
crates/knotless-serve/src/web.rs+82−18
@@ -24,7 +24,7 @@ use knotless::remote::Remote;
use knotless::session;
use knotless::url::RepoUrl;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::sync::{Arc, LazyLock, Mutex};
use std::time::{Duration, Instant};
@@ -339,16 +339,25 @@ pub struct Author {
avatar: Option<String>,
}
/// The repository's people, by DID, with the addresses they claim, hashed.
type People = [(String, Vec<String>)];
/// One whose claims a repository's pages believe: its owner and its
/// collaborators everywhere in it, and the author of a pull merged into it
/// only in the commits the pull brought in, which the merge vouched for.
struct Vouched {
did: String,
/// The addresses it claims, hashed (`knotless::emails`).
hashes: Vec<String>,
/// The commits it is believed in, if not all.
only: Option<HashSet<String>>,
}
/// `name` at `email`, by account if `email` is one of `people`'s.
async fn author(app: &App, people: &People, name: String, email: &str) -> Author {
let by = people
.iter()
.find(|(did, hashes)| knotless::emails::is(did, email, hashes));
/// `name` at `email`, by account if `email` is one of `people`'s in `sha`.
async fn author(app: &App, people: &[Vouched], sha: &str, name: String, email: &str) -> Author {
let by = people.iter().find(|p| {
let here = p.only.as_ref().is_none_or(|only| only.contains(sha));
here && knotless::emails::is(&p.did, email, &p.hashes)
});
let (account, avatar) = match by {
Some((did, _)) => (Some(handle(app, did).await), avatar(app, did).await),
Some(p) => (Some(handle(app, &p.did).await), avatar(app, &p.did).await),
None => (None, None),
};
Author {
@@ -376,27 +385,76 @@ fn coauthor(line: &str) -> Option<(String, String)> {
Some((name.trim().to_string(), email.trim().to_string()))
}
/// `commits`, as the pages show them, each with the accounts that wrote it,
/// where those are the repository's owner or collaborators.
async fn shown(app: &App, repo: &Repo, commits: Vec<read::Commit>) -> Vec<Commit> {
/// What a merge as `merged` brought in, as `git rev-list` takes it: from
/// where the base was before it, or, for a merge recorded without that, a
/// merge commit's second parent less its first, which names nothing for
/// any other kind of merge.
fn brought(merged: &str, from: Option<&str>) -> String {
match from {
Some(from) => format!("{from}..{merged}"),
None => format!("{merged}^1..{merged}^2"),
}
}
static BROUGHT: LazyLock<Mutex<HashMap<String, HashSet<String>>>> = LazyLock::new(Mutex::default);
/// The commits `range` names in `repo`, kept once git has found them: a
/// merge never changes what it brought in.
async fn brought_in(repo: &Repo, range: String) -> HashSet<String> {
if let Some(known) = BROUGHT.lock().unwrap().get(&range) {
return known.clone();
}
let listed = repo.git.text(&["rev-list", &range], None).await;
let shas: HashSet<String> = listed
.iter()
.flat_map(|l| l.lines())
.map(String::from)
.collect();
if listed.is_ok() {
BROUGHT.lock().unwrap().insert(range, shas.clone());
}
shas
}
/// Whose claims `repo`'s pages believe, and where: its owner and
/// collaborators first, so no pull's author can take their addresses.
async fn vouched(app: &App, repo: &Repo) -> Vec<Vouched> {
let record = repo.record(app).unwrap_or_default();
let dids =
let entries = assembled(app, repo).await.unwrap_or_default();
let merged = forge::assemble(&record, &repo.did, &entries)
.into_iter()
.filter_map(|i| Some((i.author, brought(&i.merged?, i.merged_from.as_deref()))));
let everywhere =
std::iter::once(repo.did.clone()).chain(record.collaborators.into_iter().map(|c| c.did));
let people = futures::future::join_all(dids.map(|did| async {
let people = everywhere
.map(|did| (did, None))
.chain(merged.map(|(did, range)| (did, Some(range))));
futures::future::join_all(people.map(|(did, range)| async move {
let only = match range {
Some(range) => Some(brought_in(repo, range).await),
None => None,
};
let hashes = claims(app, &did).await;
(did, hashes)
Vouched { did, hashes, only }
}))
.await;
.await
}
/// `commits`, as the pages show them, each with the accounts that wrote it
/// where `repo`'s pages believe their claims (`vouched`).
async fn shown(app: &App, repo: &Repo, commits: Vec<read::Commit>) -> Vec<Commit> {
let people = vouched(app, repo).await;
let mut out = Vec::new();
for c in commits {
let mut helped = Vec::new();
for (name, email) in coauthors(&c.message) {
helped.push(author(app, &people, name, &email).await);
helped.push(author(app, &people, &c.sha, name, &email).await);
}
let by = author(app, &people, &c.sha, c.author.name, &c.author.email).await;
out.push(Commit {
sha: c.sha,
parents: c.parents,
author: author(app, &people, c.author.name, &c.author.email).await,
author: by,
coauthors: helped,
date: c.author.date,
message: c.message,
@@ -1485,6 +1543,12 @@ async fn item_card_png(
mod tests {
use super::*;
#[test]
fn what_merges_brought() {
assert_eq!(brought("m", Some("f")), "f..m");
assert_eq!(brought("m", None), "m^1..m^2");
}
#[test]
fn coauthor_trailers() {
let message = "Fix it\n\nCo-authored-by: Claude <noreply@anthropic.com>\nSigned-off-by: A <a@x>\nco-authored-by: B < b@x > \nCo-authored-by: nobody\n";
crates/knotless-serve/tests/web.rs+41−1
@@ -446,7 +446,19 @@ fn pulls_and_issues() {
let fork = ok(&other, &["fork", &url]);
let clone = other.path("clone");
other.git(other.dir.path(), &["clone", "-q", &fork, "clone"]);
other.commit_file(&clone, "b", "b\n");
ok(&other, &["email", "add", "other@example.com"]);
write(&clone, "b", b"b\n");
other.git(&clone, &["add", "b"]);
let as_other = [
"commit",
"-q",
"--author",
"Other <other@example.com>",
"-m",
"b",
];
other.git(&clone, &as_other);
let brought = other.git(&clone, &["rev-parse", "HEAD"]);
other.git(&clone, &["push", "-q", "origin", "main:topic"]);
let pull = ok(
&other,
@@ -473,6 +485,34 @@ fn pulls_and_issues() {
let other_did = &other.account.did;
let thread = format!("{repo}/pull/{other_did}/{}", rkey(&pull));
wait_until(|| site.data(&thread)["merged"].is_string());
// What the pull brought in shows as its author's: the merge vouched for
// it. Their address anywhere else is no one's.
let theirs = site.data(&format!("{repo}/commit/{brought}"));
assert_eq!(
theirs["commit"]["author"]["account"],
other.account.handle.as_str()
);
let args = [
"commit",
&url,
"-b",
"aside",
"-m",
"Not theirs",
"--put",
"c",
"-",
];
let author = ["--author", "Other", "other@example.com"];
let (code, sha) = owner.cli(&[&args[..], &author].concat(), b"c\n");
assert_eq!(code, 0, "{sha}");
let aside = format!("/_data{repo}/commit/{}", sha.trim());
wait_until(|| site.status(&aside).0 == 200);
assert_eq!(
site.json(&aside)["commit"]["author"]["account"],
Value::Null
);
let d = site.data(&thread);
assert_eq!(
(d["kind"].as_str(), d["title"].as_str()),
crates/knotless/src/forge.rs+11−3
@@ -96,8 +96,10 @@ pub struct Item {
pub title: String,
pub body: Option<String>,
pub state: String,
/// The merge, for a merged pull.
/// The merge, for a merged pull, and where the base was before it.
pub merged: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merged_from: Option<String>,
/// A pull's target branch, and the repository and branch it brings in.
pub base: Option<String>,
pub head: Option<PullHead>,
@@ -229,6 +231,7 @@ pub fn assemble(repo: &RepoRecord, owner: &str, entries: &[Entry]) -> Vec<Item>
body,
state: "open".into(),
merged: None,
merged_from: None,
base: pull.as_ref().map(|p| p.base.clone()),
head: pull.map(|p| p.head),
comments: 0,
@@ -256,6 +259,7 @@ pub fn assemble(repo: &RepoRecord, owner: &str, entries: &[Entry]) -> Vec<Item>
if let Some(state) = &s.state {
item.state = state.clone();
item.merged = s.commit.clone().filter(|_| state == "merged");
item.merged_from = s.from.clone().filter(|_| state == "merged");
}
if let (Some(base), Some(_)) = (&s.base, &item.base) {
item.base = Some(base.clone());
@@ -705,7 +709,7 @@ pub async fn set_state(ctx: &Ctx, target: &RepoUrl, subject: &str, state: &str)
bail!("a pull or issue is open or closed; merging takes `merge`");
}
let t = Target::open(ctx, target).await?;
write_state(ctx, &t, subject, state, None).await
write_state(ctx, &t, subject, state, None, None).await
}
async fn write_state(
@@ -714,12 +718,14 @@ async fn write_state(
subject: &str,
state: &str,
commit: Option<String>,
from: Option<String>,
) -> Result<String> {
let record = StateRecord {
repo: t.reference(),
subject: subject.to_string(),
state: Some(state.to_string()),
commit,
from,
base: None,
created_at: records::now(),
};
@@ -765,7 +771,8 @@ pub async fn merge(ctx: &Ctx, target: &RepoUrl, pull: &str) -> Result<String> {
gateway::push(ctx, &t.remote, &t.snap, git, &me, &[update]).await?
};
accepted(report)?;
write_state(ctx, &t, pull, "merged", Some(merged.clone())).await?;
let from = Some(base.clone());
write_state(ctx, &t, pull, "merged", Some(merged.clone()), from).await?;
Ok(merged)
}
@@ -967,6 +974,7 @@ pub async fn retarget(
subject: pull.to_string(),
state: None,
commit: None,
from: None,
base: Some(base.to_string()),
created_at: records::now(),
};
crates/knotless/src/records.rs+4−0
@@ -321,6 +321,10 @@ pub struct StateRecord {
/// The merge, for `merged`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
/// Where the base was before the merge, for `merged`: `from..commit`
/// is what the pull brought in.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
/// The branch a pull targets from here on, as when the branch it
/// targeted was renamed.
#[serde(default, skip_serializing_if = "Option::is_none")]
docs/design.md+9−3
@@ -34,7 +34,7 @@ dev.knotless.redirect <old name> to, space?
dev.knotless.pull <tid> repo, base, head {repo, branch}, title, body?
dev.knotless.issue <tid> repo, title, body?
dev.knotless.comment <tid> repo, subject, body, verdict?, commit?
dev.knotless.state <tid> repo, subject, state?, commit?, base?
dev.knotless.state <tid> repo, subject, state?, commit?, from?, base?
dev.knotless.hide <tid> repo, subject
dev.knotless.block <tid> subject, repo?
@@ -56,8 +56,14 @@ picture. Each is the hex SHA-256 of the DID, a newline and the address
trimmed and lowercased: salted with the DID, so the record names no address
and one address hashes apart in each account that claims it. Nothing proves
an address is the claimant's, so a page believes a claim only from the
repository's owner and collaborators: someone else claiming your address
gains nothing on repositories they have no part in. A commit knotless makes
repository's owner and collaborators, and from the author of a merged pull
in the commits that pull brought in: the merge, which only the owner or a
collaborator can make, vouched for those. A merge's state record says where
the base was before it (`from`), so `from..commit` is what it brought in;
one recorded without it counts a merge commit's second parent less its
first, and a fast-forward or squash as nothing. The owner and collaborators
are matched first, so no pull's author can take their addresses, and
someone else claiming yours gains nothing outside what they got merged. A commit knotless makes
for an account that names no author is authored as `<did>@knotless.invalid`,
which needs no claim. The first push from a machine claims the address git
authors commits as there, if the account has made no claim yet, and says
harness/shots/baseline/darwin/commit-desktop-dark.sha256+1−1
@@ -1 +1 @@
440d36c4612ee49fe665e550c643b3466236c39c5ca6e90dc6ee666d83217eea
\ No newline at end of file
d7388bde511d27eacb4d0a8d20e5dee223c08014ba808bceac6f5226a05649b6
\ No newline at end of file
harness/shots/baseline/darwin/commit-desktop-light.sha256+1−1
@@ -1 +1 @@
440d36c4612ee49fe665e550c643b3466236c39c5ca6e90dc6ee666d83217eea
\ No newline at end of file
d7388bde511d27eacb4d0a8d20e5dee223c08014ba808bceac6f5226a05649b6
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-dark.sha256+1−1
@@ -1 +1 @@
440d36c4612ee49fe665e550c643b3466236c39c5ca6e90dc6ee666d83217eea
\ No newline at end of file
d7388bde511d27eacb4d0a8d20e5dee223c08014ba808bceac6f5226a05649b6
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-light.sha256+1−1
@@ -1 +1 @@
440d36c4612ee49fe665e550c643b3466236c39c5ca6e90dc6ee666d83217eea
\ No newline at end of file
d7388bde511d27eacb4d0a8d20e5dee223c08014ba808bceac6f5226a05649b6
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-dark.sha256+1−1
@@ -1 +1 @@
5502e33926209193d4c84807c35c5ae74ddca54ae18e708529e679c71e8ff7d0
\ No newline at end of file
bda55bd4f89cedd3f2651037b365d711b22bdd6324558f2ba6f24410e31d1513
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-light.sha256+1−1
@@ -1 +1 @@
5502e33926209193d4c84807c35c5ae74ddca54ae18e708529e679c71e8ff7d0
\ No newline at end of file
bda55bd4f89cedd3f2651037b365d711b22bdd6324558f2ba6f24410e31d1513
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-dark.sha256+1−1
@@ -1 +1 @@
5502e33926209193d4c84807c35c5ae74ddca54ae18e708529e679c71e8ff7d0
\ No newline at end of file
bda55bd4f89cedd3f2651037b365d711b22bdd6324558f2ba6f24410e31d1513
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-light.sha256+1−1
@@ -1 +1 @@
5502e33926209193d4c84807c35c5ae74ddca54ae18e708529e679c71e8ff7d0
\ No newline at end of file
bda55bd4f89cedd3f2651037b365d711b22bdd6324558f2ba6f24410e31d1513
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-dark.sha256+1−1
@@ -1 +1 @@
76de795bae3c4de753aff3ac6a6cb6de85d66c6f6da3e0ee62f9c027b1bfa31f
\ No newline at end of file
5a4e4e2fa4fa923a2fdb9bc286b16e83e19b9a53e12d04fbbbb67f7b8e5512fd
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-light.sha256+1−1
@@ -1 +1 @@
76de795bae3c4de753aff3ac6a6cb6de85d66c6f6da3e0ee62f9c027b1bfa31f
\ No newline at end of file
5a4e4e2fa4fa923a2fdb9bc286b16e83e19b9a53e12d04fbbbb67f7b8e5512fd
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-dark.sha256+1−1
@@ -1 +1 @@
76de795bae3c4de753aff3ac6a6cb6de85d66c6f6da3e0ee62f9c027b1bfa31f
\ No newline at end of file
5a4e4e2fa4fa923a2fdb9bc286b16e83e19b9a53e12d04fbbbb67f7b8e5512fd
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-light.sha256+1−1
@@ -1 +1 @@
76de795bae3c4de753aff3ac6a6cb6de85d66c6f6da3e0ee62f9c027b1bfa31f
\ No newline at end of file
5a4e4e2fa4fa923a2fdb9bc286b16e83e19b9a53e12d04fbbbb67f7b8e5512fd
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-dark.sha256+1−1
@@ -1 +1 @@
d8b8a140035c13123992b6c28840d15f48be83e5a379e7adf7de813b827aead9
\ No newline at end of file
5ad4a8a6896ab5d79d46008f147c96d1e027f632a8e43347c7f66c397a16b095
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-light.sha256+1−1
@@ -1 +1 @@
d8b8a140035c13123992b6c28840d15f48be83e5a379e7adf7de813b827aead9
\ No newline at end of file
5ad4a8a6896ab5d79d46008f147c96d1e027f632a8e43347c7f66c397a16b095
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-dark.sha256+1−1
@@ -1 +1 @@
d8b8a140035c13123992b6c28840d15f48be83e5a379e7adf7de813b827aead9
\ No newline at end of file
5ad4a8a6896ab5d79d46008f147c96d1e027f632a8e43347c7f66c397a16b095
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-light.sha256+1−1
@@ -1 +1 @@
d8b8a140035c13123992b6c28840d15f48be83e5a379e7adf7de813b827aead9
\ No newline at end of file
5ad4a8a6896ab5d79d46008f147c96d1e027f632a8e43347c7f66c397a16b095
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-dark.sha256+1−1
@@ -1 +1 @@
5b88bc816b9d090af35cb671f8f88190ff5a505d3880877d304c289cd2defc7e
\ No newline at end of file
954a475e281c0be4d0885b634015ed628c92afdf96362561dc8afaf509ff6c40
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-light.sha256+1−1
@@ -1 +1 @@
5b88bc816b9d090af35cb671f8f88190ff5a505d3880877d304c289cd2defc7e
\ No newline at end of file
954a475e281c0be4d0885b634015ed628c92afdf96362561dc8afaf509ff6c40
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-dark.sha256+1−1
@@ -1 +1 @@
5b88bc816b9d090af35cb671f8f88190ff5a505d3880877d304c289cd2defc7e
\ No newline at end of file
954a475e281c0be4d0885b634015ed628c92afdf96362561dc8afaf509ff6c40
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-light.sha256+1−1
@@ -1 +1 @@
5b88bc816b9d090af35cb671f8f88190ff5a505d3880877d304c289cd2defc7e
\ No newline at end of file
954a475e281c0be4d0885b634015ed628c92afdf96362561dc8afaf509ff6c40
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
20e2f0b07532db960ee58283385c0cc657c34f6b14544a6e0fdb3493a97811ae
\ No newline at end of file
fd1fbfe20fbc3c2cad0426bbea857712cd0952df28ed8a0875a51353261e989a
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-light.sha256+1−1
@@ -1 +1 @@
20e2f0b07532db960ee58283385c0cc657c34f6b14544a6e0fdb3493a97811ae
\ No newline at end of file
fd1fbfe20fbc3c2cad0426bbea857712cd0952df28ed8a0875a51353261e989a
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-dark.sha256+1−1
@@ -1 +1 @@
20e2f0b07532db960ee58283385c0cc657c34f6b14544a6e0fdb3493a97811ae
\ No newline at end of file
fd1fbfe20fbc3c2cad0426bbea857712cd0952df28ed8a0875a51353261e989a
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-light.sha256+1−1
@@ -1 +1 @@
20e2f0b07532db960ee58283385c0cc657c34f6b14544a6e0fdb3493a97811ae
\ No newline at end of file
fd1fbfe20fbc3c2cad0426bbea857712cd0952df28ed8a0875a51353261e989a
\ 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 @@
e80e3eb2bfc534b484a709b66990b957e2f9a559f916780d011bf09e7238ef7c
\ No newline at end of file
d92704aeb8d9f2379f2b5e7757ffa4caa374a632356883a0e0355f4f9d2ce48c
\ 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 @@
e80e3eb2bfc534b484a709b66990b957e2f9a559f916780d011bf09e7238ef7c
\ No newline at end of file
d92704aeb8d9f2379f2b5e7757ffa4caa374a632356883a0e0355f4f9d2ce48c
\ 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 @@
e80e3eb2bfc534b484a709b66990b957e2f9a559f916780d011bf09e7238ef7c
\ No newline at end of file
d92704aeb8d9f2379f2b5e7757ffa4caa374a632356883a0e0355f4f9d2ce48c
\ 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 @@
e80e3eb2bfc534b484a709b66990b957e2f9a559f916780d011bf09e7238ef7c
\ No newline at end of file
d92704aeb8d9f2379f2b5e7757ffa4caa374a632356883a0e0355f4f9d2ce48c
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-dark.sha256+1−1
@@ -1 +1 @@
78c1143e163d3d72abcdccc20ee389e7015862d7cf77913a47e6177ef0c4eb2c
\ No newline at end of file
a4035513e7889066b45d946888db5037140ab03fdccba215488a47676d6530a9
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-light.sha256+1−1
@@ -1 +1 @@
78c1143e163d3d72abcdccc20ee389e7015862d7cf77913a47e6177ef0c4eb2c
\ No newline at end of file
a4035513e7889066b45d946888db5037140ab03fdccba215488a47676d6530a9
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-dark.sha256+1−1
@@ -1 +1 @@
78c1143e163d3d72abcdccc20ee389e7015862d7cf77913a47e6177ef0c4eb2c
\ No newline at end of file
a4035513e7889066b45d946888db5037140ab03fdccba215488a47676d6530a9
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-light.sha256+1−1
@@ -1 +1 @@
78c1143e163d3d72abcdccc20ee389e7015862d7cf77913a47e6177ef0c4eb2c
\ No newline at end of file
a4035513e7889066b45d946888db5037140ab03fdccba215488a47676d6530a9
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
ab8077fb74813ffa91614239f783e5532e3eec95f04450f28fdcd0a4eda5f10c
\ No newline at end of file
51df5a974b7d30e0c511dd4f14911605f20d038ee7e2e347bbc2dec5393a1658
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
ab8077fb74813ffa91614239f783e5532e3eec95f04450f28fdcd0a4eda5f10c
\ No newline at end of file
51df5a974b7d30e0c511dd4f14911605f20d038ee7e2e347bbc2dec5393a1658
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
ab8077fb74813ffa91614239f783e5532e3eec95f04450f28fdcd0a4eda5f10c
\ No newline at end of file
51df5a974b7d30e0c511dd4f14911605f20d038ee7e2e347bbc2dec5393a1658
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-light.sha256+1−1
@@ -1 +1 @@
ab8077fb74813ffa91614239f783e5532e3eec95f04450f28fdcd0a4eda5f10c
\ No newline at end of file
51df5a974b7d30e0c511dd4f14911605f20d038ee7e2e347bbc2dec5393a1658
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-dark.sha256+1−1
@@ -1 +1 @@
fad6af24f892148d5e8718de19a0db4673102d804468e6d4dc0c622dac2b2bbd
\ No newline at end of file
bd833d088c6345100e9608effac750311bf7735df535e75aca9f210dc0d7e973
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-light.sha256+1−1
@@ -1 +1 @@
fad6af24f892148d5e8718de19a0db4673102d804468e6d4dc0c622dac2b2bbd
\ No newline at end of file
bd833d088c6345100e9608effac750311bf7735df535e75aca9f210dc0d7e973
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-dark.sha256+1−1
@@ -1 +1 @@
fad6af24f892148d5e8718de19a0db4673102d804468e6d4dc0c622dac2b2bbd
\ No newline at end of file
bd833d088c6345100e9608effac750311bf7735df535e75aca9f210dc0d7e973
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-light.sha256+1−1
@@ -1 +1 @@
fad6af24f892148d5e8718de19a0db4673102d804468e6d4dc0c622dac2b2bbd
\ No newline at end of file
bd833d088c6345100e9608effac750311bf7735df535e75aca9f210dc0d7e973
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
15fbfd6776806f45b5208e93a435a1180020eac05e7ad7181158e30b120dc309
\ No newline at end of file
36444fee9d691bb1b4d440209ea098e39ca3ec2dd19623b1990e649bd31df536
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
15fbfd6776806f45b5208e93a435a1180020eac05e7ad7181158e30b120dc309
\ No newline at end of file
36444fee9d691bb1b4d440209ea098e39ca3ec2dd19623b1990e649bd31df536
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
15fbfd6776806f45b5208e93a435a1180020eac05e7ad7181158e30b120dc309
\ No newline at end of file
36444fee9d691bb1b4d440209ea098e39ca3ec2dd19623b1990e649bd31df536
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-light.sha256+1−1
@@ -1 +1 @@
15fbfd6776806f45b5208e93a435a1180020eac05e7ad7181158e30b120dc309
\ No newline at end of file
36444fee9d691bb1b4d440209ea098e39ca3ec2dd19623b1990e649bd31df536
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-dark.sha256+1−1
@@ -1 +1 @@
ac89bfb4b94df95c6d05cdca5ff01722d4f55fb324c9c3251ca4831dc97dc580
\ No newline at end of file
2a72873a2e2e6cc3657ba11901327a3be829087a592ac12fc1a0a6c0f701d107
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-light.sha256+1−1
@@ -1 +1 @@
ac89bfb4b94df95c6d05cdca5ff01722d4f55fb324c9c3251ca4831dc97dc580
\ No newline at end of file
2a72873a2e2e6cc3657ba11901327a3be829087a592ac12fc1a0a6c0f701d107
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-dark.sha256+1−1
@@ -1 +1 @@
ac89bfb4b94df95c6d05cdca5ff01722d4f55fb324c9c3251ca4831dc97dc580
\ No newline at end of file
2a72873a2e2e6cc3657ba11901327a3be829087a592ac12fc1a0a6c0f701d107
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-light.sha256+1−1
@@ -1 +1 @@
ac89bfb4b94df95c6d05cdca5ff01722d4f55fb324c9c3251ca4831dc97dc580
\ No newline at end of file
2a72873a2e2e6cc3657ba11901327a3be829087a592ac12fc1a0a6c0f701d107
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-dark.sha256+1−1
@@ -1 +1 @@
157975af8f8b3da770baed0128d1c7994779d273e4bd7b015c63a792226efa24
\ No newline at end of file
6df9521fc724a67815770faa5b5caf94d170b2b83fc0cac975b9ba742642d75c
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-light.sha256+1−1
@@ -1 +1 @@
157975af8f8b3da770baed0128d1c7994779d273e4bd7b015c63a792226efa24
\ No newline at end of file
6df9521fc724a67815770faa5b5caf94d170b2b83fc0cac975b9ba742642d75c
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-dark.sha256+1−1
@@ -1 +1 @@
157975af8f8b3da770baed0128d1c7994779d273e4bd7b015c63a792226efa24
\ No newline at end of file
6df9521fc724a67815770faa5b5caf94d170b2b83fc0cac975b9ba742642d75c
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-light.sha256+1−1
@@ -1 +1 @@
157975af8f8b3da770baed0128d1c7994779d273e4bd7b015c63a792226efa24
\ No newline at end of file
6df9521fc724a67815770faa5b5caf94d170b2b83fc0cac975b9ba742642d75c
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-dark.sha256+1−1
@@ -1 +1 @@
0f19dcb5e8a805fad3a8515ce61fa5714e6a37c4b1d3b22903d845c7c0caf778
\ No newline at end of file
844f001041b656a42ed953bbe32ee34cffdddd47d8df7af5ab2bda50c63ca3ad
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-light.sha256+1−1
@@ -1 +1 @@
0f19dcb5e8a805fad3a8515ce61fa5714e6a37c4b1d3b22903d845c7c0caf778
\ No newline at end of file
844f001041b656a42ed953bbe32ee34cffdddd47d8df7af5ab2bda50c63ca3ad
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-dark.sha256+1−1
@@ -1 +1 @@
0f19dcb5e8a805fad3a8515ce61fa5714e6a37c4b1d3b22903d845c7c0caf778
\ No newline at end of file
844f001041b656a42ed953bbe32ee34cffdddd47d8df7af5ab2bda50c63ca3ad
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-light.sha256+1−1
@@ -1 +1 @@
0f19dcb5e8a805fad3a8515ce61fa5714e6a37c4b1d3b22903d845c7c0caf778
\ No newline at end of file
844f001041b656a42ed953bbe32ee34cffdddd47d8df7af5ab2bda50c63ca3ad
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-dark.sha256+1−1
@@ -1 +1 @@
e4d992f4a2ee2d862a651bd4f7cbe8256a15272e3765fe80efad48f1cce3be5b
\ No newline at end of file
15a94b0bbb15f26a3cbfddf955ad7b528dfb0bd0a1a0537dfab41d0375ab5cd8
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-light.sha256+1−1
@@ -1 +1 @@
e4d992f4a2ee2d862a651bd4f7cbe8256a15272e3765fe80efad48f1cce3be5b
\ No newline at end of file
15a94b0bbb15f26a3cbfddf955ad7b528dfb0bd0a1a0537dfab41d0375ab5cd8
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
8732040b28836ac64c7c81be96521c40d37edb77c6a9862a132efd1cc6c19271
\ No newline at end of file
7c79e576f32bff157cf49c7b0b42b5d7c4c25ce00e748cb870d575bfe14b11a9
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-light.sha256+1−1
@@ -1 +1 @@
8732040b28836ac64c7c81be96521c40d37edb77c6a9862a132efd1cc6c19271
\ No newline at end of file
7c79e576f32bff157cf49c7b0b42b5d7c4c25ce00e748cb870d575bfe14b11a9
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-dark.sha256+1−1
@@ -1 +1 @@
8732040b28836ac64c7c81be96521c40d37edb77c6a9862a132efd1cc6c19271
\ No newline at end of file
7c79e576f32bff157cf49c7b0b42b5d7c4c25ce00e748cb870d575bfe14b11a9
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-light.sha256+1−1
@@ -1 +1 @@
8732040b28836ac64c7c81be96521c40d37edb77c6a9862a132efd1cc6c19271
\ No newline at end of file
7c79e576f32bff157cf49c7b0b42b5d7c4c25ce00e748cb870d575bfe14b11a9
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-dark.sha256+1−1
@@ -1 +1 @@
e4d992f4a2ee2d862a651bd4f7cbe8256a15272e3765fe80efad48f1cce3be5b
\ No newline at end of file
15a94b0bbb15f26a3cbfddf955ad7b528dfb0bd0a1a0537dfab41d0375ab5cd8
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-light.sha256+1−1
@@ -1 +1 @@
e4d992f4a2ee2d862a651bd4f7cbe8256a15272e3765fe80efad48f1cce3be5b
\ No newline at end of file
15a94b0bbb15f26a3cbfddf955ad7b528dfb0bd0a1a0537dfab41d0375ab5cd8
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-dark.sha256+1−1
@@ -1 +1 @@
3004f8688b3d2a3e42f32e320625c3b75964a09a85eed137f049e054254b6f98
\ No newline at end of file
91e9b0ab3edc32e396e5e69277672f730923ebbd5a226f9b3c8e5205d3ff5a25
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-light.sha256+1−1
@@ -1 +1 @@
3004f8688b3d2a3e42f32e320625c3b75964a09a85eed137f049e054254b6f98
\ No newline at end of file
91e9b0ab3edc32e396e5e69277672f730923ebbd5a226f9b3c8e5205d3ff5a25
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-dark.sha256+1−1
@@ -1 +1 @@
89758792ae2285bb2a8e74a2740cb28bdfcbf123f6a5a3c4e726ea7d4d89c020
\ No newline at end of file
4353913ac1351bdd1c8fb4aac48129bb65287d80e61c91c91fecc09455e0c749
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-light.sha256+1−1
@@ -1 +1 @@
89758792ae2285bb2a8e74a2740cb28bdfcbf123f6a5a3c4e726ea7d4d89c020
\ No newline at end of file
4353913ac1351bdd1c8fb4aac48129bb65287d80e61c91c91fecc09455e0c749
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-dark.sha256+1−1
@@ -1 +1 @@
89758792ae2285bb2a8e74a2740cb28bdfcbf123f6a5a3c4e726ea7d4d89c020
\ No newline at end of file
4353913ac1351bdd1c8fb4aac48129bb65287d80e61c91c91fecc09455e0c749
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-light.sha256+1−1
@@ -1 +1 @@
89758792ae2285bb2a8e74a2740cb28bdfcbf123f6a5a3c4e726ea7d4d89c020
\ No newline at end of file
4353913ac1351bdd1c8fb4aac48129bb65287d80e61c91c91fecc09455e0c749
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-dark.sha256+1−1
@@ -1 +1 @@
3004f8688b3d2a3e42f32e320625c3b75964a09a85eed137f049e054254b6f98
\ No newline at end of file
91e9b0ab3edc32e396e5e69277672f730923ebbd5a226f9b3c8e5205d3ff5a25
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-light.sha256+1−1
@@ -1 +1 @@
3004f8688b3d2a3e42f32e320625c3b75964a09a85eed137f049e054254b6f98
\ No newline at end of file
91e9b0ab3edc32e396e5e69277672f730923ebbd5a226f9b3c8e5205d3ff5a25
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
427d24e007b76b37421821612b8ae2097c006fac8b2976fa16513ea4f40fca79
\ No newline at end of file
16aeaa767b22ffcbeba5cbccf458423a04544a9fa326ac22bb15539200de9b5d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
427d24e007b76b37421821612b8ae2097c006fac8b2976fa16513ea4f40fca79
\ No newline at end of file
16aeaa767b22ffcbeba5cbccf458423a04544a9fa326ac22bb15539200de9b5d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
427d24e007b76b37421821612b8ae2097c006fac8b2976fa16513ea4f40fca79
\ No newline at end of file
16aeaa767b22ffcbeba5cbccf458423a04544a9fa326ac22bb15539200de9b5d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-light.sha256+1−1
@@ -1 +1 @@
427d24e007b76b37421821612b8ae2097c006fac8b2976fa16513ea4f40fca79
\ No newline at end of file
16aeaa767b22ffcbeba5cbccf458423a04544a9fa326ac22bb15539200de9b5d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-dark.sha256+1−1
@@ -1 +1 @@
6abe189560a91f2b9110070e476f4f5a2fcb0b3bb56ad764800c567d41372b41
\ No newline at end of file
beb590f1106cbd41c832c17e1a6f804fb0773208ecf57700da0a08a8b8d4f968
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-light.sha256+1−1
@@ -1 +1 @@
6abe189560a91f2b9110070e476f4f5a2fcb0b3bb56ad764800c567d41372b41
\ No newline at end of file
beb590f1106cbd41c832c17e1a6f804fb0773208ecf57700da0a08a8b8d4f968
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-dark.sha256+1−1
@@ -1 +1 @@
6abe189560a91f2b9110070e476f4f5a2fcb0b3bb56ad764800c567d41372b41
\ No newline at end of file
beb590f1106cbd41c832c17e1a6f804fb0773208ecf57700da0a08a8b8d4f968
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-light.sha256+1−1
@@ -1 +1 @@
6abe189560a91f2b9110070e476f4f5a2fcb0b3bb56ad764800c567d41372b41
\ No newline at end of file
beb590f1106cbd41c832c17e1a6f804fb0773208ecf57700da0a08a8b8d4f968
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-dark.sha256+1−1
@@ -1 +1 @@
6ad60f6db7f971516c50904155ea84b92be4b9ebc89dc3466998250308a904dc
\ No newline at end of file
7dbe154de3fc058f87f86efe437e3445d0119a448944e34768291d30f0e4e46a
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-light.sha256+1−1
@@ -1 +1 @@
6ad60f6db7f971516c50904155ea84b92be4b9ebc89dc3466998250308a904dc
\ No newline at end of file
7dbe154de3fc058f87f86efe437e3445d0119a448944e34768291d30f0e4e46a
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-dark.sha256+1−1
@@ -1 +1 @@
6ad60f6db7f971516c50904155ea84b92be4b9ebc89dc3466998250308a904dc
\ No newline at end of file
7dbe154de3fc058f87f86efe437e3445d0119a448944e34768291d30f0e4e46a
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-light.sha256+1−1
@@ -1 +1 @@
6ad60f6db7f971516c50904155ea84b92be4b9ebc89dc3466998250308a904dc
\ No newline at end of file
7dbe154de3fc058f87f86efe437e3445d0119a448944e34768291d30f0e4e46a
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-dark.sha256+1−1
@@ -1 +1 @@
b2743ac7f9fb0df24771023fe60a47e280f25ac44f623c86f3ea44ba904bef42
\ No newline at end of file
813d05f01a8c8c233b11b6dfe7e7689954da78bbebb9bf3d519e18d9fe2a54c6
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-light.sha256+1−1
@@ -1 +1 @@
b2743ac7f9fb0df24771023fe60a47e280f25ac44f623c86f3ea44ba904bef42
\ No newline at end of file
813d05f01a8c8c233b11b6dfe7e7689954da78bbebb9bf3d519e18d9fe2a54c6
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-dark.sha256+1−1
@@ -1 +1 @@
b2743ac7f9fb0df24771023fe60a47e280f25ac44f623c86f3ea44ba904bef42
\ No newline at end of file
813d05f01a8c8c233b11b6dfe7e7689954da78bbebb9bf3d519e18d9fe2a54c6
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-light.sha256+1−1
@@ -1 +1 @@
b2743ac7f9fb0df24771023fe60a47e280f25ac44f623c86f3ea44ba904bef42
\ No newline at end of file
813d05f01a8c8c233b11b6dfe7e7689954da78bbebb9bf3d519e18d9fe2a54c6
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-dark.sha256+1−1
@@ -1 +1 @@
6c1634323b284d1d6856a66a957a63491a95826a67c9f9f4c54557b7b09e2b66
\ No newline at end of file
3f534347e2a4289729ecfa412b35b2dae2225d3953b5df52b9ede4981d2f41e0
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-light.sha256+1−1
@@ -1 +1 @@
6c1634323b284d1d6856a66a957a63491a95826a67c9f9f4c54557b7b09e2b66
\ No newline at end of file
3f534347e2a4289729ecfa412b35b2dae2225d3953b5df52b9ede4981d2f41e0
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-dark.sha256+1−1
@@ -1 +1 @@
6c1634323b284d1d6856a66a957a63491a95826a67c9f9f4c54557b7b09e2b66
\ No newline at end of file
3f534347e2a4289729ecfa412b35b2dae2225d3953b5df52b9ede4981d2f41e0
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-light.sha256+1−1
@@ -1 +1 @@
6c1634323b284d1d6856a66a957a63491a95826a67c9f9f4c54557b7b09e2b66
\ No newline at end of file
3f534347e2a4289729ecfa412b35b2dae2225d3953b5df52b9ede4981d2f41e0
\ No newline at end of file
harness/shots/fixtures.json+22−22
@@ -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:jxhkyipyrdvki5qrhox3ipdg/3myggykid2nrg",
"pull-open": "/alice.test/tally/pull/did:plc:jxhkyipyrdvki5qrhox3ipdg/3myggylhg5l6v",
"pull": "/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg",
"pull-open": "/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x",
"issues": "/alice.test/tally/issues",
"issues-closed": "/alice.test/tally/issues?state=closed",
"issue": "/alice.test/tally/issue/did:plc:jxhkyipyrdvki5qrhox3ipdg/3myggyli5er6w",
"issue-closed": "/alice.test/tally/issue/did:plc:jxhkyipyrdvki5qrhox3ipdg/3myggylju6b6y",
"issue": "/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32",
"issue-closed": "/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u",
"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:rshwqpmddeusc43mph4ykwxa\",\"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:zq4qywccowjtdq3jltex43ro\",\"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}]}"
},
"/_data/alice.test/scratch": {
"status": 200,
@@ -47,7 +47,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\"},\"rev\":\"main\",\"path\":\"\",\"empty\":false,\"last\":{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"parents\":[\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\"],\"author\":{\"name\":\"Bob Tanaka\",\"account\":null,\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-09T09:31:00+00:00\",\"message\":\"Count no lines in empty input\"},\"entries\":[{\"name\":\"assets\",\"kind\":\"tree\",\"size\":null},{\"name\":\"docs\",\"kind\":\"tree\",\"size\":null},{\"name\":\"src\",\"kind\":\"tree\",\"size\":null},{\"name\":\".gitignore\",\"kind\":\"blob\",\"size\":8},{\"name\":\"Cargo.toml\",\"kind\":\"blob\",\"size\":122},{\"name\":\"README.md\",\"kind\":\"blob\",\"size\":388}],\"readme\":{\"name\":\"README.md\",\"html\":\"<h1>tally</h1>\\n<p>Counts lines, words and bytes, like <code>wc</code>, with output that lines up.</p>\\n<p><img src=\\\"/alice.test/tally/raw/main/assets/logo.svg\\\" alt=\\\"tally\\\" /></p>\\n<h2>Usage</h2>\\n<pre><code class=\\\"language-sh\\\">cargo install --path .\\ntally notes.txt\\necho \\\"one two\\\" | tally\\n</code></pre>\\n<h2>Output</h2>\\n<table><thead><tr><th>Column</th><th>Meaning</th></tr></thead><tbody>\\n<tr><td>1</td><td>lines</td></tr>\\n<tr><td>2</td><td>words, split on whitespace</td></tr>\\n<tr><td>3</td><td>bytes</td></tr>\\n</tbody></table>\\n<p>See <a href=\\\"/alice.test/tally/blob/main/docs/design.md\\\">the design notes</a> for why it reads the whole file.</p>\\n<h2>License</h2>\\n<p>MIT</p>\\n\",\"text\":null}}"
"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\":\"\",\"empty\":false,\"last\":{\"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\"},\"entries\":[{\"name\":\"assets\",\"kind\":\"tree\",\"size\":null},{\"name\":\"docs\",\"kind\":\"tree\",\"size\":null},{\"name\":\"src\",\"kind\":\"tree\",\"size\":null},{\"name\":\".gitignore\",\"kind\":\"blob\",\"size\":8},{\"name\":\"Cargo.toml\",\"kind\":\"blob\",\"size\":122},{\"name\":\"README.md\",\"kind\":\"blob\",\"size\":388}],\"readme\":{\"name\":\"README.md\",\"html\":\"<h1>tally</h1>\\n<p>Counts lines, words and bytes, like <code>wc</code>, with output that lines up.</p>\\n<p><img src=\\\"/alice.test/tally/raw/main/assets/logo.svg\\\" alt=\\\"tally\\\" /></p>\\n<h2>Usage</h2>\\n<pre><code class=\\\"language-sh\\\">cargo install --path .\\ntally notes.txt\\necho \\\"one two\\\" | tally\\n</code></pre>\\n<h2>Output</h2>\\n<table><thead><tr><th>Column</th><th>Meaning</th></tr></thead><tbody>\\n<tr><td>1</td><td>lines</td></tr>\\n<tr><td>2</td><td>words, split on whitespace</td></tr>\\n<tr><td>3</td><td>bytes</td></tr>\\n</tbody></table>\\n<p>See <a href=\\\"/alice.test/tally/blob/main/docs/design.md\\\">the design notes</a> for why it reads the whole file.</p>\\n<h2>License</h2>\\n<p>MIT</p>\\n\",\"text\":null}}"
},
"/_data/alice.test/tally/blob/main/README.md": {
"status": 200,
@@ -95,23 +95,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\"},\"rev\":\"main\",\"path\":\"\",\"page\":0,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"parents\":[\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\"],\"author\":{\"name\":\"Bob Tanaka\",\"account\":null,\"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}"
"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:jxhkyipyrdvki5qrhox3ipdg/3myggyli5er6w": {
"/_data/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32": {
"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:jxhkyipyrdvki5qrhox3ipdg/3myggyli5er6w/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-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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32/card.png?v=160a3e2db8db2127\"}"
},
"/_data/alice.test/tally/issue/did:plc:jxhkyipyrdvki5qrhox3ipdg/3myggylju6b6y": {
"/_data/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u": {
"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:jxhkyipyrdvki5qrhox3ipdg/3myggylju6b6y/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-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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u/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:jxhkyipyrdvki5qrhox3ipdg/3myggyli5er6w\",\"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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T23:34: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:jxhkyipyrdvki5qrhox3ipdg/3myggylju6b6y\",\"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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"comments\":0}]}"
},
"/_data/alice.test/tally/pull/did:plc:jxhkyipyrdvki5qrhox3ipdg/3myggykid2nrg": {
"/_data/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg": {
"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:jxhkyipyrdvki5qrhox3ipdg/3myggykid2nrg/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-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:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg/card.png?v=7f555a00efc7106b\"}"
},
"/_data/alice.test/tally/pull/did:plc:jxhkyipyrdvki5qrhox3ipdg/3myggylhg5l6v": {
"/_data/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x": {
"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:jxhkyipyrdvki5qrhox3ipdg/3myggylhg5l6v/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-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:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x/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:jxhkyipyrdvki5qrhox3ipdg/3myggylhg5l6v\",\"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:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T22:57: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:jxhkyipyrdvki5qrhox3ipdg/3myggykid2nrg\",\"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:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T18:38:00.000Z\",\"comments\":3}]}"
},
"/_data/alice.test/tally/tree/main/src": {
"status": 200,
@@ -167,7 +167,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\"},\"rev\":\"main\",\"path\":\"src\",\"empty\":false,\"last\":{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"parents\":[\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\"],\"author\":{\"name\":\"Bob Tanaka\",\"account\":null,\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-09T09:31:00+00:00\",\"message\":\"Count no lines in empty input\"},\"entries\":[{\"name\":\"lib.rs\",\"kind\":\"blob\",\"size\":783},{\"name\":\"main.rs\",\"kind\":\"blob\",\"size\":484},{\"name\":\"parse.rs\",\"kind\":\"blob\",\"size\":518}],\"readme\":null}"
"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\":\"src\",\"empty\":false,\"last\":{\"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\"},\"entries\":[{\"name\":\"lib.rs\",\"kind\":\"blob\",\"size\":783},{\"name\":\"main.rs\",\"kind\":\"blob\",\"size\":484},{\"name\":\"parse.rs\",\"kind\":\"blob\",\"size\":518}],\"readme\":null}"
},
"/_data/bob.test": {
"status": 200,
@@ -175,7 +175,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"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:6jyayarzmhqfvz4w4tgj4xes\",\"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}]}"
},
"/_data/bob.test/tally": {
"status": 200,
@@ -216,7 +216,7 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"events\":[{\"createdAt\":\"2026-10-10T01:25:00.000Z\",\"actor\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.issue/3myggylju6b6y\",\"title\":\"Typo in the README ✏️\",\"state\":\"closed\"}},{\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"actor\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.issue/3myggylju6b6y\",\"title\":\"Typo in the README ✏️\"}},{\"createdAt\":\"2026-10-10T00:11:00.000Z\",\"actor\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.issue/3myggyli5er6w\",\"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:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.issue/3myggyli5er6w\",\"title\":\"Fails on files that are not UTF-8\"}},{\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"actor\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.pull/3myggylhg5l6v\",\"title\":\"Print counts as JSON with --json\"}},{\"createdAt\":\"2026-10-09T22:20:00.000Z\",\"actor\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.pull/3myggykid2nrg\",\"title\":\"Count no lines in empty input\",\"state\":\"merged\"}},{\"createdAt\":\"2026-10-09T21:06:00.000Z\",\"actor\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"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:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.pull/3myggykid2nrg\",\"title\":\"Count no lines in empty input\",\"text\":\"Fair enough. Thanks!\",\"verdict\":\"approve\"}},{\"createdAt\":\"2026-10-09T19:52:00.000Z\",\"actor\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.pull/3myggykid2nrg\",\"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:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.pull/3myggykid2nrg\",\"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:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:jxhkyipyrdvki5qrhox3ipdg/dev.knotless.pull/3myggykid2nrg\",\"title\":\"Count no lines in empty input\"}},{\"createdAt\":\"2026-10-09T17:24:00.000Z\",\"actor\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"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:47:00.000Z\",\"actor\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"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:jxhkyipyrdvki5qrhox3ipdg\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:jxhkyipyrdvki5qrhox3ipdg\",\"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:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"tally\"}}},{\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"actor\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\",\"name\":\"scratch\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\"}},{\"createdAt\":\"2026-10-09T12:28:00.000Z\",\"actor\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"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:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"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:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"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:rshwqpmddeusc43mph4ykwxa\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:rshwqpmddeusc43mph4ykwxa\",\"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-10T01:25:00.000Z\",\"actor\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.issue/3mygjqa7nqp4u\",\"title\":\"Typo in the README ✏️\",\"state\":\"closed\"}},{\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"actor\":{\"did\":\"did:plc:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.issue/3mygjqa7nqp4u\",\"title\":\"Typo in the README ✏️\"}},{\"createdAt\":\"2026-10-10T00:11:00.000Z\",\"actor\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.issue/3mygjqa2suo32\",\"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:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.issue/3mygjqa2suo32\",\"title\":\"Fails on files that are not UTF-8\"}},{\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"actor\":{\"did\":\"did:plc:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.pull/3mygjq7w6et2x\",\"title\":\"Print counts as JSON with --json\"}},{\"createdAt\":\"2026-10-09T22:20:00.000Z\",\"actor\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.pull/3mygjq5dbkkmg\",\"title\":\"Count no lines in empty input\",\"state\":\"merged\"}},{\"createdAt\":\"2026-10-09T21:06:00.000Z\",\"actor\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"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:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.pull/3mygjq5dbkkmg\",\"title\":\"Count no lines in empty input\",\"text\":\"Fair enough. Thanks!\",\"verdict\":\"approve\"}},{\"createdAt\":\"2026-10-09T19:52:00.000Z\",\"actor\":{\"did\":\"did:plc:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.pull/3mygjq5dbkkmg\",\"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:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.pull/3mygjq5dbkkmg\",\"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:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:6jyayarzmhqfvz4w4tgj4xes/dev.knotless.pull/3mygjq5dbkkmg\",\"title\":\"Count no lines in empty input\"}},{\"createdAt\":\"2026-10-09T17:24:00.000Z\",\"actor\":{\"did\":\"did:plc:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:6jyayarzmhqfvz4w4tgj4xes\",\"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:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:6jyayarzmhqfvz4w4tgj4xes\",\"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:6jyayarzmhqfvz4w4tgj4xes\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:6jyayarzmhqfvz4w4tgj4xes\",\"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:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"}}},{\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"actor\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"scratch\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\"}},{\"createdAt\":\"2026-10-09T12:28:00.000Z\",\"actor\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"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:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"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:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"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:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:zq4qywccowjtdq3jltex43ro\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Counts lines, words and bytes, with output that lines up\"}}]}"
}
}
}
lexicons/dev/knotless/emails.json+1−1
@@ -5,7 +5,7 @@
"defs": {
"main": {
"type": "record",
"description": "Keyed self. Readers believe it only on repositories the account owns or is a collaborator on. An account's first push claims the address git commits as there; after that it changes only when the account says.",
"description": "Keyed self. Readers believe it on repositories the account owns or is a collaborator on, and in the commits a pull of its brought into a repository when merged. An account's first push claims the address git commits as there; after that it changes only when the account says.",
"key": "literal:self",
"record": {
"type": "object",
lexicons/dev/knotless/state.json+1−0
@@ -15,6 +15,7 @@
"subject": { "type": "string", "format": "at-uri", "description": "The dev.knotless.pull or dev.knotless.issue whose state changed." },
"state": { "type": "string", "maxLength": 64, "knownValues": ["open", "closed", "merged"], "description": "`merged` applies to pulls only. Absent when the record only sets `base`: the state stands." },
"commit": { "type": "string", "minLength": 40, "maxLength": 64, "description": "The hex object ID of the merge commit, for `merged`." },
"from": { "type": "string", "minLength": 40, "maxLength": 64, "description": "For `merged`, the hex object ID the base was at before the merge: from..commit is what the pull brought in, whose commits readers may then show as its author's." },
"base": { "type": "string", "maxLength": 255, "description": "For a pull, the branch it targets from here on, without refs/heads/: set when the branch it targeted was renamed." },
"createdAt": { "type": "string", "format": "datetime" }
}