Skip to content

The Bench, M2: answer a card by tapping a number, and the board says how - #26

Merged
QuicksilverSlick merged 8 commits into
mainfrom
feat/bench-m2
Sep 16, 2026
Merged

QuicksilverSlick merged 8 commits into
mainfrom
feat/bench-m2

Conversation

@QuicksilverSlick

Copy link
Copy Markdown
Owner

What this is

M2 of the Bench: the owner answers a card by tapping a number under it, and the Bench records the answer. It also makes the Bench explain itself, because nothing on M1's board or cards said how to answer.

Answering

  • Each 2-second tick sends one /query for kind 7 reactions on the live cards, from the owner and any members.txt key. The answer set is re-derived from scratch each time, so a removed reaction simply stops counting.
  • The latest valid reaction wins, per the owner's decision. Valid means signed by an approver, the last e tag is the current card, and the content exactly matches one of the seeded keycaps. Foreign keys, the Bench's own seeds, other emoji and stale cards are ignored.
  • A recorded answer edits the card in place (kind 40003) to end with Recorded: 2. Move · HH:MM. It also writes ~/.dreamforge/bench/outbox/<writer>/<id>.json with the signed reaction for the session that asked. Undo reverses both.
  • Answered cards stay 24 h, then retire without a repost. Past-deadline pending items fold. A tick that starts failing raises one toast.

Self-explanatory

  • Board line 2: To answer: tap one number under a card. To undo or change it, tap that number again first.
  • "Needs you" lines are bullets, not numbers, so they no longer look like the answer numbers. Answered lines keep the card link.
  • Unanswered pending cards say Tap one number below to answer.
  • CARD_FORMAT = 2 reposts cards from older builds once, without pinging the phone again. Cards that are already answered keep their answer, event id and outbox file.

Verified

  • bench:: 55 passed, 0 failed. egress_guard 18 passed. No clippy diagnostics under bench/. fmt --check and the file-size ratchet are clean.
  • Mutation proofs, each caught by at least one test: oldest instead of latest; approvers check removed; loose emoji match; outbox not removed on retraction; legend removed; format refresh never fires; the refresh clearing a recorded answer; the refresh retiring an answered older-format card.
  • Built from the verified M2 spec by sequential build agents, then three review rounds with fixes and two independent verifiers.

Still to check on the hosted relay (needs an installed build)

  • Whether Block's hosted relay accepts the writer's 40003 edit on a card. If it refuses, the tick stops at the Recorded edit with one toast, and the fix is a design choice.
  • Whether the phone's keycap reaction echoes the seed byte for byte, including U+FE0F.
  • That #h plus #e scoping of kind 7 on HTTP /query behaves like this fork's relay.

🤖 Generated with Claude Code

QuicksilverSlick and others added 8 commits September 16, 2026 11:29
Item gains an Answer (reactionId, option, answeredAt) and a recorded
marker, both serde-defaulted so an M1 state.json loads unchanged.
pick_answer derives the answer from the approvers' kind-7 reactions on
the live card: latest valid tap wins (owner decision; first-wins is
reserved for tool approvals later). render_card appends a Recorded
trailer through plain_title; render_board takes the clock, moves
answered items out of Needs you into an Answered section and flags
them after 24 h. Tests live in bench/tests/answers.rs so tests.rs stays
under the 1500-line ratchet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…piry

Split query()/query_writer so the Bench can read the approvers' kind-7
reactions on its live cards; poll_answers re-derives every answer from
that set each tick (latest valid reaction wins, a retraction is one that
stopped coming back), writes outbox/<writer>/<id>.json before state
records the answer, and only ever considers a card at the item's
current hash so a stale card awaiting delete never re-supplies an
answer. expire() folds a past-deadline pending item. The fake relay
logs every /query body so tests can assert the filter shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cleanup

publish (a) leaves answered items out of the wanted cards and counts the
kept answered cards against MAX_CARDS. New (a2), run before any card
churn, edits the live card to show or drop its Recorded trailer, driven by
the difference between Item.recorded and the answer; a refused edit on a
hand-deleted card drops the card and keeps the answer. (b) exempts an
answered card from the not-wanted delete for ANSWER_KEEP_SECS, then
retires it with card and recorded reset, never to be reposted. intake
removes the outbox file on the hash-change re-drop that acks an answer.
The 40003 tag boilerplate moves into edit(), next to delete().

Tests: owner_tap_records_answer_and_writes_outbox now asserts the card
Recorded edit ahead of the board edit and `recorded`;
retraction_unrecords_in_place_and_latest_wins,
answered_card_kept_24h_then_retired_never_reposted,
recorded_edit_not_found_drops_card_keeps_answer and
ack_by_redrop_clears_answer_and_outbox are new.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
serve() raises a single tauri_plugin_notification toast titled Bench
with the tick error on the last_error None -> Some edge; an Ok tick
re-arms it, so a relay outage is one toast, not one per 2 s tick.
No Ctx or Cargo change: the plugin is already registered in lib.rs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- poll_answers only skips when the same reaction is already recorded, so
  the None arm runs every tick: a failed remove (Windows sharing
  violation) or a crash before save_state no longer leaves a withdrawn
  answer in outbox/ for the writer to act on. The ack re-drop sweeps the
  file unconditionally for the same reason.
- poll_answers iterates items in place (no key clone, no second lookup).
- Tests: retracted_answer_file_is_swept_when_state_forgot,
  truncated_answers_are_reported, kept_answered_cards_count_against_max_cards.
- Not changed: a hard (a2) edit error still aborts the tick, as in M1 for
  every relay refusal; swallowing it would hide the failure from lastError
  and the toast. Build step 6 on the hosted relay decides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The board gets a one-line legend under its header and bullets instead of ordinals under Needs you, so its lines no longer read like option keycaps. Unanswered pending cards get one line above 'as of' saying to tap a number below; answered cards keep only the Recorded trailer.

CARD_FORMAT (2) is folded into the card version, not into the Drop hash: cards are posted and tagged at '<Drop hash>.<CARD_FORMAT>'. Publish (b) and orphan adoption compare against that, so every live unanswered card from an older build is deleted and reposted once, and a relaunch reads the new tag and reposts nothing. item.hash is untouched, so an identical re-drop is still a no-op and never clears an answer or the outbox file. live_card matches on the Drop part only, so an answered older-format card keeps its answer, keeps taking late pills and keeps its 24 h keep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r board

A Recorded edit re-renders the whole card, so it now stamps card.hash with the current card format: undo (or an empty poll) on a card from an older build un-records it in place instead of (b) deleting it the same tick. Cards retired while showing the same Drop (format refresh, overflow, hand-deleted, recovered orphan) repost without the owner p tag via a persisted Item.quiet_repost, so the upgrade does not burst the phone.

Board: blank line before each section so the desktop stops folding section names into the previous bullet; answered lines link their kept card; header reads 'needs you: N · answered: M'; legend and card hint say to tap one number and to untap before changing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…refresh

Covers the verifier's surviving mutation: publish (b) retiring answered cards on a card-format mismatch. The test fails under that mutation and passes on the real code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@QuicksilverSlick
QuicksilverSlick merged commit 36e4d19 into main Sep 16, 2026
105 of 108 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant