// The world the screenshots show: a reference PDS holding the same // repositories, pulls and issues every time, so each page renders to the // same pixels. Commits carry fixed dates, so their hashes are fixed too, and // every record's createdAt is rewritten to a fixed time, in the order the // records were made. The index a knotless-serve built while it was seeded // is kept in `dir`, with its mirrors, for the next server to start from. // // node harness/shots/world.mjs seed one and keep it up, for // `just shots` runs to reuse, until // ctrl-c; written to target/shots/world.json import { spawn, execFileSync } from 'node:child_process' import { createHash } from 'node:crypto' import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { DatabaseSync } from 'node:sqlite' import { dirname, join, resolve } from 'node:path' import { createInterface } from 'node:readline' import { fileURLToPath } from 'node:url' import { crc32, deflateSync } from 'node:zlib' const here = dirname(fileURLToPath(import.meta.url)) export const root = resolve(here, '../..') const fixture = join(here, 'fixture') const bin = join(root, 'target/debug') // Where the world answers, and the pages behind it on the next two ports: // three of their own for each checkout, from its path, so the shots of two // side by side do not meet, and three more for the end-to-end run's world // (E2E=1), so it goes beside `shots-up`'s; SHOTS_PORT moves them. const own = (process.env.E2E ? 29000 : 20000) + 3 * (createHash('sha256').update(root).digest().readUInt16BE(0) % 3000) export const port = Number(process.env.SHOTS_PORT ?? own) export const base = `http://127.0.0.1:${port}` export const saved = join(root, 'target/shots/world.json') // The pages say how long ago things were: now is a few hours after the // fixtures' last, for the pages and the browser both, so the pictures are // the same any day. export const now = '2026-10-20T14:00:00Z' /// When some of each runner's minutes free up, as the pages show it. const freeing = '2026-11-02T00:00:00Z' // The views drawn signed in, and as whom: only from the recording, where // the stand-in for knotless-serve answers getViewer as that account // (fixtures.mjs). A world has no session to sign them in, so shots-live // skips them. export const viewers = { settings: 'alice.test', 'settings-branches': 'alice.test', 'settings-labels': 'alice.test', 'settings-access': 'alice.test', 'settings-actions': 'alice.test', notifications: 'alice.test', // One who is neither the owner nor a collaborator: what to hear of the // repository, and of the issue it opened. 'repository-watch': 'bob.test', 'issue-subscribe': 'bob.test', } const people = { alice: { name: 'Alice Moreau', email: 'alice@example.com' }, bob: { name: 'Bob Tanaka', email: 'bob@example.com' }, } export const until = async (what, check) => { for (let i = 0; i < 300; i++) { try { if (await check()) return } catch {} await new Promise((r) => setTimeout(r, 100)) } throw new Error(`timed out waiting for ${what}`) } /// What knotless keys a ref by: its SHA-256 in lower base32, cut to 26. const refHash = (name) => { const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' let bits = '' for (const b of createHash('sha256').update(name).digest()) bits += b.toString(2).padStart(8, '0') return (bits.match(/.{1,5}/g) ?? []) .map((c) => alphabet[parseInt(c.padEnd(5, '0'), 2)]) .join('') .slice(0, 26) } export const page = async (path) => { const res = await fetch(base + path) return res.ok ? res.text() : '' } /// The environment `who` runs the CLI, git or the server in: committing /// `when`, as written `authored`, or then too. export const environment = (net, dir, who, when, authored) => { const clean = Object.fromEntries( Object.entries(process.env).filter(([k]) => !k.startsWith('KNOTLESS_')), ) const person = people[who] ?? { name: who, email: `${who}@example.com` } return { ...clean, PATH: `${bin}:${process.env.PATH}`, KNOTLESS_HANDLE_RESOLVER: net.pds, KNOTLESS_PLC_URL: net.plc, KNOTLESS_COMPACT: 'off', KNOTLESS_APPVIEW: base, KNOTLESS_CONFIG_DIR: join(dir, who, 'config'), KNOTLESS_CACHE_DIR: join(dir, who, 'cache'), GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1', GIT_AUTHOR_NAME: person.name, GIT_AUTHOR_EMAIL: person.email, GIT_COMMITTER_NAME: person.name, GIT_COMMITTER_EMAIL: person.email, GIT_AUTHOR_DATE: authored ?? when ?? '2026-10-01T09:00:00Z', GIT_COMMITTER_DATE: when ?? '2026-10-01T09:00:00Z', } } /// The pages, from `build` (this checkout's web/build unless given), /// answering on `at` and reading from `data`. export const pages = (at, data, build = join(root, 'web/build')) => spawn('node', [build], { env: { ...process.env, HOST: '127.0.0.1', PORT: String(at), KNOTLESS_SERVE: data, KNOTLESS_NOW: now, HOST_HEADER: 'x-forwarded-host', PROTOCOL_HEADER: 'x-forwarded-proto', }, stdio: 'inherit', }) /// Whether Zoekt is here to search code with: the world's server then /// keeps a code index, and its code results are shown. `just /// shots-record` needs it, as the shots record them. export const zoekt = (() => { try { execFileSync('sh', ['-c', 'command -v zoekt-webserver && command -v zoekt-git-index']) return true } catch { return false } })() /// knotless-serve from this build, over the world's index and mirrors, on /// `at`: in front of the pages, unless `alone`. With `gateway`, the https /// front a PDS sends logins back to, it is a gateway too, which the pages /// log in to. export const serve = async (world, { at = port, alone = false, gateway } = {}) => { const drawn = alone ? null : pages(port + 1, base) const server = spawn( join(bin, 'knotless-serve'), [ '--listen', `127.0.0.1:${at}`, '--index', join(world.dir, 'index.db'), '--firehose', world.net.pds.replace('http', 'ws'), ...(zoekt ? ['--code-index', join(world.dir, 'code')] : []), ...(alone ? [] : ['--web', `http://127.0.0.1:${port + 1}`]), // atproto takes a did:web with a port only for localhost. ...(gateway ? ['--public-url', `http://localhost:${at}`, '--oauth-url', gateway] : []), ], { env: { ...environment(world.net, world.dir, 'server'), ...(gateway ? { KNOTLESS_DID_WEB_SCHEME: 'http' } : {}), }, stdio: 'inherit', }, ) const kill = () => { server.kill() drawn?.kill() } const up = alone ? `http://127.0.0.1:${at}/healthz` : base try { await until('knotless-serve and the pages', async () => (await fetch(up)).ok) } catch (e) { kill() throw e } return { kill } } /// A new world: its PDS, a server, and the seeded data. Returns them with /// the pages to shoot. export async function create() { const dir = mkdtempSync(join(tmpdir(), 'knotless-shots-')) const children = [] const stop = () => { for (const child of children.reverse()) child.kill() rmSync(dir, { recursive: true, force: true }) } try { const pds = spawn('node', ['harness/pds.mjs', '--accounts', 'alice,bob,textkit'], { cwd: root, stdio: ['pipe', 'pipe', 'inherit'], }) children.push(pds) const [line] = await createInterface({ input: pds.stdout }) [Symbol.asyncIterator]() .next() .then((r) => [r.value]) const net = JSON.parse(line) const account = Object.fromEntries(net.accounts.map((a) => [a.handle.split('.')[0], a])) const work = dir const env = (who, when, authored) => environment(net, dir, who, when, authored) const run = (who, cmd, args, opts = {}) => execFileSync(cmd, args, { env: env(who, opts.when, opts.authored), cwd: opts.cwd ?? work, input: opts.input ?? '', }) .toString() .trim() const knotless = (who, ...args) => run(who, join(bin, 'knotless'), args) const git = (who, dir, when, ...args) => run(who, 'git', args, { cwd: dir, when }) /// Copies files from a fixture step into `dir` and commits them, as /// written `authored` if that was before. const commit = (who, dir, when, message, files, authored) => { for (const [from, to] of Object.entries(files)) { mkdirSync(dirname(join(dir, to)), { recursive: true }) cpSync(join(fixture, from), join(dir, to)) } git(who, dir, when, 'add', '-A') run(who, 'git', ['commit', '-q', '-m', message], { cwd: dir, when, authored }) } children.push(await serve({ net, dir })) for (const who of ['alice', 'bob']) { run(who, join(bin, 'knotless'), ['login', account[who].handle, '--password-stdin'], { input: account[who].password, }) } const tally = 'knotless://alice.test/tally' knotless( 'alice', 'repo', 'create', tally, '--description', 'Counts lines, words and bytes, with output that lines up', '--link', 'https://tally.dev', '--link', 'https://docs.tally.dev', '--title', 'Docs', ) const a = join(work, 'alice/tally') mkdirSync(a, { recursive: true }) git('alice', a, null, 'init', '-q', '-b', 'main') commit('alice', a, '2026-10-01T09:12:00Z', 'Start tally', { 'start/Cargo.toml': 'Cargo.toml', 'start/.gitignore': '.gitignore', 'start/README.md': 'README.md', 'start/main.rs': 'src/main.rs', }) commit('alice', a, '2026-10-02T14:40:00Z', 'Count words, lines and bytes', { 'count/lib.rs': 'src/lib.rs', 'count/main.rs': 'src/main.rs', }) commit( 'alice', a, '2026-10-03T08:05:00Z', '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 "-".', { 'stdin/main.rs': 'src/main.rs' }, ) // Written days before it was committed, as a rebase leaves one: it is // listed on the day it was committed, saying when it was written. commit( 'alice', a, '2026-10-05T17:21:00Z', 'Add a logo', { 'logo/logo.svg': 'assets/logo.svg' }, '2026-10-02T11:30:00Z', ) commit('alice', a, '2026-10-06T10:02:00Z', 'Document usage and output', { 'docs/README.md': 'README.md', 'docs/design.md': 'docs/design.md', }) git('alice', a, null, 'mv', 'docs/design.md', 'docs/DESIGN.md') commit('alice', a, '2026-10-08T11:47:00Z', 'Split parsing into its own module', { 'parse/parse.rs': 'src/parse.rs', 'parse/lib.rs': 'src/lib.rs', }) const parse = git('alice', a, null, 'rev-parse', 'HEAD') git('alice', a, null, 'push', '-q', tally, 'main') const notes = 'knotless://alice.test/notes' knotless('alice', 'repo', 'create', notes, '--description', 'Scratch notes 📝') const n = join(work, 'alice/notes') mkdirSync(n, { recursive: true }) git('alice', n, null, 'init', '-q', '-b', 'main') commit('alice', n, '2026-09-20T19:30:00Z', 'Begin', { 'notes/README.md': 'README.md' }) git('alice', n, null, 'push', '-q', notes, 'main') // A branch, then main moved past it: the feed lists only what main // brought that the branch had not. git('alice', n, null, 'checkout', '-q', '-b', 'reading') commit('alice', n, '2026-09-22T08:15:00Z', 'Start a reading list', { 'reading/reading.md': 'reading.md', }) git('alice', n, null, 'push', '-q', notes, 'reading') git('alice', n, null, 'checkout', '-q', 'main') git('alice', n, null, 'merge', '-q', '--ff-only', 'reading') commit('alice', n, '2026-09-23T21:40:00Z', 'Add Working in Public', { 'reading/more.md': 'reading.md', }) knotless('alice', 'repo', 'create', 'knotless://alice.test/scratch') git('alice', n, null, 'push', '-q', notes, 'main') const fork = knotless('bob', 'repo', 'fork', tally) // A star and a follow, for the counts the pages show. knotless('bob', 'repo', 'star', tally) knotless('bob', 'account', 'follow', 'alice.test') const b = join(work, 'bob/tally') git('bob', work, null, 'clone', '-q', fork, b) git('bob', b, null, 'checkout', '-q', '-b', 'empty-input') commit('bob', b, '2026-10-09T09:31:00Z', 'Count no lines in empty input', { 'fix/parse.rs': 'src/parse.rs', }) git('bob', b, null, 'push', '-q', 'origin', 'empty-input') git('bob', b, null, 'checkout', '-q', '-b', 'json', 'main') commit('bob', b, '2026-10-10T16:12:00Z', 'Print counts as JSON with --json', { 'json/main.rs': 'src/main.rs', }) git('bob', b, null, 'push', '-q', 'origin', 'json') // An issue the pull names to close, which its merge closes. const empty = knotless('bob', 'issue', 'create', tally, 'Counts one line for an empty file') const [, , issuer, , issueKey] = empty.split('/') const pull = knotless( 'bob', 'pull', 'create', tally, 'main', fork, 'empty-input', 'Count no lines in empty input', '-m', `\`tally\` printed one line for an empty file. \`lines\` now yields nothing when the text is empty, and a test covers it.\n\nFixes [the empty file's count](https://knotless.dev/alice.test/tally/issue/${issuer}/${issueKey}).`, ) knotless( 'alice', 'comment', 'add', tally, pull, 'Could `lines` check for emptiness once, rather than in the filter?', '--changes', ) knotless( 'bob', 'comment', 'add', tally, pull, 'It only runs per line, so the cost is one comparison each. Happy to change it if you prefer.', ) knotless('alice', 'comment', 'add', tally, pull, 'Fair enough. Thanks!', '--approve') // Merging and closing read the item from the index, which follows the // PDS a moment behind. await until( 'the pull', async () => (await page('/alice.test/tally/pulls')).includes('Count no lines in empty input') && (await page('/alice.test/tally/issues')).includes('Counts one line'), ) // Squashed into main, the merge's commit links back to the pull: bob // wrote it, and alice committed it later that day. knotless('alice', 'branch', 'protect', tally, 'main') const [merged] = run('alice', join(bin, 'knotless'), ['pull', 'merge', tally, pull], { when: '2026-10-09T15:00:00Z', }).split('\n') const json = knotless( 'bob', 'pull', 'create', tally, 'main', fork, 'json', 'Print counts as JSON with --json', '-m', 'For scripts. The object has `lines`, `words`, `bytes` and `name`.', ) const utf8 = knotless( 'bob', 'issue', 'create', tally, 'Fails on files that are not UTF-8', '-m', "Running `tally image.png` panics:\n\n```\nthread 'main' panicked at src/main.rs:8:56:\nstream did not contain valid UTF-8\n```\n\nwc counts bytes for any file.", ) const confirmed = knotless( 'alice', 'comment', 'add', tally, utf8, 'Confirmed. Reading bytes would fix it.', ) // Said again, better: the page says it was edited. knotless( 'alice', 'comment', 'edit', tally, confirmed, '-m', 'Confirmed. Reading bytes and counting words lossily would fix it.', ) // Labelled: one colored and described, one exclusive in its scope. knotless( 'alice', 'label', 'define', tally, 'kind/bug', '--color', '#d73a4a', '--description', 'Something is broken', ) knotless( 'alice', 'label', 'define', tally, 'priority/high', '--exclusive', '--color', '#fbca04', ) knotless('alice', 'label', 'define', tally, 'priority/low', '--exclusive') knotless('alice', 'issue', 'label', tally, utf8, 'kind/bug', 'priority/high') const typo = knotless('bob', 'issue', 'create', tally, 'Typo in the README ✏️') await until('the issue', async () => (await page('/alice.test/tally/issues')).includes('Typo in the README'), ) knotless('alice', 'issue', 'close', tally, typo) // Pulls into the fork, for its merge queue: one landed, which merging // fast-forwards, and four to queue. const change = (branch, when, message, path, text) => { git('bob', b, null, 'checkout', '-q', '-B', branch, 'main') mkdirSync(dirname(join(b, path)), { recursive: true }) writeFileSync(join(b, path), text) git('bob', b, when, 'add', '-A') git('bob', b, when, 'commit', '-q', '-m', message) git('bob', b, null, 'push', '-q', 'origin', branch) return knotless('bob', 'pull', 'create', fork, 'main', fork, branch, message) } const usage = change( 'usage', '2026-10-11T08:00:00Z', 'Show usage on --help', 'docs/usage.md', 'tally [--json] [FILE]...\n', ) await until('the usage pull', async () => (await page('/bob.test/tally/pulls')).includes('Show usage on --help'), ) const [landed] = knotless('bob', 'pull', 'merge', fork, usage).split('\n') git('bob', b, null, 'fetch', '-q', 'origin') git('bob', b, null, 'checkout', '-q', '-B', 'main', 'origin/main') const queued = [ change( 'bytes', '2026-10-11T09:00:00Z', 'Count bytes in files that are not UTF-8', 'docs/bytes.md', 'Bytes are counted as they are.\n', ), change( 'tabs', '2026-10-11T10:00:00Z', 'Line the columns up with tabs', 'docs/tabs.md', 'Columns are tab separated.\n', ), change( 'words', '2026-10-11T11:00:00Z', 'Count words in any script', 'docs/words.md', 'Words are split on Unicode white space.\n', ), ] const ejected = change( 'totals', '2026-10-11T12:00:00Z', 'Print a total for many files', 'docs/totals.md', 'A total follows the files.\n', ) // Each names itself the runner of its tally, whose checks are shown. knotless('alice', 'runner', 'add', tally, 'alice.test') knotless('bob', 'runner', 'add', fork, 'bob.test') // The fork's main takes pulls through its merge queue. knotless('bob', 'branch', 'protect', fork, 'main', '--merges', '--queue') // Releases of alice's tally: the command twice, a pre-release of it // after, and a server's image, as an OCI layout holds one. Bob's fork // publishes none. const shipped = git('alice', a, null, 'ls-remote', tally, 'refs/heads/main').split('\t')[0] const dist = join(work, 'dist') const built = (version) => ['aarch64-apple-darwin', 'x86_64-unknown-linux-musl'].map((target) => { const file = join(dist, version, `tally-${version}-${target}.tar.gz`) mkdirSync(dirname(file), { recursive: true }) writeFileSync(file, `tally ${version} for ${target}\n`.repeat(4096)) return file }) // Each a little after the last, as one write's records are ordered // by their keys, not their times. const publish = async (pkg, version, files, notes, ...more) => { knotless( 'alice', 'release', 'publish', tally, pkg, version, ...files, '--commit', shipped, '--notes', notes, ...more, ) await new Promise((r) => setTimeout(r, 100)) } await publish( 'tally', '0.1.0', built('0.1.0'), 'The first release: `tally` counts lines, words and bytes, as `wc` does.', ) await publish( 'tally', '0.1.1', built('0.1.1'), '### Fixed\n\n- An empty file counts no lines.\n- Input that is not UTF-8 is counted as bytes rather than failing.', ) const layout = join(dist, 'oci') mkdirSync(join(layout, 'blobs/sha256'), { recursive: true }) const blob = (bytes, mediaType) => { const hex = createHash('sha256').update(bytes).digest('hex') writeFileSync(join(layout, 'blobs/sha256', hex), bytes) return { mediaType, digest: `sha256:${hex}`, size: bytes.length } } const layer = blob( Buffer.from('tally-server layer\n'.repeat(2048)), 'application/vnd.oci.image.layer.v1.tar+gzip', ) const config = blob( Buffer.from( JSON.stringify({ architecture: 'amd64', os: 'linux', rootfs: { type: 'layers', diff_ids: [layer.digest] }, }), ), 'application/vnd.oci.image.config.v1+json', ) const manifest = blob( Buffer.from( JSON.stringify({ schemaVersion: 2, mediaType: 'application/vnd.oci.image.manifest.v1+json', config, layers: [layer], }), ), 'application/vnd.oci.image.manifest.v1+json', ) writeFileSync( join(layout, 'index.json'), JSON.stringify({ schemaVersion: 2, mediaType: 'application/vnd.oci.image.index.v1+json', manifests: [manifest], }), ) writeFileSync(join(layout, 'oci-layout'), '{"imageLayoutVersion":"1.0.0"}') await publish( 'tally-server', '0.1.0', [], 'Serves counts over HTTP: `POST /count` with the text, and the counts come back as JSON.', '--oci', layout, ) await publish( 'tally', '0.2.0-rc.1', built('0.2.0-rc.1'), 'A first look at 0.2.0: try it before it is the latest.\n\n- `tally --json` prints the counts as JSON.', ) // A workflow run only when asked, which the checks page offers to run: // the fork's main's last commit, after the branches made from it, so no // other's hash moves. git('bob', b, null, 'checkout', '-q', 'main') mkdirSync(join(b, '.knotless/workflows'), { recursive: true }) writeFileSync( join(b, '.knotless/workflows/release.yml'), '# Builds the release binaries for a tag and attaches them to it.\non: {asked: {refs: [refs/tags/*]}}\nsteps:\n - name: build\n run: cargo build --release\n', ) git('bob', b, '2026-10-20T12:00:00Z', 'add', '-A') git('bob', b, '2026-10-20T12:00:00Z', 'commit', '-q', '-m', 'Release from tags when asked') git('bob', b, null, 'push', '-q', 'origin', 'main') // Every record a fixed createdAt, keeping their order. const records = [] for (const who of ['alice', 'bob']) { const session = await fetch(`${net.pds}/xrpc/com.atproto.server.createSession`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identifier: account[who].handle, password: account[who].password }), }).then((r) => r.json()) const did = account[who].did if (who === 'alice') { const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${session.accessJwt}`, }, body: JSON.stringify({ repo: did, collection: 'app.bsky.actor.profile', rkey: 'self', record: { $type: 'app.bsky.actor.profile', displayName: people.alice.name, description: 'Small tools for text. Writes tally.', }, }), }) if (!res.ok) throw new Error(`profile: ${await res.text()}`) } const { collections } = await fetch( `${net.pds}/xrpc/com.atproto.repo.describeRepo?repo=${did}`, ).then((r) => r.json()) for (const collection of collections) { let cursor = '' do { const listed = await fetch( `${net.pds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=100${cursor}`, ).then((r) => r.json()) for (const r of listed.records) { if (typeof r.value.createdAt === 'string' && collection !== 'dev.knotless.ref') records.push({ ...r, who, did, collection, jwt: session.accessJwt }) } cursor = listed.cursor ? `&cursor=${listed.cursor}` : '' } while (cursor) } } // A new ref's push is its record's first version, which no claim // records: the index keeps it as a push with nothing before it, under // the key a claim on the generation before would have had, at the // time it was made. Each takes its place among the records as that // claim would, and its stamp is written into the index rather than the // PDS. The ref records themselves keep the times they were pushed at, // which no page shows. const index = new DatabaseSync(join(dir, 'index.db')) index.exec('PRAGMA busy_timeout = 10000') const who = Object.fromEntries(Object.entries(account).map(([name, a]) => [a.did, name])) for (const p of index .prepare('SELECT uri, did, created_at FROM push WHERE before IS NULL') .all()) { records.push({ uri: p.uri, who: who[p.did], did: p.did, collection: 'dev.knotless.claim', value: { createdAt: p.created_at }, pushed: true, }) } // 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 account, collection and key instead: by // the account's name, as its DID is new in every world. 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.who} ${r.collection} ${r.uri.split('/').pop()}` 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. const repoint = (v) => { if (Array.isArray(v)) return v.map(repoint) if (v && typeof v === 'object') { const out = Object.fromEntries(Object.entries(v).map(([k, x]) => [k, repoint(x)])) if (typeof out.uri === 'string' && typeof out.cid === 'string' && cids.has(out.uri)) out.cid = cids.get(out.uri) return out } return v } for (const [i, r] of records.entries()) { const created = start + i * 37 * 60_000 // One edited says so a little after it was made. const updated = r.value.updatedAt ? { updatedAt: new Date(created + 12 * 60_000).toISOString() } : {} if (r.pushed) { index .prepare('UPDATE push SET created_at = ? WHERE uri = ?') .run(new Date(created).toISOString(), r.uri) continue } const value = repoint({ ...r.value, createdAt: new Date(created).toISOString(), ...updated }) const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${r.jwt}` }, body: JSON.stringify({ repo: r.did, collection: r.collection, rkey: r.uri.split('/').pop(), record: value, }), }) if (!res.ok) throw new Error(`putRecord ${r.uri}: ${await res.text()}`) cids.set(r.uri, (await res.json()).cid) } // An organization, textkit, with bob a member in public and alice // invited. Written as records, dated as the rest are: joining asks // the organization's gateway, which this world has none of. Lapsing // goes by the clock, not the world's now, so alice's invitation runs // long enough to stay one. const put = async (who, collection, rkey, record) => { const session = await fetch(`${net.pds}/xrpc/com.atproto.server.createSession`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identifier: account[who].handle, password: account[who].password }), }).then((r) => r.json()) const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${session.accessJwt}`, }, body: JSON.stringify({ repo: account[who].did, collection, rkey, record: { $type: collection, ...record }, }), }) if (!res.ok) throw new Error(`putRecord ${who} ${collection}: ${await res.text()}`) } await put('textkit', 'app.bsky.actor.profile', 'self', { displayName: 'Textkit', description: 'Tools for text, made together.', }) await put('textkit', 'dev.knotless.org', 'self', { createdAt: '2026-10-12T09:00:00.000Z' }) await put('textkit', 'dev.knotless.orgMember', account.bob.did, { member: account.bob.did, roles: [{ role: 'admin' }], invitedAt: '2026-10-12T09:00:00.000Z', expiresAt: '2026-10-19T09:00:00.000Z', joinedAt: '2026-10-12T11:00:00.000Z', }) await put('textkit', 'dev.knotless.orgMember', account.alice.did, { member: account.alice.did, invitedAt: '2026-10-20T09:00:00.000Z', expiresAt: '2099-10-27T09:00:00.000Z', }) await put('bob', 'dev.knotless.orgAcceptance', account.textkit.did, { subject: account.textkit.did, createdAt: '2026-10-12T10:00:00.000Z', }) // Checks, as a runner reports them: of tally's main, and of both pulls' branches. const main = git('alice', a, null, 'ls-remote', tally, 'refs/heads/main').split('\t')[0] const tip = (branch) => git('bob', b, null, 'rev-parse', branch) const checks = [ ['alice', 'alice/tally', 'main', main, 'test', 'success', '2026-10-11T09:00:00Z'], ['alice', 'alice/tally', 'main', main, 'lint', 'success', '2026-10-11T09:00:00Z'], [ 'bob', 'bob/tally', 'empty-input', tip('empty-input'), 'test', 'success', '2026-10-09T09:32:00Z', ], ['bob', 'bob/tally', 'json', tip('json'), 'build', 'success', '2026-10-10T16:13:00Z'], ['bob', 'bob/tally', 'json', tip('json'), 'test', 'failure', '2026-10-10T16:13:00Z'], ['bob', 'bob/tally', 'json', tip('json'), 'lint', 'running', '2026-10-10T16:13:00Z'], // One the change left out, as the runner notes it. [ 'bob', 'bob/tally', 'json', tip('json'), 'docs', 'success', '2026-10-10T16:13:00Z', 'skipped: nothing changed in docs/**, assets/**, .knotless/workflows/docs.yml', ], ] // What each run did, step by step, and the log of the one that failed. const ran = { success: [ ['build', 'success', 0, 38400], ['test', 'success', 0, 2100], ], failure: [ ['build', 'success', 0, 41200], ['test', 'failure', 101, 3300], ], running: [], } const failed = [ '==> build', ' Compiling tally v0.1.0 (/work)', ' Finished `test` profile [unoptimized + debuginfo] target(s) in 41.17s', '==> test', ' Running unittests src/lib.rs (target/debug/deps/tally-3f9c2a1b)', '', 'running 4 tests', 'test tests::counts_bytes ... ok', 'test tests::counts_lines ... ok', 'test tests::counts_words ... ok', 'test tests::prints_json ... FAILED', '', 'failures:', '', '---- tests::prints_json stdout ----', "thread 'tests::prints_json' panicked at src/lib.rs:41:9:", 'assertion `left == right` failed', ' left: "{\"lines\":2,\"words\":3,\"bytes\":14}"', ' right: "{\"lines\":2,\"words\":3,\"bytes\":14,\"name\":\"-\"}"', '', 'test result: FAILED. 3 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s', '', ].join('\n') // What a failed shots run leaves: a page before and after a change, and // where they differ, drawn as a band that moved down. const band = (top) => (x, y) => y >= top && y < top + 24 && x >= 24 && x < 216 ? [60, 64, 72] : [244, 245, 247] const pictures = [ ['pull-desktop-light 1 before', band(40)], ['pull-desktop-light 2 after', band(72)], [ 'pull-desktop-light 3 difference', (x, y) => (band(40)(x, y)[0] !== band(72)(x, y)[0] ? [230, 40, 40] : [250, 250, 250]), ], ] const rkeys = {} for (const [who, repo, branch, commit, workflow, status, startedAt, skipped] of checks) { const [owner, name] = repo.split('/') const did = account[owner].did const { key } = await fetch( `${net.pds}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=dev.knotless.repo&rkey=${name}`, ) .then((r) => r.json()) .then((r) => r.value) const ref = `refs/heads/${branch}` const session = await fetch(`${net.pds}/xrpc/com.atproto.server.createSession`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identifier: account[who].handle, password: account[who].password }), }).then((r) => r.json()) const auth = { authorization: `Bearer ${session.accessJwt}` } const log = status === 'failure' ? await fetch(`${net.pds}/xrpc/com.atproto.repo.uploadBlob`, { method: 'POST', headers: { 'content-type': 'text/plain', ...auth }, body: failed, }) .then((r) => r.json()) .then((r) => ({ log: r.blob })) : {} const outputs = status === 'failure' ? { outputs: await Promise.all( pictures.map(async ([alt, draw]) => { const image = await fetch(`${net.pds}/xrpc/com.atproto.repo.uploadBlob`, { method: 'POST', headers: { 'content-type': 'image/png', ...auth }, body: png(240, 136, draw), }) .then((r) => r.json()) .then((r) => r.blob) return { $type: 'dev.knotless.check#image', image, alt } }), ), } : {} const steps = skipped ? [{ name: 'build', status: 'skipped', durationMs: 0 }] : ran[status].map(([name, status, exitCode, durationMs]) => ({ name, status, exitCode, durationMs, })) const took = skipped ? 0 : steps.reduce((ms, s) => ms + s.durationMs, 0) + 1000 const record = { $type: 'dev.knotless.check', repo: { uri: `at://${did}/dev.knotless.repo/${name}`, key }, ref, commit, workflow, status, startedAt, ...(status === 'running' ? {} : { finishedAt: new Date(Date.parse(startedAt) + took).toISOString() }), steps, ...(skipped ? { skipped } : {}), ...log, ...outputs, } const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, { method: 'POST', headers: { 'content-type': 'application/json', ...auth }, body: JSON.stringify({ repo: account[who].did, collection: 'dev.knotless.check', rkey: (rkeys[`${repo} ${workflow} ${status}`] = `${refHash(did)}.${key}.${refHash(ref)}.${commit}.${workflow}`), record, }), }) if (!res.ok) throw new Error(`check ${workflow}: ${await res.text()}`) } // The minutes each runner counts for its owner: alice nearing her // allowance, bob past his. for (const [who, minutes, frees] of [ ['alice', 1640, 310], ['bob', 2140, 400], ]) { const record = { $type: 'dev.knotless.minutes', owner: account[who].did, minutes, allowance: 2000, freeing: [{ at: freeing, minutes: frees }], updatedAt: '2026-10-25T17:52:00Z', } const session = await fetch(`${net.pds}/xrpc/com.atproto.server.createSession`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identifier: account[who].handle, password: account[who].password }), }).then((r) => r.json()) const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${session.accessJwt}`, }, body: JSON.stringify({ repo: account[who].did, collection: 'dev.knotless.minutes', rkey: account[who].did, record, }), }) if (!res.ok) throw new Error(`minutes ${who}: ${await res.text()}`) } // alice, the tally's runner, publishes a key its secrets are sealed to. // None is set: this server has no gateway for `knotless secret` to sign // for, so the settings draw the box empty, with its form. { const session = await fetch(`${net.pds}/xrpc/com.atproto.server.createSession`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identifier: account.alice.handle, password: account.alice.password, }), }).then((r) => r.json()) const record = { $type: 'dev.knotless.runnerKey', key: 'did:key:zDnaepwQjCS7ayRrLp2j3oFd8kfJ97mehqYTGFBNA3QqSsvNV', createdAt: '2026-10-25T17:52:00Z', } const res = await fetch(`${net.pds}/xrpc/com.atproto.repo.putRecord`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${session.accessJwt}`, }, body: JSON.stringify({ repo: account.alice.did, collection: 'dev.knotless.runnerKey', rkey: 'self', record, }), }) if (!res.ok) throw new Error(`runner key: ${await res.text()}`) } // The fork's merge queue, as bob, its owner and runner, keeps it: three // pulls queued, the first two in a batch being checked and the third // running its pre-flight; one ejected by the checks of the batch it was // in; and the batch that landed. { const did = account.bob.did const { key } = await fetch( `${net.pds}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=dev.knotless.repo&rkey=tally`, ) .then((r) => r.json()) .then((r) => r.value) const repo = { uri: `at://${did}/dev.knotless.repo/tally`, key } const session = await fetch(`${net.pds}/xrpc/com.atproto.server.createSession`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ identifier: account.bob.handle, password: account.bob.password }), }).then((r) => r.json()) const write = async (collection, record, rkey) => { const res = await fetch( `${net.pds}/xrpc/com.atproto.repo.${rkey ? 'putRecord' : 'createRecord'}`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${session.accessJwt}`, }, body: JSON.stringify({ repo: did, collection, ...(rkey ? { rkey } : {}), record: { $type: collection, ...record }, }), }, ) if (!res.ok) throw new Error(`${collection}: ${await res.text()}`) return (await res.json()).uri } const base = git('bob', b, null, 'rev-parse', `${landed}^`) const batch = (pulls, commit) => pulls.map((pull) => ({ pull, commit })) const runs = [ [ 'landed', [usage], landed, base, [ ['build', 'success'], ['test', 'success'], ], '2026-10-19T09:00:00Z', ], [ 'ejected', [ejected], tip('totals'), landed, [ ['build', 'success'], ['test', 'failure'], ], '2026-10-19T15:00:00Z', ], [ 'checking', queued.slice(0, 2), tip('tabs'), landed, [ ['build', 'success'], ['test', 'running'], ], '2026-10-20T10:00:00Z', ], ] const failed = [] for (const [name, pulls, commit, baseCommit, ran, startedAt] of runs) { for (const [workflow, status] of ran) { const rkey = `${refHash(did)}.${key}.${refHash('queue refs/heads/main')}.${commit}.${baseCommit}.${workflow}` const finished = status === 'running' ? {} : { finishedAt: new Date(Date.parse(startedAt) + 95_000).toISOString() } const uri = await write( 'dev.knotless.check', { repo, ref: 'refs/heads/main', commit, baseCommit, batch: batch(pulls, commit), workflow, status, startedAt, ...finished, steps: [], }, rkey, ) if (name === 'ejected' && status === 'failure') failed.push({ workflow, status, check: uri }) } } // Their pre-flights: the first two's passed, in six minutes each, and // the third's has run since a little before now. const flights = [ [queued[0], 'bytes', 'success', '2026-10-20T09:40:00Z'], [queued[1], 'tabs', 'success', '2026-10-20T09:42:00Z'], [queued[2], 'words', 'running', '2026-10-20T13:57:00Z'], ] for (const [pull, branch, status, startedAt] of flights) { const commit = tip(branch) const rkey = `${refHash(did)}.${key}.${refHash(pull)}.${commit}.${landed}.test` const finished = status === 'running' ? {} : { finishedAt: new Date(Date.parse(startedAt) + 360_000).toISOString() } await write( 'dev.knotless.check', { repo, ref: `refs/heads/${branch}`, commit, baseCommit: landed, pull, queue: 'first', workflow: 'test', status, startedAt, ...finished, steps: [], }, rkey, ) } for (const [i, pull] of [...queued, ejected].entries()) { await write('dev.knotless.queue', { repo, subject: pull, state: 'queued', createdAt: `2026-10-1${5 + i}T09:30:00.000Z`, }) } await write('dev.knotless.queue', { repo, subject: ejected, state: 'ejected', commit: tip('totals'), checks: failed, reason: `its batch failed its checks at ${tip('totals').slice(0, 12)}, on main at ${landed.slice(0, 12)}: test (failure)`, createdAt: '2026-10-19T15:02:00.000Z', }) } const item = (kind, uri) => { const [, , did, , rkey] = uri.split('/') return `/alice.test/tally/${kind}/${did}/${rkey}` } const views = { home: '/', search: '/search?q=t', code: '/search?type=code&q=count', 'code-repo': '/search?type=code&q=count+repo:alice.test/tally', start: '/start', terms: '/terms', privacy: '/privacy', guidelines: '/guidelines', stats: '/stats', profile: '/alice.test', 'profile-fork': '/bob.test', 'profile-repos': '/alice.test?tab=repos', 'profile-activity': '/alice.test?tab=activity', org: '/textkit.test', 'org-members': '/textkit.test?tab=members', repository: '/alice.test/tally', 'repository-watch': '/alice.test/tally', directory: '/alice.test/tally/tree/main/src', file: '/alice.test/tally/blob/main/src/lib.rs', blame: '/alice.test/tally/blame/main/src/lib.rs', markdown: '/alice.test/tally/blob/main/README.md', 'markdown-plain': '/alice.test/tally/blob/main/README.md?plain=1', diagram: '/alice.test/tally/blob/main/docs/DESIGN.md', image: '/alice.test/tally/blob/main/assets/logo.svg', commits: '/alice.test/tally/commits/main', commit: `/alice.test/tally/commit/${parse}`, 'commit-merged': `/alice.test/tally/commit/${merged}`, pulls: '/alice.test/tally/pulls', 'pulls-closed': '/alice.test/tally/pulls?state=closed', pull: item('pull', pull), 'pull-open': item('pull', json), 'pull-files': `${item('pull', pull)}/files`, issues: '/alice.test/tally/issues', 'issues-closed': '/alice.test/tally/issues?state=closed', issue: item('issue', utf8), 'issue-subscribe': item('issue', utf8), 'issue-closed': item('issue', typo), 'issue-fixed': item('issue', empty), fork: '/bob.test/tally', empty: '/alice.test/scratch', missing: '/alice.test/tally/blob/main/nowhere', check: `/bob.test/tally/check/${account.bob.did}/${rkeys['bob/tally test failure']}`, queue: '/bob.test/tally/queue', checks: '/bob.test/tally/checks', 'pull-queued': item('pull', queued[0]).replace('/alice.test/', '/bob.test/'), 'pull-ejected': item('pull', ejected).replace('/alice.test/', '/bob.test/'), settings: '/alice.test/tally/settings', 'settings-branches': '/alice.test/tally/settings/branches', 'settings-labels': '/alice.test/tally/settings/labels', 'settings-access': '/alice.test/tally/settings/access', 'settings-actions': '/alice.test/tally/settings/actions', releases: '/alice.test/tally/releases', 'releases-image': '/alice.test/tally/releases?package=tally-server', 'releases-empty': '/bob.test/tally/releases', notifications: '/notifications', } // The pages whose social cards are shot, by the names of the shots. const cards = { home: '/', repository: '/alice.test/tally', notes: '/alice.test/notes', pull: views.pull, 'pull-open': views['pull-open'], issue: views.issue, 'issue-closed': item('issue', typo), } // The index has caught up when no page shows today's date, every // record now being dated before it, and the merge shows. The day // runner minutes free up on is a date the pages show too, and is left // out, as on that day it is today's. const day = (t) => new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC', }) const today = day(Date.now()) // What still holds the wait back, said if it times out. let missing = [] const shown = [ 'Merged as', 'Closed by', '2 Closed', 'Forked from', '1 Closed', '2/2', 'prints_json', 'In a batch of 2 being checked', 'Landed', 'Runs only when asked', 'tally-server', 'Pre-release', ] // Each tally's default branch, which its code is found at once indexed. const tips = () => ['alice.test', 'bob.test'].map( (o) => run('alice', 'git', ['ls-remote', `knotless://${o}/tally`, 'refs/heads/main']).split( /\s/, )[0], ) const indexed = (html) => tips().every((tip) => html.includes(`/blob/${tip}/`)) const caughtUp = async () => { const names = Object.keys(views) const pages = await Promise.all(Object.values(views).map(page)) const all = pages.join('') const dated = names.filter((_, i) => pages[i].replaceAll(day(freeing), '').includes(today)) missing = [ ...dated.map((v) => { const at = pages[names.indexOf(v)].replaceAll(day(freeing), '').indexOf(today) return `today's date on ${v}: ${JSON.stringify(pages[names.indexOf(v)].slice(Math.max(0, at - 160), at + 40))}` }), ...shown.filter((t) => !all.includes(t)), ...(all.includes('Some checks failed') || all.includes('Some checks were not successful') ? [] : ['Some checks failed']), ...(/Ejected