# W24 — Masked-link text escaping (Request #6)

> **SUPERSEDED by `W26-PLAN.md` (2026-07-26).** This wave's premise — "Discord honors
> backslash escapes, so the title renders as written" — was never proven on the live
> surface and is false. Testbed proofs #30 and #32 rendered LITERAL with the escapes
> visible; #33 (a title with no symbols) rendered as one clickable link. Escaping the text
> half is what breaks the construct. Shipped in v0.4.2, repaired in v0.4.3 by keeping
> Operator text out of the text half entirely. `REQ-LINK-TEXT-ESCAPE` is retired and
> replaced by `REQ-LINK-TEXT-MACHINE`. Kept as the record of how the wrong fix got in:
> every gate was green because the tests asserted what alchemy emits, and the subject of
> the bug was how another system renders it.

**Scope:** one bug, one requirement, one shared helper. Ships alongside W23 so the
install dance is paid once.

**Gate:** `cargo build` + `cargo test` green, `traceable-reqs check` exit 0, live proof
on Discord (a Request whose title carries a markdown link renders as one clean link).

## The defect

The daemon writes Discord **masked links** — `[text](url)` — in five places, and the
`text` half is always Operator-authored, untrusted content:

| site | shape |
|---|---|
| `capture.rs:97` `seed_entry_content` | `- [<seed first line>](<jump url>)` |
| `capture.rs:133` `update_note_capture` | `🌱 [<title>](<mirror url>)` |
| `capture.rs:139` `update_note_mint` | `[#N \| <title>](<card url>)` |
| `botpost.rs:202` milestone sub-issue line | `- [#N \| <title>](<issue url>)` |
| `reconcile.rs:271` state-change note | `[#N \| <title>](<card url>)` |

A `]` anywhere in the title closes the bracket early: the remainder of the line, URL
included, spills out as literal text. A title that carries its *own* markdown link —
`](` — breaks it completely. This repo's own Request #5 is exactly that shape:

```
add: `# [<Owner>/<repo>](https://github.com/...)` as TOC header
```

Reported by `discord:reavo.` on Request #6 with a screenshot of the broken render.

## Design ruling (DRI, 2026-07-26)

The reporter proposed detecting the URL shape and stripping the URL and brackets out of
the title. **Escaping is the better rule, and it is what ships.**

- Stripping needs a heuristic for "URL shape", and every such heuristic is wrong on some
  input — it would mangle titles that merely contain brackets while missing exotic link
  forms.
- Stripping destroys information. The title is the Operator's own words; #5's title is
  *about* a markdown link, so deleting it would make the card lie about the Request.
- Escaping is one rule, total over all input, and reversible on the eye: Discord honors
  backslash escapes, so the title renders as written.

**Only the text half is escaped.** The url half is machine-generated and must stay
byte-exact — `seed_entry_matches` keys the Seeds-thread mirror lookup on `](<url>)`, so
touching the url half would orphan every existing entry.

## Open design questions

- **Existing posts re-render once.** Escaping changes the content the reconcile diffs
  against, so posts whose titles contain `[` or `]` will be rewritten on the next cycle.
  This is a one-time repair, not a violation of `REQ-HAZARD-RECONCILE-IDEMPOTENT`: the
  invariant is zero mutations over an *already-consistent* Project, and these posts are
  by definition inconsistent — they are the bug. Idempotency holds from the second cycle
  on. Verify this in the live proof rather than assuming it.

  **MEASURED 2026-07-26 on 0.4.2, and the retro-repair half was too broad.** Only ONE of
  the five escape sites writes content reconcile diffs: the milestone sub-issue line
  (`botpost.rs:229`, inside `milestone_card`). The other four are written once and never
  revisited — Seeds-thread entries (`capture.rs:100`), both Updates notes
  (`capture.rs:138/148`) and the state-change note (`reconcile.rs:274`) are one-shot
  posts, and the Contents post carries thread names, not Request titles. So an
  already-broken Seeds entry or Updates line **stays broken**; only milestone posts
  self-repair. Three cycles on 0.4.2 over `_spt-progress-tool` mutated nothing, which is
  consistent with this and not with the broader claim. CHANGELOG wording corrected and
  the published release body re-issued to match — it had promised the broad repair.
  Idempotency leg PROVEN separately: `#30 posted in Backlog` on one cycle, zero mutations
  on the next over the same Project.
- **Which characters.** `\`, `[`, `]` — backslash first, or the escape escapes itself
  wrong. Parens in the text half do not break the construct and stay untouched.

## Tasks

- **T1 — registry.** `REQ-LINK-TEXT-ESCAPE`, activated `["doc","impl","unit"]`. *(done)*
- **T2 — doc.** CONTEXT.md, under Seeds thread / Updates thread.
  `<!-- [doc->REQ-LINK-TEXT-ESCAPE] -->`
- **T3 — impl.** `botpost::escape_link_text`, applied at all five sites.
- **T4 — unit.** Red-first: a title carrying `](` renders one intact link; the url half is
  untouched; `seed_entry_matches` still finds an escaped entry; plain titles are byte-identical
  to today (no gratuitous churn).
- **T5 — live proof.** A Request titled like #5 renders as one clean link in a State thread,
  and a second reconcile cycle mutates nothing.

## Not in scope

- The embed **title** field (`#N | title`) — Discord renders no markdown there, so it was
  never at risk and needs no escaping.
- Issue **bodies** mirrored into the card description. They are already markdown by intent;
  the Operator writing markdown in a body gets markdown, which is correct.
