# W29 — What the board says while it is working

Three W28 surfaces came back from the Operator's read. All three failed the same way: the
code did what its test said and the *board* still did not do what the Operator needed. Plus
#35, which is #15's other half and arrives with it.

## A. #30 FAILED — the card image is unreachable to Discord

Testbed **#37**'s card carries no image. The embed was set; Discord dropped it. Measured:

```
curl -sI https://github.com/BigscreenVR/alchemy-testbed/releases/download/untagged-…/…png
→ 404
```

The attachments release is a **draft release on a private repo**. Its `browser_download_url`
resolves only for an authenticated caller, and Discord's image proxy is not one. So every
image alchemy has ever "carried" is invisible to the surface it was carried for. The unit
test passed because it asserted the field was set, which was never the question — the same
mistake class as v0.4.2 and W26, one layer further out.

**The image has to arrive on Discord's own CDN, uploaded by us.** The supported, non-expiring
way is `attachment://<name>`: the card message carries the bytes and the embed references
them by filename. A Discord CDN URL harvested any other way is signed and expires.

- **T1 — impl.** `BotCard::image` becomes a name + source pair. The serenity port fetches the
  blob authenticated and posts/edits the card with the attachment, embed image
  `attachment://<name>`.
- **T2 — the drift trap.** Discord rewrites the image URL to its own CDN, so reading a card
  back and comparing URLs would diff **every cycle** — a re-upload per cycle per card, which
  is worse than the bug. Equality is on the **filename only**, which survives the rewrite.
  The zero-mutation bar is the test that proves it.
- **T3 — the `!` leak.** #37's card body reads `!chrome_wS4uyJHZtV.png` — Discord renders no
  markdown images, so #28's `!` shows as a literal bang in the mirrored body. The card's
  description drops it; the GitHub body keeps it, which is where it does work.

## B. #16 — the receipt lives 12 hours, so it cannot be a timer

The Operator wants the mint receipt to last **12 hours**, not 120 seconds. The constant is
the easy half. The hard half: a 12-hour `tokio::spawn` sleep does not survive a daemon
restart, and a restart inside 12 hours is likely, not exotic. Every restart would strand a
receipt in the channel permanently — a standing violation of *"a Project's channel holds only
un-promoted Seeds"*, which is the whole reason the receipt expires at all.

- **T4 — impl.** Receipt cleanup becomes **reconcile-owned**, like everything else here: each
  cycle, the channel's own bot messages are scanned for the receipt shape and any older than
  the TTL is deleted. The age comes from the message snowflake, so nothing is remembered
  between cycles and a restart costs at most one cycle of lateness. The in-process timer goes.
- **T5 — unit.** A receipt past the TTL is swept, one inside it is not, a non-receipt bot
  message is never touched, and a swept channel converges to zero mutations.

## C. #15 — the mark comes off before the work is done

The mark cleared when the **GitHub write** returned. What the Operator watches for is the
**post** changing, which happens later, on the reconcile. So the marker flashes and vanishes
while the card still shows the old text — it reports the wrong thing finishing.

- **T6 — impl.** The Edit path marks, writes, **awaits the refresh that re-renders the card**,
  and only then clears. The mark now spans the interval it claims to.
- **T7 — unit.** The mark is still present while the refresh runs and gone after it; the
  failure path still clears it (a failed edit must never leave the board spinning).

## D. #35 — retire the ephemeral post-update messages

With the mark telling the truth, the `#42 updated (refreshing)` ephemeral is a second, worse
answer to the same question. It goes — **except on failure.** Discord requires an interaction
response, and silence on a failed write would be a silent loss, which this project does not
ship. Success acknowledges and says nothing; failure still says what broke.

- **T8 — impl.** Edit-modal and button submits acknowledge without a message on success;
  the ephemeral survives as the error path only.
- **T9 — unit.** The decision is a pure function of the write's outcome, so it tests without
  a gateway.

## Gate

`cargo test` green, `traceable-reqs check` exit 0, ship through `spt adapter update alchemy`.

**Acceptance is the Operator's card read, not my test run** — #30 is the standing proof that
a green assertion about a field says nothing about what Discord renders. #30 in particular
closes on a testbed Request with an image whose picture is *visible*, nothing less.
