Experimental Bluesky client for agents
perch docs FOLLOWING-ACCEPTANCE.md
9.0 kB
Markdown
at main

Following-feed acceptance — 2026-09-05 #

The feature in FOLLOWING.md was exercised through real authenticated timeline reads, the actual CLI, immutable SQLite reading positions, browser-produced PNGs, independent raw-source comparisons, and focused regressions. This extends the first delivery at eb02964; it does not replace the thread reader or add social writes.

Final integrated checks #

Run from the repository:

bun test
bun run typecheck

Final result: 47 tests passed, 0 failed, 546 assertions across nine files; strict TypeScript checking passed. The first integrated compiler run exposed two missing discriminant/optional-value narrowings; Core fixed them and the final checks were rerun. The final CSS reference-contrast repair is included in these results. Sols ran only their owned targeted tests; Main ran the integrated suite and typecheck after source barriers settled.

Requirement evidence #

Contract Exercised behavior
F1 Real following --auth --limit 20 returned an authenticated Liv-view snapshot. Missing --auth failed without moving the existing position. Focused tests cover public-reader rejection, valid bounds, invalid bounds before requests, and unusable login credentials.
F2 The raw API specimen requested 20 and returned 19 entries. Main independently compared all 19 normalized occurrences against original source order, canonical post URI, exact text including empty text, repost actor/reason, parent URI, and likes/reposts/replies. Duplicate occurrences, root/parent hydration, raw quotes, unknown counts/reasons, malformed records/media and the primary-first 600-node cap have focused coverage.
F3 Main forked a feed, opened a repost occurrence into its real thread, and checked exact origin view/occurrence/post/repost attribution in the encounter breadcrumb. The parent session remained unchanged. back returned byte-identical original JSON; local occurrence peek remained stable. Ordinary shared links did not fabricate encounter metadata.
F4 Real more --auth used the original opaque cursor, produced a separate 18-entry page, and retained the old occurrence ref. back restored the original page. refresh --auth --limit 7 produced a newest page with no request cursor; back again restored the original. Tests cover no-cursor/no-request, unchanged-cursor rejection, failures, stale CAS, and viewer/service mismatch before timeline continuation.
F5 Actual reply-heavy timelines were rendered and inspected at native reading scale, not just synthetic cards. People and post bodies lead; parent/quote context is shallow, images remain contained and uncropped, media-only posts do not acquire an empty-text warning box, and long original text is explicitly recoverable from disclosure/text/JSON.
F6 The original specimen included Andrea reposting Damon Young's tablet-mail post at 3,327 likes, 920 reposts and 87 replies. Later polls included another image repost with 936 likes and 131 reposts. Compact visible engagement retained these differences without sorting or enlarging popular cards. Exact counts remain in the paired source.
F7 Repeated real mixed feeds rendered approximately 3–5 complete cards per panel, with 20 entries across five panels in two later samples. Main independently measured 20px post body and 16px context-glimpse text in Chromium, zero horizontal overflow, no initially open disclosures, and no computed uppercasing of reference leaves. Repost-reference contrast was separately corrected and verified.

Main took an actual share URL from a source facet, rather than substituting an AT URI:

https://bsky.app/profile/thewaether.bsky.social/post/3murkp2raps27

open selected at://did:plc:ifcbmerblqzkochthw722ihk/app.bsky.feed.post/3murkp2raps27, independently identified from the native quoted record. It retained the originally requested URL and returned a public thread with ten nodes. Opening the same URL as a detour from a forked feed and then going back restored the exact feed and refs. Martin can hand Liv an ordinary link in conversation; no AT-URI translation or new harness integration is needed.

Independent review and repairs #

Core's independent Luna falsifier rechecked the final source and reported no surviving concrete F1–F4/I1–I4 violation. Concrete repairs were:

  • Error redaction now retains config credentials and every session token observed through the SDK lifecycle, including superseded tokens after refresh. It merges overlapping matches against the original error string rather than destroying longer matches through sequential replacement. A real SDK synthetic 401 → refresh → retried 503 scenario exercises the boundary.
  • Empty access/refresh credentials cannot label a request authenticated. The following path checks a usable same-viewer session before dispatch.
  • Empty cursors normalize to unavailable with an explicit warning, rather than disagreeing with the renderer about whether more is possible.
  • Malformed image-array entries produce unsupported-data cues rather than disappearing silently.
  • A shared canonical single-record URI boundary rejects extra path segments, reserved record keys, disallowed characters and keys over 512 characters. The small local key guard is grounded in the installed AT Protocol syntax rules; no undeclared dependency or new auth protocol was added.

Two proposed changes were rejected after checking the installed schemas: absent optional relationship URIs inside a supplied authenticated ViewerState are not the same as missing viewer metadata; and PostView.record must not be blended with a nonstandard shadow value because it looks richer. Valid quote ViewRecord.value follows its own supported path.

Visual review identified warning overflow and missing hydrated-root disclosure; the renderer now retains that context in its one closed exact-source disclosure without restoring a large visible root card. The first visual falsifier supplied those two findings, then stalled during terminal recheck and was aborted; a fresh bounded read-only Luna independently checked the final root-disclosure, warning-wrap and reference-styling seams and returned a terminal empty finding list. Its evidence is source-only; the actual browser/regression evidence is recorded separately here. Main independently rejected the initial density and caught uppercased context references and low-contrast repost-actor references. Final contrast verification on the durable feed found all four repost-actor badges using the established light foreground rgb(255, 250, 240) on rgb(23, 33, 43), with text-transform: none. The corrected capture retained byte-identical source JSON.

Durable reading position and artifacts #

The existing default database now has:

  • main → v1, unchanged burger conversation.
  • clarification → v2, unchanged independent branch, with its original history.
  • following → v3, a newly fetched authenticated feed with 20 occurrences, 43 canonical/context nodes, four repost occurrences and thirteen replies.

The final durable feed capture is in ignored .perch/following/: v3.html, v3.txt, v3.json, v3-overview.png, and v3-panel-01.png through v3-panel-05.png. Main inspected the actual first durable panel and independently verified the final reference repair in Chromium. The harness browser could not navigate the host file URI, so its DOM check loaded the same generated HTML with page.setContent; Perch's own Playwright capture produced the actual PNGs normally.

Additional ignored evidence includes .perch/following-specimen.json, .perch/following-source-comparison.json, .perch/following-acceptance-1788624919513.sqlite, and the Visual lead's fresh captures under .perch/feed-visual-fresh/panels-final/. These are local authenticated reading artifacts, not committed fixtures.

bun run perch show --session following
bun run perch render --session following --out .perch/following
bun run perch more --auth --session following
bun run perch refresh --auth --session following

Boundaries and remaining choices #

No post, like, follow, quote, notification handling, autonomous polling daemon, push, deployment, existing-server change or harness/plugin configuration change was made. Polling during acceptance was deliberate read-only sampling. Remote timeline cursors do not make the changing network transactionally frozen; stored local pages are immutable, and continuation failures are explicit. Counts and relative ages describe their captured page reference, not a live ticker.

Custom feeds/search, posting surfaces, notification reading, custom pronoun/labeler hydration, arbitrary article bodies, authentication caching and a Liv-oriented reader-agent definition remain separate choices. No universal visual-perception benchmark or complete-network/thread guarantee is claimed.