# W34 — Targets

Fourth wave of **Milestone #38** (*the control surface*), and the first half of Request
**#10**. Two properties land: the **🎯 Target reaction** on Backlog and Eval cards, and the
**staging post** `/new-milestone` and `/pick-milestone` open. Assignment itself is W35.

W33 shipped as **v0.10.0** and is closed by the installed binary. W32's #11 and #18 still
await the Operator's board read — they are not Done, and nothing here changes that.

Activates `REQ-TARGET-REACTION`, `REQ-MILESTONE-STAGING`,
`REQ-HAZARD-TARGET-RELOCATION-CARRY`.

## The platform fact that reshapes the carry (measured, not assumed)

`REQ-HAZARD-TARGET-RELOCATION-CARRY` as written says the relocation "carries its Target
reactions onto the new card". **Discord cannot do that.** A bot may only create a reaction
*as itself*: serenity 0.12.5's `Http::create_reaction` takes no user id and routes to
`Route::ChannelMessageReactionMe` — the `PUT …/reactions/{emoji}/@me` endpoint. There is no
add-as-user route to call. Measured in the dependency this daemon links, the same way the
select-in-modal constraint at the top of `M38-PLAN.md` was measured.

The asymmetry that saves the wave: **removal as another user IS available** —
`Http::delete_reaction(channel, message, user_id, emoji)` (`MANAGE_MESSAGES`). So the store
can be *cleared* on a member's behalf, just never *restored*.

**Ruling (mine, DRI):** the carry rides the **new card**, not the new message's reactions.
A relocation composes the destination card with a machine-readable `🎯 carried:` line naming
the targeting Alchemists as `<@id>` mentions, and the read side is the **union** of the
message's live 🎯 reactors and that line. Nothing is remembered between cycles — Discord
still holds the whole state, the card is as much a Discord artifact as the reaction is, and
the daemon keeps no store. What changes is only *which* Discord field holds it after a move.

This diverges from `CONTEXT.md`'s Target entry — "the reactions themselves are the store" —
so it is **filed, not absorbed**: the divergence goes to the Operator (`mobile-gw`) in the
same breath as this plan, and `CONTEXT.md` gains the constraint and its consequence as the
`doc` stage of the hazard. The property CONTEXT.md was protecting (no daemon memory between
cycles) survives intact; the mechanism it named does not exist.

### The un-target hole the carry opens, and its close

A carried target lives in a card line, so its Alchemist has no reaction of their own to
remove — an accidental target could never be dropped, only consumed. So the gesture is a
**toggle**, and the state machine per (card, Alchemist) is derived wholly from Discord:

| live 🎯 reaction | carried on card | meaning | daemon action |
|---|---|---|---|
| yes | no | targeted (the ordinary shape) | none |
| no | yes | targeted, carried across a relocation | none |
| yes | yes | **un-target** — the only way to say it | remove that user's reaction, drop the id |
| no | no | not targeted | none |

Converged, every row is "none": a settled board makes **zero** mutations, which is the
`REQ-HAZARD-RECONCILE-IDEMPOTENT` bar this wave must not breach.

## The rulings this wave inherits — do not re-derive them

1. **Targets exist only in Backlog and Eval** (`CONTEXT.md`, Target). The seed goes on bot
   cards in those two threads and nowhere else.
2. **Per-Alchemist, non-Alchemists inert** (`CONTEXT.md`, Target) — exactly as on a Seed:
   a non-Alchemist's 🎯 is *counted by nobody and removed by nobody*. The roster is
   `role_members(alchemist_role_name(entry.role_key))`, the stable key, never the display
   name.
3. **A relocation OUT of the targetable zone drops the target and says so.** Carrying a
   target into Greenlit is meaningless — Greenlit is Milestone-owned — but dropping it in
   silence is the loss this project refuses to ship, so the drop posts one Updates note
   naming the Request and the Alchemists whose target went with it. No note when there
   were no targets.
