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

Shots: checks skip pages that did not change, and hold still between runs

shots-check and shots-live take only the pages whose digest changed since
their baselines, as shots does; SHOTS_ALL=1 takes every one. A digest is
now of the browser's version too, so a new browser takes every page again,
and leaves out what differs between builds of the same pages (SvelteKit's
version and the name made from it) and record keys in the data a page
carries: before, every build changed every page, so nothing was ever
skipped. Each page's digest is worked out once, in setup.

The world's records got their times in the order they were stamped, and
one push's records are stamped in the same millisecond or a millisecond
apart, so a ref's time on the front page moved by a slot from one seeding
to another. Records made within 50 ms of each other are ordered by
collection and key instead. The fixtures are recorded again: they held
page answers from before they were sent no-cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chad Miller and Claude Opus 5.5 committed on Oct 21, 2026 parent 1b3f6cc commit dddda39
Showing 99 changed files with 181 additions and 144 deletions
CLAUDE.md+7−1
@@ -22,7 +22,13 @@ work is done.
`harness/shots/baseline/<platform>/`. After a change to what a page's
answer holds, `just shots-up` in the background once, then `just
shots-record`. If another checkout's shots hold the ports (2593 to
2595), set `SHOTS_PORT` to another base for all of these.
2595), set `SHOTS_PORT` to another base for all of these, though
`start` shows its port and will not match on another.
- `shots-check` and `shots-live` take only pages whose HTML or browser
changed since their baselines, as `shots` does; `SHOTS_ALL=1` takes
every one. Keep `just shots-up` running in the background while
working on pages: `shots-live` then reuses its world rather than
seeding one, which is most of its time.
- In Svelte markup, keep a run of inline text one expression or one line:
a block or component beside it splits the text into nodes, which shapes
differently from the same text whole and moves pixels.
Justfile+8−5
@@ -102,15 +102,18 @@ web-dev:
shots *args: web
cd harness && npx playwright test -c shots/playwright.config.mjs --update-snapshots=all {{args}}
# Every screenshot, failing on any pixel that differs from the baselines.
# The screenshots of pages whose HTML or browser changed since their
# baselines were taken, failing on any pixel that differs: the others
# follow from what they did. SHOTS_ALL=1 takes every one.
shots-check *args: web
cd harness && SHOTS_ALL=1 npx playwright test -c shots/playwright.config.mjs {{args}}
cd harness && npx playwright test -c shots/playwright.config.mjs {{args}}
# Every screenshot of the whole stack, the pages behind knotless-serve over
# a seeded PDS, failing on any pixel that differs from the baselines.
# The same, of the whole stack: the pages behind knotless-serve over a
# seeded PDS, the one `shots-up` keeps up if it is running. A page whose
# HTML is what its baseline was taken from passes without a screenshot.
shots-live *args: web
{{cargo}} build --bins
cd harness && SHOTS_LIVE=1 SHOTS_ALL=1 npx playwright test -c shots/playwright.config.mjs {{args}}
cd harness && SHOTS_LIVE=1 npx playwright test -c shots/playwright.config.mjs {{args}}
# Records again what knotless-serve answers for each page, from the world
# `shots-up` keeps up or a new one, into harness/shots/fixtures.json: after
harness/shots/baseline/darwin/commit-desktop-dark.sha256+1−1
@@ -1 +1 @@
30c2287591545df9cad1c6568975854d27c3cd9f28c2f2e7db548a9a55923a63
\ No newline at end of file
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
harness/shots/baseline/darwin/commit-desktop-light.sha256+1−1
@@ -1 +1 @@
30c2287591545df9cad1c6568975854d27c3cd9f28c2f2e7db548a9a55923a63
\ No newline at end of file
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-dark.sha256+1−1
@@ -1 +1 @@
30c2287591545df9cad1c6568975854d27c3cd9f28c2f2e7db548a9a55923a63
\ No newline at end of file
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
harness/shots/baseline/darwin/commit-phone-light.sha256+1−1
@@ -1 +1 @@
30c2287591545df9cad1c6568975854d27c3cd9f28c2f2e7db548a9a55923a63
\ No newline at end of file
5f33fdeee80107fbfd0cde6823aa256066ab72c372765cf12176229e107646f5
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-dark.sha256+1−1
@@ -1 +1 @@
ae3b82c8d221203e2dec744821b04f75cb78bc43a30bea3169c12007d98dba46
\ No newline at end of file
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
harness/shots/baseline/darwin/commits-desktop-light.sha256+1−1
@@ -1 +1 @@
ae3b82c8d221203e2dec744821b04f75cb78bc43a30bea3169c12007d98dba46
\ No newline at end of file
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-dark.sha256+1−1
@@ -1 +1 @@
ae3b82c8d221203e2dec744821b04f75cb78bc43a30bea3169c12007d98dba46
\ No newline at end of file
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
harness/shots/baseline/darwin/commits-phone-light.sha256+1−1
@@ -1 +1 @@
ae3b82c8d221203e2dec744821b04f75cb78bc43a30bea3169c12007d98dba46
\ No newline at end of file
a29044f01576d24fb93df70c5e8f6d06855c0070f875f5d27055335f217a7b70
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-dark.sha256+1−1
@@ -1 +1 @@
680ed046f9ec49fc79d8293809e825061d0753959d2b7dd81d202bf258265a90
\ No newline at end of file
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
harness/shots/baseline/darwin/directory-desktop-light.sha256+1−1
@@ -1 +1 @@
680ed046f9ec49fc79d8293809e825061d0753959d2b7dd81d202bf258265a90
\ No newline at end of file
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-dark.sha256+1−1
@@ -1 +1 @@
680ed046f9ec49fc79d8293809e825061d0753959d2b7dd81d202bf258265a90
\ No newline at end of file
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
harness/shots/baseline/darwin/directory-phone-light.sha256+1−1
@@ -1 +1 @@
680ed046f9ec49fc79d8293809e825061d0753959d2b7dd81d202bf258265a90
\ No newline at end of file
a479045b6c6c00e1127b7960476abe9b409bac0a230fac4ec4f82afc1a3c165e
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-dark.sha256+1−1
@@ -1 +1 @@
4874e3d6de149e8c479101191ea671d57de2cda33b0b8572f80f76e3ce8ec152
\ No newline at end of file
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
harness/shots/baseline/darwin/empty-desktop-light.sha256+1−1
@@ -1 +1 @@
4874e3d6de149e8c479101191ea671d57de2cda33b0b8572f80f76e3ce8ec152
\ No newline at end of file
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-dark.sha256+1−1
@@ -1 +1 @@
4874e3d6de149e8c479101191ea671d57de2cda33b0b8572f80f76e3ce8ec152
\ No newline at end of file
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
harness/shots/baseline/darwin/empty-phone-light.sha256+1−1
@@ -1 +1 @@
4874e3d6de149e8c479101191ea671d57de2cda33b0b8572f80f76e3ce8ec152
\ No newline at end of file
b761ba0df24067cb45f0da46a6a0ede9a0ad68b7b6bd4bbd083b16bb28e6cca0
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-dark.sha256+1−1
@@ -1 +1 @@
fcaa6c8b2594e10f2c400beab719a4b81c88e06c9952365df97c2bc4c602fc68
\ No newline at end of file
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
harness/shots/baseline/darwin/file-desktop-light.sha256+1−1
@@ -1 +1 @@
fcaa6c8b2594e10f2c400beab719a4b81c88e06c9952365df97c2bc4c602fc68
\ No newline at end of file
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-dark.sha256+1−1
@@ -1 +1 @@
fcaa6c8b2594e10f2c400beab719a4b81c88e06c9952365df97c2bc4c602fc68
\ No newline at end of file
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
harness/shots/baseline/darwin/file-phone-light.sha256+1−1
@@ -1 +1 @@
fcaa6c8b2594e10f2c400beab719a4b81c88e06c9952365df97c2bc4c602fc68
\ No newline at end of file
477b5dd5b713ad61fe043581595dff1dd7ccc26295140d72c9ef8ab4378db407
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
653896e6201969938566188b0f857a34fab83bc305a4963c1da4c23b1861e275
\ No newline at end of file
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
harness/shots/baseline/darwin/fork-desktop-light.sha256+1−1
@@ -1 +1 @@
653896e6201969938566188b0f857a34fab83bc305a4963c1da4c23b1861e275
\ No newline at end of file
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-dark.sha256+1−1
@@ -1 +1 @@
653896e6201969938566188b0f857a34fab83bc305a4963c1da4c23b1861e275
\ No newline at end of file
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
harness/shots/baseline/darwin/fork-phone-light.sha256+1−1
@@ -1 +1 @@
653896e6201969938566188b0f857a34fab83bc305a4963c1da4c23b1861e275
\ No newline at end of file
7a414e54a8301431eb6d4ad6a143c1c834b921c494ea30aa623268adf83ae41a
\ No newline at end of file
harness/shots/baseline/darwin/home-desktop-dark.sha256+1−1
@@ -1 +1 @@
361a0cd3b876a181c797407c18ddcb8d11227736be5ff396112b5d5221f4e86e
\ No newline at end of file
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
harness/shots/baseline/darwin/home-desktop-light.sha256+1−1
@@ -1 +1 @@
361a0cd3b876a181c797407c18ddcb8d11227736be5ff396112b5d5221f4e86e
\ No newline at end of file
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
harness/shots/baseline/darwin/home-phone-dark.sha256+1−1
@@ -1 +1 @@
361a0cd3b876a181c797407c18ddcb8d11227736be5ff396112b5d5221f4e86e
\ No newline at end of file
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
harness/shots/baseline/darwin/home-phone-light.sha256+1−1
@@ -1 +1 @@
361a0cd3b876a181c797407c18ddcb8d11227736be5ff396112b5d5221f4e86e
\ No newline at end of file
69e43d28d6bb91a144feb18e5828905c126364d320d8ab6af1899f8639143e33
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-dark.sha256+1−1
@@ -1 +1 @@
7fc83dceffbb6c1c134d4ac7d796c5fdcaade2dc443f831f1db4f86156adf19e
\ No newline at end of file
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
harness/shots/baseline/darwin/image-desktop-light.sha256+1−1
@@ -1 +1 @@
7fc83dceffbb6c1c134d4ac7d796c5fdcaade2dc443f831f1db4f86156adf19e
\ No newline at end of file
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-dark.sha256+1−1
@@ -1 +1 @@
7fc83dceffbb6c1c134d4ac7d796c5fdcaade2dc443f831f1db4f86156adf19e
\ No newline at end of file
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
harness/shots/baseline/darwin/image-phone-light.sha256+1−1
@@ -1 +1 @@
7fc83dceffbb6c1c134d4ac7d796c5fdcaade2dc443f831f1db4f86156adf19e
\ No newline at end of file
44f48b67e5931381247fc9aa76614c26326cf132174382fd02f88eb8e3f00d86
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
262ad50dc39ab7d379e795071ac0a82ad8829f012547f53c3f9fd4d858567f88
\ No newline at end of file
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
262ad50dc39ab7d379e795071ac0a82ad8829f012547f53c3f9fd4d858567f88
\ No newline at end of file
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
262ad50dc39ab7d379e795071ac0a82ad8829f012547f53c3f9fd4d858567f88
\ No newline at end of file
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
harness/shots/baseline/darwin/issue-closed-phone-light.sha256+1−1
@@ -1 +1 @@
262ad50dc39ab7d379e795071ac0a82ad8829f012547f53c3f9fd4d858567f88
\ No newline at end of file
56a3f516604027d9dfcc09f95514afdc8028f98dae66850c4bb29a03460fa230
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-dark.sha256+1−1
@@ -1 +1 @@
700957d163bbd2cf3329ad9e771dbb2a4f5cb1702f89bb81b196b53b0dab455a
\ No newline at end of file
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
harness/shots/baseline/darwin/issue-desktop-light.sha256+1−1
@@ -1 +1 @@
700957d163bbd2cf3329ad9e771dbb2a4f5cb1702f89bb81b196b53b0dab455a
\ No newline at end of file
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-dark.sha256+1−1
@@ -1 +1 @@
700957d163bbd2cf3329ad9e771dbb2a4f5cb1702f89bb81b196b53b0dab455a
\ No newline at end of file
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
harness/shots/baseline/darwin/issue-phone-light.sha256+1−1
@@ -1 +1 @@
700957d163bbd2cf3329ad9e771dbb2a4f5cb1702f89bb81b196b53b0dab455a
\ No newline at end of file
2bc3e7020528e239e2ce7712002c6cdf390c99a8f8758ffe8fade4fc4e770236
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
f3859abca9ed1f3d2d75bbcbb12e9dc3c0e05a854834285ea6fdb8940116e57e
\ No newline at end of file
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
f3859abca9ed1f3d2d75bbcbb12e9dc3c0e05a854834285ea6fdb8940116e57e
\ No newline at end of file
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
f3859abca9ed1f3d2d75bbcbb12e9dc3c0e05a854834285ea6fdb8940116e57e
\ No newline at end of file
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
harness/shots/baseline/darwin/issues-closed-phone-light.sha256+1−1
@@ -1 +1 @@
f3859abca9ed1f3d2d75bbcbb12e9dc3c0e05a854834285ea6fdb8940116e57e
\ No newline at end of file
c19af68e9834765eef196d108ed794145a81006d840e3bcb7b925a4d4124314c
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-dark.sha256+1−1
@@ -1 +1 @@
4136ae8cda5cd8e0a60544cabb5bc9c2fd2383aca7456efe938a446a69ef139f
\ No newline at end of file
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
harness/shots/baseline/darwin/issues-desktop-light.sha256+1−1
@@ -1 +1 @@
4136ae8cda5cd8e0a60544cabb5bc9c2fd2383aca7456efe938a446a69ef139f
\ No newline at end of file
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-dark.sha256+1−1
@@ -1 +1 @@
4136ae8cda5cd8e0a60544cabb5bc9c2fd2383aca7456efe938a446a69ef139f
\ No newline at end of file
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
harness/shots/baseline/darwin/issues-phone-light.sha256+1−1
@@ -1 +1 @@
4136ae8cda5cd8e0a60544cabb5bc9c2fd2383aca7456efe938a446a69ef139f
\ No newline at end of file
c31abe88126abf5cb983d1c4cf5e3de4dc8697d99bb4a20624da8682de6bd470
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-dark.sha256+1−1
@@ -1 +1 @@
d71b8d6d1dda0b5cfa596591a39ef356f2669dc54bfbd9007432ec8d2458b0fb
\ No newline at end of file
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
harness/shots/baseline/darwin/markdown-desktop-light.sha256+1−1
@@ -1 +1 @@
d71b8d6d1dda0b5cfa596591a39ef356f2669dc54bfbd9007432ec8d2458b0fb
\ No newline at end of file
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-dark.sha256+1−1
@@ -1 +1 @@
d71b8d6d1dda0b5cfa596591a39ef356f2669dc54bfbd9007432ec8d2458b0fb
\ No newline at end of file
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
harness/shots/baseline/darwin/markdown-phone-light.sha256+1−1
@@ -1 +1 @@
d71b8d6d1dda0b5cfa596591a39ef356f2669dc54bfbd9007432ec8d2458b0fb
\ No newline at end of file
2bcc1c4cb881fb0e36764ff9715171f6cd493f4d8290261121a82756a299f260
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-dark.sha256+1−1
@@ -1 +1 @@
3e52a0a6548af003e6b68a1ff276bf21e953cbd166a162e3e70150a52381c76f
\ No newline at end of file
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
harness/shots/baseline/darwin/missing-desktop-light.sha256+1−1
@@ -1 +1 @@
3e52a0a6548af003e6b68a1ff276bf21e953cbd166a162e3e70150a52381c76f
\ No newline at end of file
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-dark.sha256+1−1
@@ -1 +1 @@
3e52a0a6548af003e6b68a1ff276bf21e953cbd166a162e3e70150a52381c76f
\ No newline at end of file
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
harness/shots/baseline/darwin/missing-phone-light.sha256+1−1
@@ -1 +1 @@
3e52a0a6548af003e6b68a1ff276bf21e953cbd166a162e3e70150a52381c76f
\ No newline at end of file
68fb65092837b2dab69af58d1ca50ae4a8b7762354d62d06b72741e92c82afbf
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-dark.sha256+1−1
@@ -1 +1 @@
932e2a886118b68e26b06e5e71517882ae6ddc0583f1aecd1cafa48bd581eec3
\ No newline at end of file
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
harness/shots/baseline/darwin/profile-desktop-light.sha256+1−1
@@ -1 +1 @@
932e2a886118b68e26b06e5e71517882ae6ddc0583f1aecd1cafa48bd581eec3
\ No newline at end of file
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-dark.sha256+1−1
@@ -1 +1 @@
f363bf9ca0352375c312f4fe518f3fa9012976827723d2f21d5217806458277f
\ No newline at end of file
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-desktop-light.sha256+1−1
@@ -1 +1 @@
f363bf9ca0352375c312f4fe518f3fa9012976827723d2f21d5217806458277f
\ No newline at end of file
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-dark.sha256+1−1
@@ -1 +1 @@
f363bf9ca0352375c312f4fe518f3fa9012976827723d2f21d5217806458277f
\ No newline at end of file
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
harness/shots/baseline/darwin/profile-fork-phone-light.sha256+1−1
@@ -1 +1 @@
f363bf9ca0352375c312f4fe518f3fa9012976827723d2f21d5217806458277f
\ No newline at end of file
384d2aa6d1e8a35b24c13b282f88134956c69e1d17231fd8d9d487ac472aa5c5
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-dark.sha256+1−1
@@ -1 +1 @@
932e2a886118b68e26b06e5e71517882ae6ddc0583f1aecd1cafa48bd581eec3
\ No newline at end of file
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
harness/shots/baseline/darwin/profile-phone-light.sha256+1−1
@@ -1 +1 @@
932e2a886118b68e26b06e5e71517882ae6ddc0583f1aecd1cafa48bd581eec3
\ No newline at end of file
019faa64f2278213d21bee20c62386996347d75a7d9ce703e13554893ae85521
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-dark.sha256+1−1
@@ -1 +1 @@
69ea0625f1944a1bc82bf1c67c3ea1974b9f6dae5291fa0c022191b8f2ccaec9
\ No newline at end of file
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
harness/shots/baseline/darwin/pull-desktop-light.sha256+1−1
@@ -1 +1 @@
69ea0625f1944a1bc82bf1c67c3ea1974b9f6dae5291fa0c022191b8f2ccaec9
\ No newline at end of file
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-dark.sha256+1−1
@@ -1 +1 @@
1308591b3abd722e83b9476d6ac44e9a0d9a3fbfb3646cb70bd80f14a0817769
\ No newline at end of file
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-desktop-light.sha256+1−1
@@ -1 +1 @@
1308591b3abd722e83b9476d6ac44e9a0d9a3fbfb3646cb70bd80f14a0817769
\ No newline at end of file
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-dark.sha256+1−1
@@ -1 +1 @@
1308591b3abd722e83b9476d6ac44e9a0d9a3fbfb3646cb70bd80f14a0817769
\ No newline at end of file
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
harness/shots/baseline/darwin/pull-open-phone-light.sha256+1−1
@@ -1 +1 @@
1308591b3abd722e83b9476d6ac44e9a0d9a3fbfb3646cb70bd80f14a0817769
\ No newline at end of file
9d8c1e92639e49042e980e181cf0de860ad0ba85dc662de983d4bc681c045937
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-dark.sha256+1−1
@@ -1 +1 @@
69ea0625f1944a1bc82bf1c67c3ea1974b9f6dae5291fa0c022191b8f2ccaec9
\ No newline at end of file
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
harness/shots/baseline/darwin/pull-phone-light.sha256+1−1
@@ -1 +1 @@
69ea0625f1944a1bc82bf1c67c3ea1974b9f6dae5291fa0c022191b8f2ccaec9
\ No newline at end of file
6563f759880226dc1a30ca6c3421101e6cdad6ee79c134ffe826d3b29c95cbfe
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-dark.sha256+1−1
@@ -1 +1 @@
28c271033f2eed93c545bb82dd59d2706512f75c416c186689405c56b06f1f5d
\ No newline at end of file
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-desktop-light.sha256+1−1
@@ -1 +1 @@
28c271033f2eed93c545bb82dd59d2706512f75c416c186689405c56b06f1f5d
\ No newline at end of file
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-dark.sha256+1−1
@@ -1 +1 @@
28c271033f2eed93c545bb82dd59d2706512f75c416c186689405c56b06f1f5d
\ No newline at end of file
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-closed-phone-light.sha256+1−1
@@ -1 +1 @@
28c271033f2eed93c545bb82dd59d2706512f75c416c186689405c56b06f1f5d
\ No newline at end of file
eee77c78ee5b8631fce52e93142b0406d3e783c16ffd98e5700d5f9d9905204d
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-dark.sha256+1−1
@@ -1 +1 @@
0b62d78c40f0b2d0d4442e83d222bb7f1632e13fd1447150022be162c8c7ae2c
\ No newline at end of file
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
harness/shots/baseline/darwin/pulls-desktop-light.sha256+1−1
@@ -1 +1 @@
0b62d78c40f0b2d0d4442e83d222bb7f1632e13fd1447150022be162c8c7ae2c
\ No newline at end of file
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-dark.sha256+1−1
@@ -1 +1 @@
0b62d78c40f0b2d0d4442e83d222bb7f1632e13fd1447150022be162c8c7ae2c
\ No newline at end of file
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
harness/shots/baseline/darwin/pulls-phone-light.sha256+1−1
@@ -1 +1 @@
0b62d78c40f0b2d0d4442e83d222bb7f1632e13fd1447150022be162c8c7ae2c
\ No newline at end of file
b25928b19bb96125e47691e9039500f93ad343042a32202fdc469b58ca67f036
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-dark.sha256+1−1
@@ -1 +1 @@
45eb9d421b03985216195bb1b8105eef507601db62e6f9936856942a9733a9a1
\ No newline at end of file
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
harness/shots/baseline/darwin/repository-desktop-light.sha256+1−1
@@ -1 +1 @@
45eb9d421b03985216195bb1b8105eef507601db62e6f9936856942a9733a9a1
\ No newline at end of file
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-dark.sha256+1−1
@@ -1 +1 @@
45eb9d421b03985216195bb1b8105eef507601db62e6f9936856942a9733a9a1
\ No newline at end of file
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
harness/shots/baseline/darwin/repository-phone-light.sha256+1−1
@@ -1 +1 @@
45eb9d421b03985216195bb1b8105eef507601db62e6f9936856942a9733a9a1
\ No newline at end of file
eac125019c62eab00ad5c67a627c8409ab575daba682b9283063968691b71769
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-dark.sha256+1−1
@@ -1 +1 @@
2d4c709ad9dcb6c3e7621726e7eb80aeca8b7d4e8d423d61afd7d402587c7e7d
\ No newline at end of file
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
harness/shots/baseline/darwin/search-desktop-light.sha256+1−1
@@ -1 +1 @@
2d4c709ad9dcb6c3e7621726e7eb80aeca8b7d4e8d423d61afd7d402587c7e7d
\ No newline at end of file
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-dark.sha256+1−1
@@ -1 +1 @@
2d4c709ad9dcb6c3e7621726e7eb80aeca8b7d4e8d423d61afd7d402587c7e7d
\ No newline at end of file
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
harness/shots/baseline/darwin/search-phone-light.sha256+1−1
@@ -1 +1 @@
2d4c709ad9dcb6c3e7621726e7eb80aeca8b7d4e8d423d61afd7d402587c7e7d
\ No newline at end of file
23008d19fec907bc53c030dedebee818357798ba0b0847725e16b606f64afa9f
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-dark.sha256+1−1
@@ -1 +1 @@
d5186dd93b1f659689538986a512dc7b84fd9c63a9bbfba5cf305af23b0e3e22
\ No newline at end of file
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
harness/shots/baseline/darwin/start-desktop-light.sha256+1−1
@@ -1 +1 @@
d5186dd93b1f659689538986a512dc7b84fd9c63a9bbfba5cf305af23b0e3e22
\ No newline at end of file
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-dark.sha256+1−1
@@ -1 +1 @@
d5186dd93b1f659689538986a512dc7b84fd9c63a9bbfba5cf305af23b0e3e22
\ No newline at end of file
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
harness/shots/baseline/darwin/start-phone-light.sha256+1−1
@@ -1 +1 @@
d5186dd93b1f659689538986a512dc7b84fd9c63a9bbfba5cf305af23b0e3e22
\ No newline at end of file
4144258dbb38373e3bf6d6a7ea8663d0602a0bd6c228080acfacfa87c22df26c
\ No newline at end of file
harness/shots/digest.mjs+12−3
@@ -1,12 +1,21 @@
// What a page's screenshot follows from: its HTML, which carries the
// styles and scripts too, less what differs between worlds: DIDs, the
// record keys in links to pulls and issues, and the port it is served on.
// styles and scripts too, less what differs between worlds (DIDs, record
// keys, in links and in the data the page carries, and the port it is
// served on) and between builds of the same pages (SvelteKit's version,
// the time of the build, and the name of its state, made from it).
import { createHash } from 'node:crypto'
export const digest = (html) =>
createHash('sha256')
.update(html.replace(/did:plc:[a-z2-7]{24}/g, 'did').replace(/127\.0\.0\.1:\d+/g, 'host').replace(/\/[2-7a-z]{13}(?=["/?#])/g, '/rkey'))
.update(
html
.replace(/did:plc:[a-z2-7]{24}/g, 'did')
.replace(/127\.0\.0\.1:\d+/g, 'host')
.replace(/\/[2-7a-z]{13}(?=["/?#\\])/g, '/rkey')
.replace(/__sveltekit_[a-z0-9]+/g, '__sveltekit')
.replace(/version: "\d+"/g, 'version'),
)
.digest('hex')
/// The path of the social card a page names, which carries a hash of what
harness/shots/fixtures.json+20−20
@@ -14,12 +14,12 @@
"commit": "/alice.test/tally/commit/97727c2c19d5d8b3849b2ed9ad47130cee9798f5",
"pulls": "/alice.test/tally/pulls",
"pulls-closed": "/alice.test/tally/pulls?state=closed",
"pull": "/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg",
"pull-open": "/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x",
"pull": "/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g",
"pull-open": "/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi",
"issues": "/alice.test/tally/issues",
"issues-closed": "/alice.test/tally/issues?state=closed",
"issue": "/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32",
"issue-closed": "/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u",
"issue": "/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs",
"issue-closed": "/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m",
"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: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}]}"
"body": "{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"Alice Moreau\",\"bio\":\"Small tools for text. Writes tally.\",\"avatar\":null,\"about\":\"Small tools for text. Writes tally.\",\"repos\":[{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"scratch\",\"description\":null,\"forkOf\":null,\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"notes\",\"description\":\"Scratch notes 📝\",\"forkOf\":null,\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"private\":false},{\"owner\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"private\":false}]}"
},
"/_data/alice.test/scratch": {
"status": 200,
@@ -85,7 +85,7 @@
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "public, max-age=31536000, immutable"
"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\"},\"commit\":{\"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\"},\"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\":\"}\"}]}]}"
},
@@ -97,21 +97,21 @@
},
"body": "{\"repo\":{\"authority\":\"alice.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":null,\"head\":\"main\",\"pulls\":1,\"issues\":1,\"about\":\"Counts lines, words and bytes, with output that lines up\",\"card\":\"/alice.test/tally/card.png?v=715ad689e9f7ec63\"},\"rev\":\"main\",\"path\":\"\",\"page\":0,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"parents\":[\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\"],\"author\":{\"name\":\"Bob Tanaka\",\"account\":\"bob.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-09T09:31:00+00:00\",\"message\":\"Count no lines in empty input\"},{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"parents\":[\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-08T11:47:00+00:00\",\"message\":\"Split parsing into its own module\"},{\"sha\":\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\",\"parents\":[\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-06T10:02:00+00:00\",\"message\":\"Document usage and output\"},{\"sha\":\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\",\"parents\":[\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-05T17:21:00+00:00\",\"message\":\"Add a logo\"},{\"sha\":\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\",\"parents\":[\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-03T08:05:00+00:00\",\"message\":\"Read from stdin when no file is given\\n\\nSo tally works at the end of a pipe, as wc does. The name shown\\nfor stdin is \\\"-\\\".\"},{\"sha\":\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\",\"parents\":[\"cac02c464843db4d55fecfa69d327ef7b12702c0\"],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-02T14:40:00+00:00\",\"message\":\"Count words, lines and bytes\"},{\"sha\":\"cac02c464843db4d55fecfa69d327ef7b12702c0\",\"parents\":[],\"author\":{\"name\":\"Alice Moreau\",\"account\":\"alice.test\",\"avatar\":null},\"coauthors\":[],\"date\":\"2026-10-01T09:12:00+00:00\",\"message\":\"Start tally\"}],\"more\":false}"
},
"/_data/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32": {
"/_data/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs": {
"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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32/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:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs/card.png?v=160a3e2db8db2127\"}"
},
"/_data/alice.test/tally/issue/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u": {
"/_data/alice.test/tally/issue/did:plc:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m": {
"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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u/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:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m/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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa2suo32\",\"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:rlblzwyobhzohu5brycywuq5/3mygokbyh7bvs\",\"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:6jyayarzmhqfvz4w4tgj4xes/3mygjqa7nqp4u\",\"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:rlblzwyobhzohu5brycywuq5/3mygokcei6c5m\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"comments\":0}]}"
},
"/_data/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg": {
"/_data/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g": {
"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:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg/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:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g/card.png?v=7f555a00efc7106b\"}"
},
"/_data/alice.test/tally/pull/did:plc:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x": {
"/_data/alice.test/tally/pull/did:plc:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi": {
"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:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x/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:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi/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:6jyayarzmhqfvz4w4tgj4xes/3mygjq7w6et2x\",\"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:rlblzwyobhzohu5brycywuq5/3mygokbv5rksi\",\"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:6jyayarzmhqfvz4w4tgj4xes/3mygjq5dbkkmg\",\"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:rlblzwyobhzohu5brycywuq5/3mygok6ir5j4g\",\"author\":\"bob.test\",\"createdAt\":\"2026-10-09T18:38:00.000Z\",\"comments\":3}]}"
},
"/_data/alice.test/tally/tree/main/src": {
"status": 200,
@@ -175,7 +175,7 @@
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"did\":\"did:plc: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}]}"
"body": "{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":null,\"bio\":null,\"avatar\":null,\"about\":null,\"repos\":[{\"owner\":\"bob.test\",\"avatar\":null,\"name\":\"tally\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":{\"shown\":\"alice.test/tally\",\"href\":\"/alice.test/tally\"},\"createdAt\":\"2026-10-09T14:19:00.000Z\",\"private\":false}]}"
},
"/_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: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\"}}]}"
"body": "{\"events\":[{\"createdAt\":\"2026-10-10T01:25:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokcei6c5m\",\"title\":\"Typo in the README ✏️\",\"state\":\"closed\"}},{\"createdAt\":\"2026-10-10T00:48:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokcei6c5m\",\"title\":\"Typo in the README ✏️\"}},{\"createdAt\":\"2026-10-10T00:11:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokbyh7bvs\",\"title\":\"Fails on files that are not UTF-8\",\"text\":\"Confirmed. Reading bytes and counting words lossily would fix it.\"}},{\"createdAt\":\"2026-10-09T23:34:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.issue/3mygokbyh7bvs\",\"title\":\"Fails on files that are not UTF-8\"}},{\"createdAt\":\"2026-10-09T22:57:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygokbv5rksi\",\"title\":\"Print counts as JSON with --json\"}},{\"createdAt\":\"2026-10-09T22:20:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#changed\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"state\":\"merged\"}},{\"createdAt\":\"2026-10-09T21:06:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":false,\"count\":1,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"message\":\"Count no lines in empty input\"}]}},{\"createdAt\":\"2026-10-09T20:29:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"text\":\"Fair enough. Thanks!\",\"verdict\":\"approve\"}},{\"createdAt\":\"2026-10-09T19:52:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"text\":\"It only runs per line, so the cost is one comparison each. Happy to change it if you prefer.\"}},{\"createdAt\":\"2026-10-09T19:15:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#commented\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\",\"text\":\"Could `lines` check for emptiness once, rather than in the filter?\",\"verdict\":\"changes\"}},{\"createdAt\":\"2026-10-09T18:38:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#opened\",\"subject\":\"at://did:plc:rlblzwyobhzohu5brycywuq5/dev.knotless.pull/3mygok6ir5j4g\",\"title\":\"Count no lines in empty input\"}},{\"createdAt\":\"2026-10-09T17:24:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/json\",\"created\":true,\"count\":1,\"commits\":[{\"sha\":\"2063dabcda7417d5831f3b6bfd8ffb18da82fb8e\",\"message\":\"Print counts as JSON with --json\"}]}},{\"createdAt\":\"2026-10-09T16:10:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/empty-input\",\"created\":true,\"count\":1,\"commits\":[{\"sha\":\"686a36205b82620af07dd99997c570f04f461df9\",\"message\":\"Count no lines in empty input\"}]}},{\"createdAt\":\"2026-10-09T14:19:00.000Z\",\"actor\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\"},\"repo\":{\"did\":\"did:plc:rlblzwyobhzohu5brycywuq5\",\"handle\":\"bob.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"forkOf\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"}}},{\"createdAt\":\"2026-10-09T13:42:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"scratch\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\"}},{\"createdAt\":\"2026-10-09T12:28:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":true,\"count\":1,\"commits\":[{\"sha\":\"e78543abf0f799b7eb5713ccdf8d88b84d0f8ec0\",\"message\":\"Begin\"}]}},{\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"notes\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Scratch notes 📝\"}},{\"createdAt\":\"2026-10-09T10:37:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#pushed\",\"ref\":\"refs/heads/main\",\"created\":true,\"count\":6,\"commits\":[{\"sha\":\"97727c2c19d5d8b3849b2ed9ad47130cee9798f5\",\"message\":\"Split parsing into its own module\"},{\"sha\":\"4f6b9a1168c6534f64c836b4c3a11e23e74637ad\",\"message\":\"Document usage and output\"},{\"sha\":\"d1f55d39d12c4d6ba6ab25b1e9c76d15fb2cbaca\",\"message\":\"Add a logo\"},{\"sha\":\"7818a5bc1bef9184ad313cf79175da3d87d4b9cf\",\"message\":\"Read from stdin when no file is given\\n\\nSo tally works at the end of a pipe, as wc does. The name shown\\nfor stdin is \\\"-\\\".\"},{\"sha\":\"a7e467c6f9acd7b1be0603afdec0b8cfdcf3cf49\",\"message\":\"Count words, lines and bytes\"},{\"sha\":\"cac02c464843db4d55fecfa69d327ef7b12702c0\",\"message\":\"Start tally\"}]}},{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"actor\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\"},\"repo\":{\"did\":\"did:plc:5y4vhycox5bylby7jqcqga6c\",\"handle\":\"alice.test\",\"name\":\"tally\"},\"action\":{\"$type\":\"dev.knotless.getFeed#created\",\"description\":\"Counts lines, words and bytes, with output that lines up\"}}]}"
}
}
}
harness/shots/setup.mjs+23−15
@@ -5,12 +5,16 @@
// front of it. The pages are written to
// target/shots/views.json, and those whose HTML is as it was when their
// baselines were taken, in every project, to target/shots/unchanged.json:
// views.spec.mjs skips them before a browser page is opened. So too the
// views.spec.mjs skips them before a browser page is opened. A page's
// digest is of its HTML and the browser's version, so a new browser takes
// every page again; each one is in target/shots/sums.json, for the
// baselines to be written beside. SHOTS_ALL=1 skips none. So too the
// pages whose cards are shot, to cards.json, and the cards whose URL is
// as it was, as `card:<name>`. knotless-serve draws the cards, so they are
// shot only from a world: from fixtures, cards.json is null and
// cards.spec.mjs skips them. Returns the teardown.
import { chromium } from '@playwright/test'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { base, create, page, root, saved, serve, until } from './world.mjs'
@@ -50,22 +54,26 @@ export default async function setup() {
writeFileSync(join(root, 'target/shots/views.json'), JSON.stringify(world.views, null, 2))
const cards = recorded ? null : world.cards
writeFileSync(join(root, 'target/shots/cards.json'), JSON.stringify(cards, null, 2))
const browser = await chromium.launch()
const version = browser.version()
await browser.close()
const at = (name, project, ext) => join(here, 'baseline', process.platform, `${name}-${project}.${ext}`)
const same = (name, project, sum) =>
!process.env.SHOTS_ALL &&
existsSync(at(name, project, 'png')) &&
existsSync(at(name, project, 'sha256')) &&
readFileSync(at(name, project, 'sha256'), 'utf8') === sum
const unchanged = []
if (!process.env.SHOTS_ALL) {
const at = (name, project, ext) => join(here, 'baseline', process.platform, `${name}-${project}.${ext}`)
const same = (name, project, sum) =>
existsSync(at(name, project, 'png')) &&
existsSync(at(name, project, 'sha256')) &&
readFileSync(at(name, project, 'sha256'), 'utf8') === sum
for (const [name, path] of Object.entries(world.views)) {
const sum = digest(await (await fetch(base + path)).text())
if (projects.every((p) => same(name, p, sum))) unchanged.push(name)
}
for (const [name, path] of Object.entries(cards ?? {})) {
const sum = digest(card(await (await fetch(base + path)).text()))
if (same(name, 'card', sum)) unchanged.push(`card:${name}`)
}
const sums = {}
for (const [name, path] of Object.entries(world.views)) {
sums[name] = digest(`chromium ${version}\n${await (await fetch(base + path)).text()}`)
if (projects.every((p) => same(name, p, sums[name]))) unchanged.push(name)
}
for (const [name, path] of Object.entries(cards ?? {})) {
const sum = digest(card(await (await fetch(base + path)).text()))
if (same(name, 'card', sum)) unchanged.push(`card:${name}`)
}
writeFileSync(join(root, 'target/shots/unchanged.json'), JSON.stringify(unchanged))
writeFileSync(join(root, 'target/shots/sums.json'), JSON.stringify(sums, null, 2))
return teardown
}
harness/shots/views.spec.mjs+6−7
@@ -2,16 +2,15 @@
// paths are known once setup.mjs has made them, so they are read in each test.
//
// A page's pixels follow from its HTML and the browser. Beside each
// baseline is a digest of the HTML it was taken from, and setup.mjs lists
// the pages whose HTML still matches in every project: those are skipped
// baseline is a digest of both it was taken from, and setup.mjs lists
// the pages whose digest still matches in every project: those are skipped
// as unchanged, before a browser page is opened. SHOTS_ALL=1 takes every
// one regardless, as `just shots-check` does.
// one regardless.
import { test, expect } from '@playwright/test'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { digest } from './digest.mjs'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const read = (file, otherwise) => {
@@ -19,6 +18,7 @@ const read = (file, otherwise) => {
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : otherwise
}
const unchanged = new Set(read('unchanged.json', []))
const sums = read('sums.json', {})
const names = [
'home', 'search', 'start', 'profile', 'profile-fork', 'repository', 'directory', 'file', 'markdown', 'image', 'commits', 'commit',
@@ -28,16 +28,15 @@ const names = [
for (const name of names) {
test.describe(() => {
test.skip(unchanged.has(name), 'unchanged')
test(name, async ({ page, request }, info) => {
test(name, async ({ page }, info) => {
const path = read('views.json', {})[name]
const sum = digest(await (await request.get(path)).text())
await page.goto(path, { waitUntil: 'load' })
// The pages paint their highlighting once they are running.
await page.locator('html[data-ready]').waitFor({ state: 'attached' })
// A DID is new each run, as the PLC directory makes one per account:
// it is covered, so the rest can match to the pixel.
await expect(page).toHaveScreenshot(`${name}.png`, { fullPage: true, mask: [page.locator('.did')], maskColor: '#80808a' })
writeFileSync(info.snapshotPath(`${name}.sha256`), sum)
writeFileSync(info.snapshotPath(`${name}.sha256`), sums[name])
})
})
}
harness/shots/world.mjs+13−1
@@ -272,7 +272,19 @@ export async function create() {
} while (cursor)
}
}
records.sort((x, y) => (x.value.createdAt < y.value.createdAt ? -1 : x.value.createdAt > y.value.createdAt ? 1 : 0))
// One write's records can be stamped a millisecond apart or in the same
// one, which would order them either way: those made within 50 ms of
// each other are ordered by collection and key instead.
const at = (r) => Date.parse(r.value.createdAt)
records.sort((x, y) => at(x) - at(y))
const writes = []
for (const r of records) {
const last = writes.at(-1)
if (last && at(r) - at(last.at(-1)) < 50) last.push(r)
else writes.push([r])
}
const named = (r) => `${r.collection} ${r.uri}`
records.splice(0, records.length, ...writes.flatMap((w) => w.sort((x, y) => (named(x) < named(y) ? -1 : 1))))
const start = Date.parse('2026-10-09T10:00:00Z')
const cids = new Map()
// Strong references to a record whose CID changed are pointed at the new one.