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

Bluesky avatars wherever an account is shown

Repository headers, lists of repositories and what is said on pulls and
issues carry the account's avatar as Bluesky's CDN serves it, looked up
once per account and again whenever its profile page is shown. Commit
authors are git names, not accounts, and keep their lettered marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chad Miller committed on Oct 21, 2026 parent f536f2b commit 5fc251d
Showing 103 changed files with 215 additions and 145 deletions
crates/knotless-serve/src/web.rs+62−17
@@ -189,6 +189,46 @@ async fn handle(app: &App, did: &str) -> String {
shown
}
static AVATARS: LazyLock<Mutex<HashMap<String, Option<String>>>> = LazyLock::new(Mutex::default);
/// Where Bluesky's CDN serves profile pictures, by DID and blob CID.
const AVATAR_CDN: &str = "https://cdn.bsky.app/img/avatar/plain";
/// The account's Bluesky profile record, or nothing, if it has none.
async fn bsky_profile(remote: &Remote) -> serde_json::Value {
let found = remote
.get_record::<serde_json::Value>("app.bsky.actor.profile", "self")
.await;
found.ok().flatten().map(|r| r.value).unwrap_or_default()
}
/// The picture `about`, `did`'s profile record, names, as Bluesky's CDN
/// serves it.
fn avatar_url(did: &str, about: &serde_json::Value) -> Option<String> {
let cid = about["avatar"]["ref"]["$link"].as_str()?;
Some(format!("{AVATAR_CDN}/{did}/{cid}@jpeg"))
}
/// `did`'s profile picture, if it has one: looked up once, and again
/// whenever its profile page is shown.
async fn avatar(app: &App, did: &str) -> Option<String> {
if let Some(known) = AVATARS.lock().unwrap().get(did) {
return known.clone();
}
let at = RepoUrl {
authority: did.to_string(),
name: "_".into(),
};
let remote = Remote::connect(&app.ctx, &at).await.ok();
let about = futures::future::OptionFuture::from(remote.as_ref().map(bsky_profile)).await;
let found = avatar_url(did, &about.unwrap_or_default());
AVATARS
.lock()
.unwrap()
.insert(did.to_string(), found.clone());
found
}
/// `text` on one line, cut at `n` characters.
fn summary(text: &str, n: usize) -> String {
let flat = text.split_whitespace().collect::<Vec<_>>().join(" ");
@@ -261,6 +301,8 @@ impl From<read::Commit> for Commit {
#[ts(export)]
pub struct RepoHead {
authority: String,
/// The owner's profile picture, if it has one.
avatar: Option<String>,
name: String,
description: Option<String>,
fork_of: Option<Link>,
@@ -374,6 +416,7 @@ impl Repo {
});
RepoHead {
authority: self.authority.clone(),
avatar: avatar(app, &self.did).await,
name: self.name.clone(),
description,
fork_of,
@@ -422,6 +465,8 @@ pub struct SearchQuery {
#[ts(export)]
pub struct Listed {
owner: String,
/// The owner's profile picture, if it has one.
avatar: Option<String>,
name: String,
description: Option<String>,
fork_of: Option<Link>,
@@ -450,11 +495,13 @@ async fn search(State(app): State<Arc<App>>, Query(query): Query<SearchQuery>) -
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
let owners = futures::future::join_all(found.iter().map(|r| handle(&app, &r.did))).await;
let avatars = futures::future::join_all(found.iter().map(|r| avatar(&app, &r.did))).await;
let repos = found
.into_iter()
.zip(owners)
.map(|(r, owner)| Listed {
.zip(owners.into_iter().zip(avatars))
.map(|(r, (owner, avatar))| Listed {
owner,
avatar,
name: r.name,
description: r.description,
fork_of: None,
@@ -483,9 +530,6 @@ pub struct Profile {
repos: Vec<Listed>,
}
/// Where Bluesky's CDN serves profile pictures, by DID and blob CID.
const AVATAR_CDN: &str = "https://cdn.bsky.app/img/avatar/plain";
async fn profile(State(app): State<Arc<App>>, Path(authority): Path<String>) -> Result<Response> {
let at = RepoUrl {
authority: authority.clone(),
@@ -494,18 +538,11 @@ async fn profile(State(app): State<Arc<App>>, Path(authority): Path<String>) ->
let remote = Remote::connect(&app.ctx, &at).await.map_err(not_found)?;
let did = remote.did().to_string();
let shown = handle(&app, &did).await;
let about = remote
.get_record::<serde_json::Value>("app.bsky.actor.profile", "self")
.await
.ok()
.flatten()
.map(|r| r.value)
.unwrap_or_default();
let about = bsky_profile(&remote).await;
let text = |field: &str| about[field].as_str().map(str::to_string);
let name = text("displayName").filter(|n| !n.trim().is_empty());
let avatar = about["avatar"]["ref"]["$link"]
.as_str()
.map(|cid| format!("{AVATAR_CDN}/{did}/{cid}@jpeg"));
let avatar = avatar_url(&did, &about);
AVATARS.lock().unwrap().insert(did.clone(), avatar.clone());
let mut found = remote.list_repos().await.map_err(AppError::upstream)?;
let mut private = Vec::new();
if app.private
@@ -532,6 +569,7 @@ async fn profile(State(app): State<Arc<App>>, Path(authority): Path<String>) ->
.map(|(r, fork_of)| Listed {
private: private.contains(&r.rkey),
owner: shown.clone(),
avatar: avatar.clone(),
name: r.value.name,
description: r.value.description,
fork_of,
@@ -1141,6 +1179,8 @@ async fn items(
#[ts(export)]
pub struct Said {
author: String,
/// The author's profile picture, if it has one.
avatar: Option<String>,
created_at: String,
html: String,
/// `approve` or `changes`, for a review.
@@ -1158,6 +1198,8 @@ pub struct Discussion {
title: String,
state: String,
author: String,
/// The author's profile picture, if it has one.
avatar: Option<String>,
created_at: String,
/// A pull's target branch, and the repository and branch it brings.
base: Option<String>,
@@ -1234,11 +1276,13 @@ async fn item(
};
let base = repo.href("");
let names = futures::future::join_all(comments.iter().map(|c| handle(&app, &c.author))).await;
let avatars = futures::future::join_all(comments.iter().map(|c| avatar(&app, &c.author))).await;
let said = comments
.iter()
.zip(names)
.map(|(c, author)| Said {
.zip(names.into_iter().zip(avatars))
.map(|(c, (author, avatar))| Said {
author,
avatar,
created_at: c.value["createdAt"].as_str().unwrap_or_default().into(),
html: markdown(c.value["body"].as_str().unwrap_or_default(), &base, &base),
verdict: c.value["verdict"].as_str().map(str::to_string),
@@ -1253,6 +1297,7 @@ async fn item(
title: item.title,
state: item.state,
author,
avatar: avatar(&app, &item.author).await,
created_at: item.created_at,
from,
branch: item.head.map(|h| h.branch),
crates/knotless-serve/tests/web.rs+15−6
@@ -620,14 +620,23 @@ fn pulls_and_issues() {
assert_eq!(mine["handle"], owner.account.handle.as_str());
assert_eq!(mine["bio"], "Keeps things tracked.");
assert_eq!(mine["about"], "Keeps things tracked.");
// Its picture, as Bluesky's CDN serves it, wherever the account is
// shown: over its repositories, beside them, and beside what it said.
let cid = picture["blob"]["ref"]["$link"].as_str().unwrap();
assert_eq!(
mine["avatar"],
format!(
"https://cdn.bsky.app/img/avatar/plain/{}/{cid}@jpeg",
owner.account.did
)
let pictured = format!(
"https://cdn.bsky.app/img/avatar/plain/{}/{cid}@jpeg",
owner.account.did
);
assert_eq!(mine["avatar"], pictured.as_str());
assert_eq!(mine["repos"][0]["avatar"], pictured.as_str());
let discussed = site.data(&issue_page);
assert_eq!(discussed["repo"]["avatar"], pictured.as_str());
assert_eq!(discussed["avatar"], Value::Null);
let seen = discussed["comments"].as_array().unwrap().iter();
let seen: Vec<_> = seen
.filter(|c| c["author"] == owner.account.handle.as_str())
.collect();
assert_eq!(seen[0]["avatar"], pictured.as_str());
assert_eq!(
named(&mine["repos"], "tracked")["description"],
"Tracks <things>"
harness/shots/baseline/darwin/commit-desktop-dark.sha256+1−1
@@ -1 +1 @@
1541518c44a2c6bb3302b6ec0cd245c099d10781e2ad665e78dfc58d9c062dfa
\ No newline at end of file
6eda188fc6374fb68008295a6a6a99bdc3d1671cde04ad64886ab21227458a25
\ No newline at end of file
harness/shots/baseline/darwin/commit-desktop-light.sha256+1−1
@@ -1 +1 @@
1541518c44a2c6bb3302b6ec0cd245c099d10781e2ad665e78dfc58d9c062dfa
\ No newline at end of file
6eda188fc6374fb68008295a6a6a99bdc3d1671cde04ad64886ab21227458a25
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-dark.sha256+1−1
@@ -1 +1 @@
1541518c44a2c6bb3302b6ec0cd245c099d10781e2ad665e78dfc58d9c062dfa
\ No newline at end of file
6eda188fc6374fb68008295a6a6a99bdc3d1671cde04ad64886ab21227458a25
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-light.sha256+1−1
@@ -1 +1 @@
1541518c44a2c6bb3302b6ec0cd245c099d10781e2ad665e78dfc58d9c062dfa
\ No newline at end of file
6eda188fc6374fb68008295a6a6a99bdc3d1671cde04ad64886ab21227458a25
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-dark.sha256+1−1
@@ -1 +1 @@
39a3ad3f28561ef0c742c57783665f40ef0a9e7fb8871b44fbb8c2fec30ed2a0
\ No newline at end of file
9742a8d377e1e906a96d80a793e18f2e801d32bf985965b4e361ab4be19b4d40
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-light.sha256+1−1
@@ -1 +1 @@
39a3ad3f28561ef0c742c57783665f40ef0a9e7fb8871b44fbb8c2fec30ed2a0
\ No newline at end of file
9742a8d377e1e906a96d80a793e18f2e801d32bf985965b4e361ab4be19b4d40
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-dark.sha256+1−1
@@ -1 +1 @@
39a3ad3f28561ef0c742c57783665f40ef0a9e7fb8871b44fbb8c2fec30ed2a0
\ No newline at end of file
9742a8d377e1e906a96d80a793e18f2e801d32bf985965b4e361ab4be19b4d40
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-light.sha256+1−1
@@ -1 +1 @@
39a3ad3f28561ef0c742c57783665f40ef0a9e7fb8871b44fbb8c2fec30ed2a0
\ No newline at end of file
9742a8d377e1e906a96d80a793e18f2e801d32bf985965b4e361ab4be19b4d40
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-dark.sha256+1−1
@@ -1 +1 @@
b0b6c319627a76fb3b49e7b1f218f459cf68f09fe8cf212b77d48e4f6997db5e
\ No newline at end of file
a9eca4eebe82f7a8c6d7376dee0ac6ddd6d18c5a332ad58bae2acde8207e693d
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-light.sha256+1−1
@@ -1 +1 @@
b0b6c319627a76fb3b49e7b1f218f459cf68f09fe8cf212b77d48e4f6997db5e
\ No newline at end of file
a9eca4eebe82f7a8c6d7376dee0ac6ddd6d18c5a332ad58bae2acde8207e693d
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-dark.sha256+1−1
@@ -1 +1 @@
b0b6c319627a76fb3b49e7b1f218f459cf68f09fe8cf212b77d48e4f6997db5e
\ No newline at end of file
a9eca4eebe82f7a8c6d7376dee0ac6ddd6d18c5a332ad58bae2acde8207e693d
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-light.sha256+1−1
@@ -1 +1 @@
b0b6c319627a76fb3b49e7b1f218f459cf68f09fe8cf212b77d48e4f6997db5e
\ No newline at end of file
a9eca4eebe82f7a8c6d7376dee0ac6ddd6d18c5a332ad58bae2acde8207e693d
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-dark.sha256+1−1
@@ -1 +1 @@
87e5b1dde7ab2b30352e6b9c41c74f5d45367db70d5717860b8a6d4e28a975e8
\ No newline at end of file
c0f3aa55fe615760f584d5424753883dc69d61bbf4ac7ba1df50aa61fe9daf42
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-light.sha256+1−1
@@ -1 +1 @@
87e5b1dde7ab2b30352e6b9c41c74f5d45367db70d5717860b8a6d4e28a975e8
\ No newline at end of file
c0f3aa55fe615760f584d5424753883dc69d61bbf4ac7ba1df50aa61fe9daf42
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-dark.sha256+1−1
@@ -1 +1 @@
87e5b1dde7ab2b30352e6b9c41c74f5d45367db70d5717860b8a6d4e28a975e8
\ No newline at end of file
c0f3aa55fe615760f584d5424753883dc69d61bbf4ac7ba1df50aa61fe9daf42
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-light.sha256+1−1
@@ -1 +1 @@
87e5b1dde7ab2b30352e6b9c41c74f5d45367db70d5717860b8a6d4e28a975e8
\ No newline at end of file
c0f3aa55fe615760f584d5424753883dc69d61bbf4ac7ba1df50aa61fe9daf42
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-dark.sha256+1−1
@@ -1 +1 @@
90674a818ce5ae4c7722b433ea8f92a84b874b4380ba44fff2c5e648d556c92a
\ No newline at end of file
e63323407bcc445dc8dcc8188c721c84932db56142d5e73380c5dd173efc8549
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-light.sha256+1−1
@@ -1 +1 @@
90674a818ce5ae4c7722b433ea8f92a84b874b4380ba44fff2c5e648d556c92a
\ No newline at end of file
e63323407bcc445dc8dcc8188c721c84932db56142d5e73380c5dd173efc8549
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-dark.sha256+1−1
@@ -1 +1 @@
90674a818ce5ae4c7722b433ea8f92a84b874b4380ba44fff2c5e648d556c92a
\ No newline at end of file
e63323407bcc445dc8dcc8188c721c84932db56142d5e73380c5dd173efc8549
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-light.sha256+1−1
@@ -1 +1 @@
90674a818ce5ae4c7722b433ea8f92a84b874b4380ba44fff2c5e648d556c92a
\ No newline at end of file
e63323407bcc445dc8dcc8188c721c84932db56142d5e73380c5dd173efc8549
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
91b63089b6f59cc8405abfba5b8b6cdb0c88e74b73d082c340d39e013844dbf6
\ No newline at end of file
454a3ac6ac89d5c0e4c3a473a6f39584415fd3e5fce112de348e429ae74386ee
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-light.sha256+1−1
@@ -1 +1 @@
91b63089b6f59cc8405abfba5b8b6cdb0c88e74b73d082c340d39e013844dbf6
\ No newline at end of file
454a3ac6ac89d5c0e4c3a473a6f39584415fd3e5fce112de348e429ae74386ee
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-dark.sha256+1−1
@@ -1 +1 @@
91b63089b6f59cc8405abfba5b8b6cdb0c88e74b73d082c340d39e013844dbf6
\ No newline at end of file
454a3ac6ac89d5c0e4c3a473a6f39584415fd3e5fce112de348e429ae74386ee
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-light.sha256+1−1
@@ -1 +1 @@
91b63089b6f59cc8405abfba5b8b6cdb0c88e74b73d082c340d39e013844dbf6
\ No newline at end of file
454a3ac6ac89d5c0e4c3a473a6f39584415fd3e5fce112de348e429ae74386ee
\ No newline at end of file
harness/shots/baseline/darwin/home-desktop-dark.sha256+1−1
@@ -1 +1 @@
81f2defd3d1d369ba4b9d010480be85b351b188d467bf127783e2c921f1b535d
\ No newline at end of file
0c35dd41fa06540b3bb995ad331ffc66906deb329dc6e1665a2d0df3bd05215a
\ No newline at end of file
harness/shots/baseline/darwin/home-desktop-light.sha256+1−1
@@ -1 +1 @@
81f2defd3d1d369ba4b9d010480be85b351b188d467bf127783e2c921f1b535d
\ No newline at end of file
0c35dd41fa06540b3bb995ad331ffc66906deb329dc6e1665a2d0df3bd05215a
\ No newline at end of file
harness/shots/baseline/darwin/home-phone-dark.sha256+1−1
@@ -1 +1 @@
81f2defd3d1d369ba4b9d010480be85b351b188d467bf127783e2c921f1b535d
\ No newline at end of file
0c35dd41fa06540b3bb995ad331ffc66906deb329dc6e1665a2d0df3bd05215a
\ No newline at end of file
harness/shots/baseline/darwin/home-phone-light.sha256+1−1
@@ -1 +1 @@
81f2defd3d1d369ba4b9d010480be85b351b188d467bf127783e2c921f1b535d
\ No newline at end of file
0c35dd41fa06540b3bb995ad331ffc66906deb329dc6e1665a2d0df3bd05215a
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-dark.sha256+1−1
@@ -1 +1 @@
bfa31d84c5e1f0db09d78a7d133a6c5ae7b2dd5546e8b0f359aec4455aa31794
\ No newline at end of file
540ef71f2d8e4cdb719207c67705dd93844e005eb7e2379c877cc5e782d0e3bb
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-light.sha256+1−1
@@ -1 +1 @@
bfa31d84c5e1f0db09d78a7d133a6c5ae7b2dd5546e8b0f359aec4455aa31794
\ No newline at end of file
540ef71f2d8e4cdb719207c67705dd93844e005eb7e2379c877cc5e782d0e3bb
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-dark.sha256+1−1
@@ -1 +1 @@
bfa31d84c5e1f0db09d78a7d133a6c5ae7b2dd5546e8b0f359aec4455aa31794
\ No newline at end of file
540ef71f2d8e4cdb719207c67705dd93844e005eb7e2379c877cc5e782d0e3bb
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-light.sha256+1−1
@@ -1 +1 @@
bfa31d84c5e1f0db09d78a7d133a6c5ae7b2dd5546e8b0f359aec4455aa31794
\ No newline at end of file
540ef71f2d8e4cdb719207c67705dd93844e005eb7e2379c877cc5e782d0e3bb
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
c4980838942c9c06cba8382387d414588a99919caaf5089e8388ed5018ea5b8f
\ No newline at end of file
266f1be2c7e361d746bd53c06d25198e1981c4c9049bee91cbe7f35f9a279144
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
c4980838942c9c06cba8382387d414588a99919caaf5089e8388ed5018ea5b8f
\ No newline at end of file
266f1be2c7e361d746bd53c06d25198e1981c4c9049bee91cbe7f35f9a279144
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
c4980838942c9c06cba8382387d414588a99919caaf5089e8388ed5018ea5b8f
\ No newline at end of file
266f1be2c7e361d746bd53c06d25198e1981c4c9049bee91cbe7f35f9a279144
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-light.sha256+1−1
@@ -1 +1 @@
c4980838942c9c06cba8382387d414588a99919caaf5089e8388ed5018ea5b8f
\ No newline at end of file
266f1be2c7e361d746bd53c06d25198e1981c4c9049bee91cbe7f35f9a279144
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-dark.sha256+1−1
@@ -1 +1 @@
2cf2e1356a46e2e3f1b7768c2de2977d8a499a92e214647383e98d508f85d4a5
\ No newline at end of file
ffc55fd7ddca55861317699953ac8d847af9ff75e1691f36fa7e9ec58d4b6a79
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-light.sha256+1−1
@@ -1 +1 @@
2cf2e1356a46e2e3f1b7768c2de2977d8a499a92e214647383e98d508f85d4a5
\ No newline at end of file
ffc55fd7ddca55861317699953ac8d847af9ff75e1691f36fa7e9ec58d4b6a79
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-dark.sha256+1−1
@@ -1 +1 @@
2cf2e1356a46e2e3f1b7768c2de2977d8a499a92e214647383e98d508f85d4a5
\ No newline at end of file
ffc55fd7ddca55861317699953ac8d847af9ff75e1691f36fa7e9ec58d4b6a79
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-light.sha256+1−1
@@ -1 +1 @@
2cf2e1356a46e2e3f1b7768c2de2977d8a499a92e214647383e98d508f85d4a5
\ No newline at end of file
ffc55fd7ddca55861317699953ac8d847af9ff75e1691f36fa7e9ec58d4b6a79
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
be8df56ce2fcf453d1f8b55cbb8b7d4e5f58845480016108f3322fdec10714ca
\ No newline at end of file
7e423c5bf140a962bd0fc7fd5484238eca88b61fd24f6e27492e9090b1641c4a
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
be8df56ce2fcf453d1f8b55cbb8b7d4e5f58845480016108f3322fdec10714ca
\ No newline at end of file
7e423c5bf140a962bd0fc7fd5484238eca88b61fd24f6e27492e9090b1641c4a
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
be8df56ce2fcf453d1f8b55cbb8b7d4e5f58845480016108f3322fdec10714ca
\ No newline at end of file
7e423c5bf140a962bd0fc7fd5484238eca88b61fd24f6e27492e9090b1641c4a
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-light.sha256+1−1
@@ -1 +1 @@
be8df56ce2fcf453d1f8b55cbb8b7d4e5f58845480016108f3322fdec10714ca
\ No newline at end of file
7e423c5bf140a962bd0fc7fd5484238eca88b61fd24f6e27492e9090b1641c4a
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-dark.sha256+1−1
@@ -1 +1 @@
743947e8ebf32fb3b174f4330242d180f6ee2381bff8acf72b7ab6ece39b6b5b
\ No newline at end of file
3c48a96bf539899e8c3f296ac56d42feba22eb9fb861d285af2195add73fd822
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-light.sha256+1−1
@@ -1 +1 @@
743947e8ebf32fb3b174f4330242d180f6ee2381bff8acf72b7ab6ece39b6b5b
\ No newline at end of file
3c48a96bf539899e8c3f296ac56d42feba22eb9fb861d285af2195add73fd822
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-dark.sha256+1−1
@@ -1 +1 @@
743947e8ebf32fb3b174f4330242d180f6ee2381bff8acf72b7ab6ece39b6b5b
\ No newline at end of file
3c48a96bf539899e8c3f296ac56d42feba22eb9fb861d285af2195add73fd822
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-light.sha256+1−1
@@ -1 +1 @@
743947e8ebf32fb3b174f4330242d180f6ee2381bff8acf72b7ab6ece39b6b5b
\ No newline at end of file
3c48a96bf539899e8c3f296ac56d42feba22eb9fb861d285af2195add73fd822
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-dark.sha256+1−1
@@ -1 +1 @@
b6ab77b3fbb7d46ab5de8d6e90a731f09c75184950441c3b851b085a31761fbc
\ No newline at end of file
89d5305d05bb328d4d5d77d45ca21f35c6a6f1f231bc6e2c13f5ce2565f2831e
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-light.sha256+1−1
@@ -1 +1 @@
b6ab77b3fbb7d46ab5de8d6e90a731f09c75184950441c3b851b085a31761fbc
\ No newline at end of file
89d5305d05bb328d4d5d77d45ca21f35c6a6f1f231bc6e2c13f5ce2565f2831e
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-dark.sha256+1−1
@@ -1 +1 @@
b6ab77b3fbb7d46ab5de8d6e90a731f09c75184950441c3b851b085a31761fbc
\ No newline at end of file
89d5305d05bb328d4d5d77d45ca21f35c6a6f1f231bc6e2c13f5ce2565f2831e
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-light.sha256+1−1
@@ -1 +1 @@
b6ab77b3fbb7d46ab5de8d6e90a731f09c75184950441c3b851b085a31761fbc
\ No newline at end of file
89d5305d05bb328d4d5d77d45ca21f35c6a6f1f231bc6e2c13f5ce2565f2831e
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-dark.sha256+1−1
@@ -1 +1 @@
cc05725c4b4c6f2c636984a48e167c9ae026974b2d429f1da4ba7387c8d6472a
\ No newline at end of file
b94a493d9a26d4e9c78b9c8af6c3bb3faf36494faee70a18994d007c7409d026
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-light.sha256+1−1
@@ -1 +1 @@
cc05725c4b4c6f2c636984a48e167c9ae026974b2d429f1da4ba7387c8d6472a
\ No newline at end of file
b94a493d9a26d4e9c78b9c8af6c3bb3faf36494faee70a18994d007c7409d026
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-dark.sha256+1−1
@@ -1 +1 @@
cc05725c4b4c6f2c636984a48e167c9ae026974b2d429f1da4ba7387c8d6472a
\ No newline at end of file
b94a493d9a26d4e9c78b9c8af6c3bb3faf36494faee70a18994d007c7409d026
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-light.sha256+1−1
@@ -1 +1 @@
cc05725c4b4c6f2c636984a48e167c9ae026974b2d429f1da4ba7387c8d6472a
\ No newline at end of file
b94a493d9a26d4e9c78b9c8af6c3bb3faf36494faee70a18994d007c7409d026
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-dark.sha256+1−1
@@ -1 +1 @@
c0761a48e149c127f5fa11b7c6a9e5364b68906320ecabd2d239f90a342484a0
\ No newline at end of file
38cc53314d8da534ef8e5ad4d8a316ae53f7ce2006405e23e00f2d83a6c56a88
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-light.sha256+1−1
@@ -1 +1 @@
c0761a48e149c127f5fa11b7c6a9e5364b68906320ecabd2d239f90a342484a0
\ No newline at end of file
38cc53314d8da534ef8e5ad4d8a316ae53f7ce2006405e23e00f2d83a6c56a88
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
0b985d5463606ab499740b541c0161571601bcc788647c0a58b3e0a2cad10d42
\ No newline at end of file
0fb9caf11bc940d61cfd19173171b589029620d976ca6ae41c24584503e1bab2
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-light.sha256+1−1
@@ -1 +1 @@
0b985d5463606ab499740b541c0161571601bcc788647c0a58b3e0a2cad10d42
\ No newline at end of file
0fb9caf11bc940d61cfd19173171b589029620d976ca6ae41c24584503e1bab2
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-dark.sha256+1−1
@@ -1 +1 @@
0b985d5463606ab499740b541c0161571601bcc788647c0a58b3e0a2cad10d42
\ No newline at end of file
0fb9caf11bc940d61cfd19173171b589029620d976ca6ae41c24584503e1bab2
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-light.sha256+1−1
@@ -1 +1 @@
0b985d5463606ab499740b541c0161571601bcc788647c0a58b3e0a2cad10d42
\ No newline at end of file
0fb9caf11bc940d61cfd19173171b589029620d976ca6ae41c24584503e1bab2
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-dark.sha256+1−1
@@ -1 +1 @@
c0761a48e149c127f5fa11b7c6a9e5364b68906320ecabd2d239f90a342484a0
\ No newline at end of file
38cc53314d8da534ef8e5ad4d8a316ae53f7ce2006405e23e00f2d83a6c56a88
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-light.sha256+1−1
@@ -1 +1 @@
c0761a48e149c127f5fa11b7c6a9e5364b68906320ecabd2d239f90a342484a0
\ No newline at end of file
38cc53314d8da534ef8e5ad4d8a316ae53f7ce2006405e23e00f2d83a6c56a88
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-dark.sha256+1−1
@@ -1 +1 @@
0292f8464f05e73aa1a45729bba0c92ed3559755e5b61ecd161cef77360e6de2
\ No newline at end of file
f42db2de5001567f90e7a76cd230ad3c57b0def3a411d9eb82edded846a2b905
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-light.sha256+1−1
@@ -1 +1 @@
0292f8464f05e73aa1a45729bba0c92ed3559755e5b61ecd161cef77360e6de2
\ No newline at end of file
f42db2de5001567f90e7a76cd230ad3c57b0def3a411d9eb82edded846a2b905
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-dark.sha256+1−1
@@ -1 +1 @@
8b08f39ac139531ee3bd6f1b0e4902a76878a7f8242fb8b6bd2dcb50e083307b
\ No newline at end of file
4496dbce8d0124698b6ea02e8e26b3a36c5a34123ac540430dd7fb56f03a7587
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-light.sha256+1−1
@@ -1 +1 @@
8b08f39ac139531ee3bd6f1b0e4902a76878a7f8242fb8b6bd2dcb50e083307b
\ No newline at end of file
4496dbce8d0124698b6ea02e8e26b3a36c5a34123ac540430dd7fb56f03a7587
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-dark.sha256+1−1
@@ -1 +1 @@
8b08f39ac139531ee3bd6f1b0e4902a76878a7f8242fb8b6bd2dcb50e083307b
\ No newline at end of file
4496dbce8d0124698b6ea02e8e26b3a36c5a34123ac540430dd7fb56f03a7587
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-light.sha256+1−1
@@ -1 +1 @@
8b08f39ac139531ee3bd6f1b0e4902a76878a7f8242fb8b6bd2dcb50e083307b
\ No newline at end of file
4496dbce8d0124698b6ea02e8e26b3a36c5a34123ac540430dd7fb56f03a7587
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-dark.sha256+1−1
@@ -1 +1 @@
0292f8464f05e73aa1a45729bba0c92ed3559755e5b61ecd161cef77360e6de2
\ No newline at end of file
f42db2de5001567f90e7a76cd230ad3c57b0def3a411d9eb82edded846a2b905
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-light.sha256+1−1
@@ -1 +1 @@
0292f8464f05e73aa1a45729bba0c92ed3559755e5b61ecd161cef77360e6de2
\ No newline at end of file
f42db2de5001567f90e7a76cd230ad3c57b0def3a411d9eb82edded846a2b905
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
3be90c357aa50fa9883b29bb497601758c35c0cbc9436af50ddf8bc162bde7ca
\ No newline at end of file
594d0c215cfbdb2ce0bb3bb15e46994e4f1a907bf2cf69fd0c7c58fee4bd6224
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
3be90c357aa50fa9883b29bb497601758c35c0cbc9436af50ddf8bc162bde7ca
\ No newline at end of file
594d0c215cfbdb2ce0bb3bb15e46994e4f1a907bf2cf69fd0c7c58fee4bd6224
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
3be90c357aa50fa9883b29bb497601758c35c0cbc9436af50ddf8bc162bde7ca
\ No newline at end of file
594d0c215cfbdb2ce0bb3bb15e46994e4f1a907bf2cf69fd0c7c58fee4bd6224
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-light.sha256+1−1
@@ -1 +1 @@
3be90c357aa50fa9883b29bb497601758c35c0cbc9436af50ddf8bc162bde7ca
\ No newline at end of file
594d0c215cfbdb2ce0bb3bb15e46994e4f1a907bf2cf69fd0c7c58fee4bd6224
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-dark.sha256+1−1
@@ -1 +1 @@
af1b06ab2147c2453ce04d7bb149f50d0a2a752a2b11e624f2da802e81438b33
\ No newline at end of file
4baffd2ec650a2337a64a8cd2d3e78f8f864df49550691eb3b4f29168e569830
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-light.sha256+1−1
@@ -1 +1 @@
af1b06ab2147c2453ce04d7bb149f50d0a2a752a2b11e624f2da802e81438b33
\ No newline at end of file
4baffd2ec650a2337a64a8cd2d3e78f8f864df49550691eb3b4f29168e569830
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-dark.sha256+1−1
@@ -1 +1 @@
af1b06ab2147c2453ce04d7bb149f50d0a2a752a2b11e624f2da802e81438b33
\ No newline at end of file
4baffd2ec650a2337a64a8cd2d3e78f8f864df49550691eb3b4f29168e569830
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-light.sha256+1−1
@@ -1 +1 @@
af1b06ab2147c2453ce04d7bb149f50d0a2a752a2b11e624f2da802e81438b33
\ No newline at end of file
4baffd2ec650a2337a64a8cd2d3e78f8f864df49550691eb3b4f29168e569830
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-dark.sha256+1−1
@@ -1 +1 @@
b058bc48e648af1a58c4ecdb576ff70cfc3d24a7a14b930d827d3c7c5087eb3e
\ No newline at end of file
d4315598544786e854e69f917f851f650eec82dcc4ef6545f30e2393790277c0
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-light.sha256+1−1
@@ -1 +1 @@
b058bc48e648af1a58c4ecdb576ff70cfc3d24a7a14b930d827d3c7c5087eb3e
\ No newline at end of file
d4315598544786e854e69f917f851f650eec82dcc4ef6545f30e2393790277c0
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-dark.sha256+1−1
@@ -1 +1 @@
b058bc48e648af1a58c4ecdb576ff70cfc3d24a7a14b930d827d3c7c5087eb3e
\ No newline at end of file
d4315598544786e854e69f917f851f650eec82dcc4ef6545f30e2393790277c0
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-light.sha256+1−1
@@ -1 +1 @@
b058bc48e648af1a58c4ecdb576ff70cfc3d24a7a14b930d827d3c7c5087eb3e
\ No newline at end of file
d4315598544786e854e69f917f851f650eec82dcc4ef6545f30e2393790277c0
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-dark.sha256+1−1
@@ -1 +1 @@
518ddae04caeb665e46b4c19ff0ac8d7895202d1f92702e3a8f6f65b2ae83ed6
\ No newline at end of file
17f414256569c617a3bd80aa0f8c53962bd7d3ebe0bfb620755a5cc03e12f871
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-light.sha256+1−1
@@ -1 +1 @@
518ddae04caeb665e46b4c19ff0ac8d7895202d1f92702e3a8f6f65b2ae83ed6
\ No newline at end of file
17f414256569c617a3bd80aa0f8c53962bd7d3ebe0bfb620755a5cc03e12f871
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-dark.sha256+1−1
@@ -1 +1 @@
518ddae04caeb665e46b4c19ff0ac8d7895202d1f92702e3a8f6f65b2ae83ed6
\ No newline at end of file
17f414256569c617a3bd80aa0f8c53962bd7d3ebe0bfb620755a5cc03e12f871
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-light.sha256+1−1
@@ -1 +1 @@
518ddae04caeb665e46b4c19ff0ac8d7895202d1f92702e3a8f6f65b2ae83ed6
\ No newline at end of file
17f414256569c617a3bd80aa0f8c53962bd7d3ebe0bfb620755a5cc03e12f871
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-dark.sha256+1−1
@@ -1 +1 @@
7f6d6bc4fc4ed315b7f7fad13181bd0adec610bb4eb0bb00c1f667bca0039c2e
\ No newline at end of file
f318ce8d6bdb789fc8fe8364e77dd869fc9cde18e2ab1d37f833583acad5d899
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-light.sha256+1−1
@@ -1 +1 @@
7f6d6bc4fc4ed315b7f7fad13181bd0adec610bb4eb0bb00c1f667bca0039c2e
\ No newline at end of file
f318ce8d6bdb789fc8fe8364e77dd869fc9cde18e2ab1d37f833583acad5d899
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-dark.sha256+1−1
@@ -1 +1 @@
7f6d6bc4fc4ed315b7f7fad13181bd0adec610bb4eb0bb00c1f667bca0039c2e
\ No newline at end of file
f318ce8d6bdb789fc8fe8364e77dd869fc9cde18e2ab1d37f833583acad5d899
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-light.sha256+1−1
@@ -1 +1 @@
7f6d6bc4fc4ed315b7f7fad13181bd0adec610bb4eb0bb00c1f667bca0039c2e
\ No newline at end of file
f318ce8d6bdb789fc8fe8364e77dd869fc9cde18e2ab1d37f833583acad5d899
\ No newline at end of file
harness/shots/fixtures.json+20−20
@@ -31,7 +31,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"did\":\"did:plc:3ygt44h3csutc5c7xfpkp45l\",\"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\",\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"name\":\"notes\",\"description\":\"Scratch notes 📝\",\"forkOf\":null,\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"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:3ygt44h3csutc5c7xfpkp45l\",\"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,
@@ -39,7 +39,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"head\":\"main\",\"pulls\":0,\"issues\":0,\"about\":\"A git repository in alice.test's atmosphere account.\",\"card\":\"/alice.test/scratch/card.png?v=93262b63e081657c\"},\"rev\":\"main\",\"path\":\"\",\"empty\":true,\"last\":null,\"entries\":[],\"readme\":null}"
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"head\":\"main\",\"pulls\":0,\"issues\":0,\"about\":\"A git repository in alice.test's atmosphere account.\",\"card\":\"/alice.test/scratch/card.png?v=93262b63e081657c\"},\"rev\":\"main\",\"path\":\"\",\"empty\":true,\"last\":null,\"entries\":[],\"readme\":null}"
},
"/_data/alice.test/tally": {
"status": 200,
@@ -47,7 +47,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\":\"Bob Tanaka\",\"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\":\"Bob Tanaka\",\"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,
@@ -55,7 +55,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\":\"README.md\",\"shown\":\"text\",\"size\":388,\"lines\":27,\"text\":\"# tally\\n\\nCounts lines, words and bytes, like `wc`, with output that lines up.\\n\\n![tally](assets/logo.svg)\\n\\n## Usage\\n\\n```sh\\ncargo install --path .\\ntally notes.txt\\necho \\\"one two\\\" | tally\\n```\\n\\n## Output\\n\\n| Column | Meaning |\\n| --- | --- |\\n| 1 | lines |\\n| 2 | words, split on whitespace |\\n| 3 | bytes |\\n\\nSee [the design notes](docs/design.md) for why it reads the whole file.\\n\\n## License\\n\\nMIT\\n\",\"ranges\":\"\"}"
"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\":\"README.md\",\"shown\":\"text\",\"size\":388,\"lines\":27,\"text\":\"# tally\\n\\nCounts lines, words and bytes, like `wc`, with output that lines up.\\n\\n![tally](assets/logo.svg)\\n\\n## Usage\\n\\n```sh\\ncargo install --path .\\ntally notes.txt\\necho \\\"one two\\\" | tally\\n```\\n\\n## Output\\n\\n| Column | Meaning |\\n| --- | --- |\\n| 1 | lines |\\n| 2 | words, split on whitespace |\\n| 3 | bytes |\\n\\nSee [the design notes](docs/design.md) for why it reads the whole file.\\n\\n## License\\n\\nMIT\\n\",\"ranges\":\"\"}"
},
"/_data/alice.test/tally/blob/main/assets/logo.svg": {
"status": 200,
@@ -63,7 +63,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\":\"assets/logo.svg\",\"shown\":\"image\",\"size\":282,\"lines\":1,\"text\":null,\"ranges\":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\":\"assets/logo.svg\",\"shown\":\"image\",\"size\":282,\"lines\":1,\"text\":null,\"ranges\":null}"
},
"/_data/alice.test/tally/blob/main/nowhere": {
"status": 404,
@@ -79,7 +79,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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/lib.rs\",\"shown\":\"text\",\"size\":783,\"lines\":37,\"text\":\"//! Counting, kept apart from reading so it can be tested.\\n\\nmod parse;\\n\\n/// What a text holds.\\n#[derive(Debug, Default, PartialEq)]\\npub struct Counts {\\n pub lines: usize,\\n pub words: usize,\\n pub bytes: usize,\\n}\\n\\n/// Counts `text` in one pass.\\npub fn count(text: &str) -> Counts {\\n let mut counts = Counts { bytes: text.len(), ..Counts::default() };\\n for line in parse::lines(text) {\\n counts.lines += 1;\\n counts.words += parse::words(line);\\n }\\n counts\\n}\\n\\n#[cfg(test)]\\nmod tests {\\n use super::*;\\n\\n #[test]\\n fn counts() {\\n let c = count(\\\"one two\\\\nthree\\\\n\\\");\\n assert_eq!(c, Counts { lines: 2, words: 3, bytes: 14 });\\n }\\n\\n #[test]\\n fn a_last_line_without_a_newline() {\\n assert_eq!(count(\\\"a\\\\nb\\\").lines, 2);\\n }\\n}\\n\",\"ranges\":\"comment:0,59,13,23,126,31;string:585,18,153,6;keyword:60,3,69,3,1,6,14,3,19,3,19,3,49,3,1,2,35,3,1,3,65,3,6,2,126,3,13,3,1,5,22,2,20,3,120,2;number:422,1,221,1,9,1,9,2,105,1;function:259,5,67,3,14,7,29,5,67,5,91,6,21,5,30,10,73,29,13,10;type:104,5,2,7,2,9,14,6,18,5,17,5,17,5,56,3,5,6,24,6,24,6,282,6;attr:95,9,5,2,7,2,9,2,358,12,36,7,138,7\"}"
"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/lib.rs\",\"shown\":\"text\",\"size\":783,\"lines\":37,\"text\":\"//! Counting, kept apart from reading so it can be tested.\\n\\nmod parse;\\n\\n/// What a text holds.\\n#[derive(Debug, Default, PartialEq)]\\npub struct Counts {\\n pub lines: usize,\\n pub words: usize,\\n pub bytes: usize,\\n}\\n\\n/// Counts `text` in one pass.\\npub fn count(text: &str) -> Counts {\\n let mut counts = Counts { bytes: text.len(), ..Counts::default() };\\n for line in parse::lines(text) {\\n counts.lines += 1;\\n counts.words += parse::words(line);\\n }\\n counts\\n}\\n\\n#[cfg(test)]\\nmod tests {\\n use super::*;\\n\\n #[test]\\n fn counts() {\\n let c = count(\\\"one two\\\\nthree\\\\n\\\");\\n assert_eq!(c, Counts { lines: 2, words: 3, bytes: 14 });\\n }\\n\\n #[test]\\n fn a_last_line_without_a_newline() {\\n assert_eq!(count(\\\"a\\\\nb\\\").lines, 2);\\n }\\n}\\n\",\"ranges\":\"comment:0,59,13,23,126,31;string:585,18,153,6;keyword:60,3,69,3,1,6,14,3,19,3,19,3,49,3,1,2,35,3,1,3,65,3,6,2,126,3,13,3,1,5,22,2,20,3,120,2;number:422,1,221,1,9,1,9,2,105,1;function:259,5,67,3,14,7,29,5,67,5,91,6,21,5,30,10,73,29,13,10;type:104,5,2,7,2,9,14,6,18,5,17,5,17,5,56,3,5,6,24,6,24,6,282,6;attr:95,9,5,2,7,2,9,2,358,12,36,7,138,7\"}"
},
"/_data/alice.test/tally/commit/97727c2c19d5d8b3849b2ed9ad47130cee9798f5": {
"status": 200,
@@ -87,7 +87,7 @@
"content-type": "application/json",
"cache-control": "public, max-age=31536000, immutable"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\"},\"commit\":{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},\"files\":[{\"old\":\"docs/design.md\",\"new\":\"docs/DESIGN.md\",\"status\":\"renamed\",\"added\":0,\"removed\":0,\"note\":\"Moved without changes.\",\"ranges\":null,\"rows\":[]},{\"old\":\"src/lib.rs\",\"new\":\"src/lib.rs\",\"status\":\"modified\",\"added\":9,\"removed\":2,\"note\":null,\"ranges\":\"comment:0,58,14,22,58,30;string:529,18,153,6;keyword:60,3,69,3,1,6,41,3,38,3,1,3,65,3,6,2,20,3,6,2,177,3,120,2;number:384,1,203,1,9,1,9,2,105,1;function:262,3,14,7,27,5,28,5,65,16,3,5,35,5,43,5,30,10,73,29,13,10;type:104,5,2,7,2,9,14,6,54,3,5,6,24,6,24,6,295,6;attr:95,9,5,2,7,2,9,2,495,7;xins:339,17,112,17;xdel:308,11,92,30\",\"rows\":[{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -1,5 +1,7 @@\"},{\"kind\":\"\",\"line\":true,\"old\":1,\"new\":1,\"text\":\"//! Counting, kept apart from reading so it can be tested.\"},{\"kind\":\"\",\"line\":true,\"old\":2,\"new\":2,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":3,\"text\":\"mod parse;\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":4,\"text\":\"\"},{\"kind\":\"\",\"line\":true,\"old\":3,\"new\":5,\"text\":\"/// What a text holds.\"},{\"kind\":\"\",\"line\":true,\"old\":4,\"new\":6,\"text\":\"#[derive(Debug, Default, PartialEq)]\"},{\"kind\":\"\",\"line\":true,\"old\":5,\"new\":7,\"text\":\"pub struct Counts {\"},{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -11,9 +13,9 @@ pub struct Counts {\"},{\"kind\":\"\",\"line\":true,\"old\":11,\"new\":13,\"text\":\"/// Counts `text` in one pass.\"},{\"kind\":\"\",\"line\":true,\"old\":12,\"new\":14,\"text\":\"pub fn count(text: &str) -> Counts {\"},{\"kind\":\"\",\"line\":true,\"old\":13,\"new\":15,\"text\":\" let mut counts = Counts { bytes: text.len(), ..Counts::default() };\"},{\"kind\":\"d\",\"line\":true,\"old\":14,\"new\":null,\"text\":\" for line in text.lines() {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":16,\"text\":\" for line in parse::lines(text) {\"},{\"kind\":\"\",\"line\":true,\"old\":15,\"new\":17,\"text\":\" counts.lines += 1;\"},{\"kind\":\"d\",\"line\":true,\"old\":16,\"new\":null,\"text\":\" counts.words += line.split_whitespace().count();\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":18,\"text\":\" counts.words += parse::words(line);\"},{\"kind\":\"\",\"line\":true,\"old\":17,\"new\":19,\"text\":\" }\"},{\"kind\":\"\",\"line\":true,\"old\":18,\"new\":20,\"text\":\" counts\"},{\"kind\":\"\",\"line\":true,\"old\":19,\"new\":21,\"text\":\"}\"},{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -27,4 +29,9 @@ mod tests {\"},{\"kind\":\"\",\"line\":true,\"old\":27,\"new\":29,\"text\":\" let c = count(\\\"one two\\\\nthree\\\\n\\\");\"},{\"kind\":\"\",\"line\":true,\"old\":28,\"new\":30,\"text\":\" assert_eq!(c, Counts { lines: 2, words: 3, bytes: 14 });\"},{\"kind\":\"\",\"line\":true,\"old\":29,\"new\":31,\"text\":\" }\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":32,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":33,\"text\":\" #[test]\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":34,\"text\":\" fn a_last_line_without_a_newline() {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":35,\"text\":\" assert_eq!(count(\\\"a\\\\nb\\\").lines, 2);\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":36,\"text\":\" }\"},{\"kind\":\"\",\"line\":true,\"old\":30,\"new\":37,\"text\":\"}\"}]},{\"old\":\"src/parse.rs\",\"new\":\"src/parse.rs\",\"status\":\"added\",\"added\":11,\"removed\":0,\"note\":null,\"ranges\":\"comment:0,48,2,57,76,68;keyword:108,3,1,2,138,3,1,2,22,4;function:115,5,33,16,3,5,82,5,54,5;type:128,3,5,5,131,3,10,8,1,4,4,3\",\"rows\":[{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -0,0 +1,11 @@\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":1,\"text\":\"//! Splitting text into the things tally counts.\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":2,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":3,\"text\":\"/// The words of `line`: runs of anything but whitespace.\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":4,\"text\":\"pub fn words(line: &str) -> usize {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":5,\"text\":\" line.split_whitespace().count()\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":6,\"text\":\"}\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":7,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":8,\"text\":\"/// The lines of `text`. A last line without a newline still counts.\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":9,\"text\":\"pub fn lines(text: &str) -> impl Iterator<Item = &str> {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":10,\"text\":\" text.lines()\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":11,\"text\":\"}\"}]}]}"
"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\"},\"commit\":{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},\"files\":[{\"old\":\"docs/design.md\",\"new\":\"docs/DESIGN.md\",\"status\":\"renamed\",\"added\":0,\"removed\":0,\"note\":\"Moved without changes.\",\"ranges\":null,\"rows\":[]},{\"old\":\"src/lib.rs\",\"new\":\"src/lib.rs\",\"status\":\"modified\",\"added\":9,\"removed\":2,\"note\":null,\"ranges\":\"comment:0,58,14,22,58,30;string:529,18,153,6;keyword:60,3,69,3,1,6,41,3,38,3,1,3,65,3,6,2,20,3,6,2,177,3,120,2;number:384,1,203,1,9,1,9,2,105,1;function:262,3,14,7,27,5,28,5,65,16,3,5,35,5,43,5,30,10,73,29,13,10;type:104,5,2,7,2,9,14,6,54,3,5,6,24,6,24,6,295,6;attr:95,9,5,2,7,2,9,2,495,7;xins:339,17,112,17;xdel:308,11,92,30\",\"rows\":[{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -1,5 +1,7 @@\"},{\"kind\":\"\",\"line\":true,\"old\":1,\"new\":1,\"text\":\"//! Counting, kept apart from reading so it can be tested.\"},{\"kind\":\"\",\"line\":true,\"old\":2,\"new\":2,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":3,\"text\":\"mod parse;\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":4,\"text\":\"\"},{\"kind\":\"\",\"line\":true,\"old\":3,\"new\":5,\"text\":\"/// What a text holds.\"},{\"kind\":\"\",\"line\":true,\"old\":4,\"new\":6,\"text\":\"#[derive(Debug, Default, PartialEq)]\"},{\"kind\":\"\",\"line\":true,\"old\":5,\"new\":7,\"text\":\"pub struct Counts {\"},{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -11,9 +13,9 @@ pub struct Counts {\"},{\"kind\":\"\",\"line\":true,\"old\":11,\"new\":13,\"text\":\"/// Counts `text` in one pass.\"},{\"kind\":\"\",\"line\":true,\"old\":12,\"new\":14,\"text\":\"pub fn count(text: &str) -> Counts {\"},{\"kind\":\"\",\"line\":true,\"old\":13,\"new\":15,\"text\":\" let mut counts = Counts { bytes: text.len(), ..Counts::default() };\"},{\"kind\":\"d\",\"line\":true,\"old\":14,\"new\":null,\"text\":\" for line in text.lines() {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":16,\"text\":\" for line in parse::lines(text) {\"},{\"kind\":\"\",\"line\":true,\"old\":15,\"new\":17,\"text\":\" counts.lines += 1;\"},{\"kind\":\"d\",\"line\":true,\"old\":16,\"new\":null,\"text\":\" counts.words += line.split_whitespace().count();\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":18,\"text\":\" counts.words += parse::words(line);\"},{\"kind\":\"\",\"line\":true,\"old\":17,\"new\":19,\"text\":\" }\"},{\"kind\":\"\",\"line\":true,\"old\":18,\"new\":20,\"text\":\" counts\"},{\"kind\":\"\",\"line\":true,\"old\":19,\"new\":21,\"text\":\"}\"},{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -27,4 +29,9 @@ mod tests {\"},{\"kind\":\"\",\"line\":true,\"old\":27,\"new\":29,\"text\":\" let c = count(\\\"one two\\\\nthree\\\\n\\\");\"},{\"kind\":\"\",\"line\":true,\"old\":28,\"new\":30,\"text\":\" assert_eq!(c, Counts { lines: 2, words: 3, bytes: 14 });\"},{\"kind\":\"\",\"line\":true,\"old\":29,\"new\":31,\"text\":\" }\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":32,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":33,\"text\":\" #[test]\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":34,\"text\":\" fn a_last_line_without_a_newline() {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":35,\"text\":\" assert_eq!(count(\\\"a\\\\nb\\\").lines, 2);\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":36,\"text\":\" }\"},{\"kind\":\"\",\"line\":true,\"old\":30,\"new\":37,\"text\":\"}\"}]},{\"old\":\"src/parse.rs\",\"new\":\"src/parse.rs\",\"status\":\"added\",\"added\":11,\"removed\":0,\"note\":null,\"ranges\":\"comment:0,48,2,57,76,68;keyword:108,3,1,2,138,3,1,2,22,4;function:115,5,33,16,3,5,82,5,54,5;type:128,3,5,5,131,3,10,8,1,4,4,3\",\"rows\":[{\"kind\":\"h\",\"line\":false,\"old\":null,\"new\":null,\"text\":\"@@ -0,0 +1,11 @@\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":1,\"text\":\"//! Splitting text into the things tally counts.\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":2,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":3,\"text\":\"/// The words of `line`: runs of anything but whitespace.\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":4,\"text\":\"pub fn words(line: &str) -> usize {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":5,\"text\":\" line.split_whitespace().count()\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":6,\"text\":\"}\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":7,\"text\":\"\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":8,\"text\":\"/// The lines of `text`. A last line without a newline still counts.\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":9,\"text\":\"pub fn lines(text: &str) -> impl Iterator<Item = &str> {\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":10,\"text\":\" text.lines()\"},{\"kind\":\"a\",\"line\":true,\"old\":null,\"new\":11,\"text\":\"}\"}]}]}"
},
"/_data/alice.test/tally/commits/main": {
"status": 200,
@@ -95,7 +95,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\":\"Bob Tanaka\",\"date\":\"2026-10-09T09:31:00+00:00\",\"message\":\"Count no lines in empty input\"},{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},{\"sha\":\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\",\"parents\":[\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-06T10:02:00+00:00\",\"message\":\"Document usage and output\"},{\"sha\":\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\",\"parents\":[\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-05T17:21:00+00:00\",\"message\":\"Add a logo\"},{\"sha\":\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\",\"parents\":[\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\"],\"author\":\"Alice Moreau\",\"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\":\"Alice Moreau\",\"date\":\"2026-10-02T14:40:00+00:00\",\"message\":\"Count words, lines and bytes\"},{\"sha\":\"cac02c464843db4d55fecfa69d327ef7b12702c0\",\"parents\":[],\"author\":\"Alice Moreau\",\"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\":\"Bob Tanaka\",\"date\":\"2026-10-09T09:31:00+00:00\",\"message\":\"Count no lines in empty input\"},{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},{\"sha\":\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\",\"parents\":[\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-06T10:02:00+00:00\",\"message\":\"Document usage and output\"},{\"sha\":\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\",\"parents\":[\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-05T17:21:00+00:00\",\"message\":\"Add a logo\"},{\"sha\":\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\",\"parents\":[\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\"],\"author\":\"Alice Moreau\",\"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\":\"Alice Moreau\",\"date\":\"2026-10-02T14:40:00+00:00\",\"message\":\"Count words, lines and bytes\"},{\"sha\":\"cac02c464843db4d55fecfa69d327ef7b12702c0\",\"parents\":[],\"author\":\"Alice Moreau\",\"date\":\"2026-10-01T09:12:00+00:00\",\"message\":\"Start tally\"}],\"more\":false}"
},
"/_data/alice.test/tally/issue/did:plc:22yzhoyrfxuuax3knxdw3t72/3myg7luqyfmyu": {
"status": 200,
@@ -103,7 +103,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\",\"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\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7luqyfmyu/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:22yzhoyrfxuuax3knxdw3t72/3myg7luqyfmyu/card.png?v=160a3e2db8db2127\"}"
},
"/_data/alice.test/tally/issue/did:plc:22yzhoyrfxuuax3knxdw3t72/3myg7lusdxeyx": {
"status": 200,
@@ -111,7 +111,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7lusdxeyx/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:22yzhoyrfxuuax3knxdw3t72/3myg7lusdxeyx/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\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7luqyfmyu\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7luqyfmyu\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T23:34:00.000Z\",\"comments\":1}]}"
},
"/_data/alice.test/tally/issues?state=closed": {
"status": 200,
@@ -127,7 +127,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7lusdxeyx\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7lusdxeyx\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"comments\":0}]}"
},
"/_data/alice.test/tally/pull/did:plc:22yzhoyrfxuuax3knxdw3t72/3myg7ltvdaqlc": {
"status": 200,
@@ -135,7 +135,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\",\"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\",\"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\",\"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\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7ltvdaqlc/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:22yzhoyrfxuuax3knxdw3t72/3myg7ltvdaqlc/card.png?v=7f555a00efc7106b\"}"
},
"/_data/alice.test/tally/pull/did:plc:22yzhoyrfxuuax3knxdw3t72/3myg7luqbsoys": {
"status": 200,
@@ -143,7 +143,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"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\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7luqbsoys/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:22yzhoyrfxuuax3knxdw3t72/3myg7luqbsoys/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\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7luqbsoys\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7luqbsoys\",\"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\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7ltvdaqlc\",\"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:22yzhoyrfxuuax3knxdw3t72/3myg7ltvdaqlc\",\"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\",\"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\":\"Bob Tanaka\",\"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\":\"Bob Tanaka\",\"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:22yzhoyrfxuuax3knxdw3t72\",\"handle\":\"bob.test\",\"name\":null,\"bio\":null,\"avatar\":null,\"about\":null,\"repos\":[{\"owner\":\"bob.test\",\"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:22yzhoyrfxuuax3knxdw3t72\",\"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,
@@ -183,7 +183,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"repo\":{\"authority\":\"bob.test\",\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":{\"shown\":\"alice.test/tally\",\"href\":\"/alice.test/tally\"},\"head\":\"main\",\"pulls\":0,\"issues\":0,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/bob.test/tally/card.png?v=e791445b9678abe3\"},\"rev\":\"main\",\"path\":\"\",\"empty\":false,\"last\":{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},\"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=\\\"/bob.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=\\\"/bob.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\":\"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\"},\"head\":\"main\",\"pulls\":0,\"issues\":0,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/bob.test/tally/card.png?v=e791445b9678abe3\"},\"rev\":\"main\",\"path\":\"\",\"empty\":false,\"last\":{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":\"Alice Moreau\",\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},\"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=\\\"/bob.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=\\\"/bob.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/search?q=t": {
"status": 200,
@@ -191,7 +191,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"query\":\"t\",\"repos\":[{\"owner\":\"bob.test\",\"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\",\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"name\":\"notes\",\"description\":\"Scratch notes 📝\",\"forkOf\":null,\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"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-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}]}"
},
"/alice.test/tally/raw/main/assets/logo.svg": {
"status": 200,
web/src/lib/api/Discussion.ts+5−1
@@ -9,7 +9,11 @@ export type Discussion = { repo: RepoHead,
/**
* `pull` or `issue`.
*/
kind: string, title: string, state: string, author: string, createdAt: string,
kind: string, title: string, state: string, author: string,
/**
* The author's profile picture, if it has one.
*/
avatar: string | null, createdAt: string,
/**
* A pull's target branch, and the repository and branch it brings.
*/
web/src/lib/api/Listed.ts+5−1
@@ -4,7 +4,11 @@ import type { Link } from "./Link";
/**
* A repository among others: whose, its name, and what it is.
*/
export type Listed = { owner: string, name: string, description: string | null, forkOf: Link | null, createdAt: string,
export type Listed = { owner: string,
/**
* The owner's profile picture, if it has one.
*/
avatar: string | null, name: string, description: string | null, forkOf: Link | null, createdAt: string,
/**
* Whether it is private, in a space: shown only serving `--private`.
*/
web/src/lib/api/RepoHead.ts+5−1
@@ -4,7 +4,11 @@ import type { Link } from "./Link";
/**
* What every page about a repository shows above itself.
*/
export type RepoHead = { authority: string, name: string, description: string | null, forkOf: Link | null,
export type RepoHead = { authority: string,
/**
* The owner's profile picture, if it has one.
*/
avatar: string | null, name: string, description: string | null, forkOf: Link | null,
/**
* The default branch.
*/
web/src/lib/api/Said.ts+5−1
@@ -3,7 +3,11 @@
/**
* Something said on a pull or an issue, made into HTML.
*/
export type Said = { author: string, createdAt: string, html: string,
export type Said = { author: string,
/**
* The author's profile picture, if it has one.
*/
avatar: string | null, createdAt: string, html: string,
/**
* `approve` or `changes`, for a review.
*/
web/src/lib/organisms/Comment.svelte+2−2
@@ -4,13 +4,13 @@
import Avatar from '#lib/atoms/Avatar.svelte';
import Person from '#lib/molecules/Person.svelte';
/** Something said on a pull or an issue: who said it, when, and what. */
let { author, said, createdAt, html, end }: { author: string; said: string; createdAt: string; html: string; end?: Snippet } =
let { author, avatar, said, createdAt, html, end }: { author: string; avatar?: string | null; said: string; createdAt: string; html: string; end?: Snippet } =
$props();
</script>
<div class="box comment">
<div class="boxhead">
<Avatar name={author} />
<Avatar name={author} src={avatar} />
<Person handle={author} />
<span class="mute">{said} on {date(createdAt)}</span>
{#if end}<span class="end">{@render end()}</span>{/if}
web/src/lib/organisms/RepoHeader.svelte+1−1
@@ -18,7 +18,7 @@
<div class="repohead">
<div class="about">
<h1><Avatar name={repo.authority} /><a href="/{urlPath(repo.authority)}">{repo.authority}</a><span class="mute">/</span><a href={href()}><b>{repo.name}</b></a></h1>
<h1><Avatar name={repo.authority} src={repo.avatar} /><a href="/{urlPath(repo.authority)}">{repo.authority}</a><span class="mute">/</span><a href={href()}><b>{repo.name}</b></a></h1>
{#if repo.description}<p>{repo.description}</p>{/if}
{#if repo.forkOf}<p><Icon name="fork" /> Forked from <a href={repo.forkOf.href}>{repo.forkOf.shown}</a></p>{/if}
</div>
web/src/lib/organisms/RepoList.svelte+1−1
@@ -25,7 +25,7 @@
</li>
{:else}
<li>
<Avatar name={r.owner} size="lg" />
<Avatar name={r.owner} size="lg" src={r.avatar} />
<div class="grow">
<a class="clip" href={repoHref(r.owner, r.name)}><span class="mute">{r.owner}/</span><b>{r.name}</b></a>
<div class="meta clip">{r.description ?? 'No description'}</div>
web/src/routes/[authority]/[repo]/[kind=item]/[did]/[rkey]/+page.svelte+2−2
@@ -26,9 +26,9 @@
<span class="end"><Icon name="chat" />{n} comment{n === 1 ? '' : 's'}</span>
</div>
<div class="timeline">
{#if d.body !== null}<Comment author={d.author} said="opened" createdAt={d.createdAt} html={d.body} />{/if}
{#if d.body !== null}<Comment author={d.author} avatar={d.avatar} said="opened" createdAt={d.createdAt} html={d.body} />{/if}
{#each d.comments as c, i (i)}
<Comment author={c.author} said="commented" createdAt={c.createdAt} html={c.html}>
<Comment author={c.author} avatar={c.avatar} said="commented" createdAt={c.createdAt} html={c.html}>
{#snippet end()}<Verdict verdict={c.verdict} />{/snippet}
</Comment>
{/each}