4. **Read cost is bounded by the resting shape.** A message's own fetch already carries its
   reaction counts (`count` + `me`), so the per-card `get_reaction_users` call fires **only
   where the 🎯 count exceeds one** — i.e. where a human has reacted. On a settled board
   every card sits at count 1 (the seed) and the cycle makes **zero** extra API calls. A
   design that polled reactors per card per cycle would rate-limit a hundred-card Backlog.
5. **Finalize is W35.** The staging post's rail carries Finalize *and* Cancel because the
   post's contract says so, and until W35 Finalize answers an ephemeral refusal naming what
   is missing. A button that silently does nothing is the failure mode; a button that says
   "not yet" is honest and bounded to one wave.

## Tasks

- **T1 — doc, first.** `CONTEXT.md`: amend the **Target** entry with the platform constraint
  and the union/toggle consequence (`[doc->REQ-TARGET-REACTION]`,
  `[doc->REQ-HAZARD-TARGET-RELOCATION-CARRY]`), and add the staging-post paragraph
  (`[doc->REQ-MILESTONE-STAGING]`). `docs/KNOWN-HAZARDS.md` gains the relocation-carry entry
  bound to the existing `REQ-HAZARD-TARGET-RELOCATION-CARRY` id — **one id per property**,
  the discipline W33's 1.3a set. `traceable-reqs.toml`: activate the three ids' stages.

- **T2 — the port surface.** `PinnedMessage` gains `reactions: Vec<ReactionFact>`
  (`{emoji, count, me}`) — free with the message fetch, and the thing ruling 4 rests on.
  `DiscordPort` gains `reaction_users(channel, message, emoji) -> Vec<u64>` and
  `remove_user_reaction(channel, message, user, emoji)`. `FakeGuild` grows per-user
  reactions (today it stores only the bot's own), and every existing call site keeps
  compiling.

- **T3 — the decision core** (`src/daemon/targets.rs`, pure, no gateway). `TARGET_EMOJI`;
  render/parse of the `🎯 carried:` line; `targets_of(card, reactions, roster)` returning the
  union filtered to Alchemists; `resolve(…)` returning the toggle's clears; and the
  relocation composer that folds a dying card's union into the destination card. Unit tests
  are the whole table above, **including the settled row asserting no action** —
  and per the standing method note, a test that replays the same input twice.

- **T4 — reconcile wiring.** Seed 🎯 on every bot card in Backlog and Eval that lacks it
  (`me == false`); carry across the relocation branch's delete+repost; drop-with-a-note on
  a move out of the zone; apply the toggle's removals. The zero-mutation assertion over a
  settled channel is the gate this task cannot pass without.

- **T5 — the staging post.** Composer (one line per Target: a Discord jump link and a GitHub
  link, machine-generated text halves, title as a `plain_tail`), a recognizer, and the
  invoker-keyed marker so a re-invoke **edits** rather than duplicates. Pinned in Backlog,
  refreshed by the cycle. `/new-milestone` and `/pick-milestone` registered beside
  `/alchemist`; non-Alchemist refused ephemerally; `/pick-milestone` refused ephemerally
  when no Milestone sits in Backlog or Eval. Cancel deletes the post and leaves Targets
  alone.
  **Trap to close in this task:** `scan_channel` classifies any bot post in a State thread
  with no parseable ref as a **loud orphan**. The staging post is exactly that shape and
  must be recognized *before* that branch, or every cycle shouts about it.

- **T6 — gate and ship.** `cargo test` green, `./traceable-reqs.exe check` exit 0, then the
  release per `docs/RELEASE-RUNBOOK.md` (DRI authority — no ask), `spt adapter update
  alchemy`, and daemon process-start compared against exe mtime. Then the live read: the 🎯
  seeds appear on Backlog and Eval, a state change carries a target, and the `<@id>` mention
  renders as a name in the embed description — the one rendering claim in this plan that
  Discord, not the test suite, has to confirm.

## Gate

`cargo test` green · `./traceable-reqs.exe check` exit 0 · verified against the **installed
binary and the live board**, never a green assertion alone. The wave's board read (#10's
first half) is owed to the Operator and is not mine to declare.
