# W48 — the facet nobody looks at

Requests **#74**, **#75**, **#76**. Two bodies of work, not three: #74 and #75 are one
defect measured from two surfaces, and #76 is the taxonomy's missing repair arm.

---

## Part A — #74 + #75 are the same object

### What was UNVERIFIED, now measured

#74 asked what the ref-less bot posts actually are and said the answer "needs a Discord
read." It got one (read-only, live guild, 2026-08-21):

```
GET /channels/1529984038230818896/messages/1533809250323791952
  id        : 1533809250323791952
  type      : 4                       <- CHANNEL_NAME_CHANGE
  timestamp : 2026-08-03T05:10:22Z
  content   : Backlog 1
  author    : Alchemy Hub (bot = true)
  embeds    : 0
```

They are **Discord system messages** — the "changed the channel name" notice the platform
emits when the bot renames a thread. Every one of them is dated 2026-08-03, which is when
`REQ-THREAD-NAME-PROJECT-INDEX` shipped and renamed every managed thread to carry its
Project's index (`Backlog` → `Backlog 1`). One rename, one notice, per thread, for ever.
They are not provisioning-era, and they are not orphans: they were never bot *posts* at all.

Confirmed universal, not one thread — every bot-authored message in `⚙alchemy-testbed`,
grouped by message type:

```
Releases 1             botmsgs=3   types=[0x2  4x1]
Updates 1              botmsgs=58  types=[0x57 4x1]
Inactive Milestones 1  botmsgs=4   types=[0x3  4x1]
Active Milestones 1    botmsgs=1   types=[     4x1]
Seeds 1                botmsgs=1   types=[     4x1]
Shelved 1              botmsgs=1   types=[     4x1]
Cut 1                  botmsgs=12  types=[0x11 4x1]
Done 1                 botmsgs=19  types=[0x18 4x1]
Acceptance 1           botmsgs=1   types=[     4x1]
WIP 1                  botmsgs=2   types=[0x1  4x1]
Greenlit 1             botmsgs=2   types=[0x1  4x1]
Eval 1                 botmsgs=1   types=[     4x1]
Backlog 1              botmsgs=10  types=[0x9  4x1]
```

**Exactly one `type=4` in every thread.** That single fact is both Requests:

- **#75** — `desired_contents` counts `bot_messages(thread).len()`, so every counted thread
  reads exactly **one too many**. The Operator's two examples land precisely:
  `spt-progress-tool` Eval `3` against 2 cards, `spt-bs-releases` Cut `1` against 0 cards.
  The offset is constant because the cause is one message per thread, and it is `+1` and
  never `+2` because a thread is renamed to its index once.
- **#74** — the same message reaches `scan_channel`, carries no ref, and takes the orphan
  branch. Once per State thread and both Milestone threads, per Project, per cycle: the
  ~50 lines/cycle and 2930 in one run that #74 measured.

### The root cause, stated once

`SerenityPort::bot_messages` filters the history by **author only**:

```rust
.filter(|m| m.author.id == self.self_id)
```

Author is not the whole test. Discord authors its own system notices *to the bot* when the
bot performs the act that triggers them, so "authored by the bot" admits messages the bot
never wrote and does not own. Everything downstream — the board scan, the Contents counts,
the Seeds mirror, the TTL sweep — then reasons about platform furniture as if it were a
bot post.

Same family as `REQ-HAZARD-BOARD-SCAN-WINDOW` (2.11) and for the same underlying reason:
the port answered with something that was *not* what the trait's contract said it was, and
every consumer inherited the error. There too, idempotence held in the logic and was broken
by the read underneath it.

### Why the test suite could not have caught it

`FakeGuild` stores `PinnedMessage`, which has no message-type field. **The double cannot
represent a system message at all**, so the class of message that causes the defect does
not exist anywhere in the suite. This is 2.11's corollary arriving from the other
direction: there the double was *more* capable than the port (whole history for free);
here it is *less* capable (no system messages ever). Either way the gap between double and
port is where the bug lives.

The double already models this correctly for one case — `create_thread` records a starter
notice in `starters`, like Discord does. The rename path must model its notice the same way.

### Decision: filter at the port, do not clear

#74 offered two repairs — recognize them before the orphan branch (the way a staging post
is recognized), or clear them. Now that they are named, **neither is right**:

- Recognizing them at the orphan branch fixes #74 and leaves #75 standing, because the
  count read is a different call site. One cause must not get two patches.
- Clearing them is a mutation per cycle against messages that regenerate on the next
  rename (an index moves whenever the Project registry does). Filtering is idempotent;
  deleting races. The main channel's starter-notice clearing is not precedent — that
  serves a stated property ("the channel stays Seeds-only", Operator request 2026-07-22),
  and no such property is stated for threads.

The fix is at the seam the trait already documents: `bot_messages` and `pinned` say they
return **the bot's own messages**, meaning bot *posts*. They will.

### Tasks (A)

- **A1** — `is_bot_post(MessageType) -> bool` as a pure, directly-tested predicate: admits
  `Regular` and `InlineReply`, refuses every system type. Used by `bot_messages` and
  `pinned`. Not applied to `all_messages`, which `thread_starter_messages` needs intact.
- **A2** — `FakeGuild` grows a system-message store; `rename_thread` records a
  name-change notice into it, as Discord does. `bot_messages`/`pinned` never return it,
  and `FakeGuild::messages` (test assertions) does not either.
- **A3** — Tests: a renamed thread's Contents count is unchanged (#75), and a renamed
  thread's scan produces an empty loud list (#74). Both must fail against today's code.
- **A4** — `docs/KNOWN-HAZARDS.md` **2.12** + `REQ-HAZARD-BOT-AUTHORED-SYSTEM-MESSAGE`.

### Also worth recording: #74's thesis, proven live

While reading the log for this: `⚙spt-bs-releases/Backlog` carried a **second** orphan,
`1534117665948434493`. It appears in exactly **two** cycles and never again, and the
message is now `10008 Unknown Message` — deleted. That is a genuine transient orphan, the
exact thing the loud list exists to surface, and it arrived and left inside ~50 lines of
permanent noise per cycle without anyone seeing it. #74's argument is not hypothetical.

---

## Part B — #76, the taxonomy's missing repair arm

Scope confirmed by deployah's measurement carried in the issue's comment: across all 209
`spt-bs-releases` issues, backfill population is **one** (#153). The fill arm is a repair;
the loud arm is the one that earns its keep.

### B1 — the fill arm: a `type` verb that only ever fills

`type #N <TYPE>` writes `type: <TYPE>` **only when the Request carries none**, and refuses
when one is present, naming it. This is not `state`'s any-jump shape and must not be
mistaken for it: CONTEXT.md holds Type is "Set when the Request is minted and **never
changed**" (Operator ruling 2026-07-31), and the refusal is what makes that property hold
**by construction** instead of by discipline. `tag`'s refusal of the reserved `type:`
namespace stands unchanged — this is not a second path to a *change*, it is the only path
to a *fill*.

It mirrors `create`'s IDEA rule (`REQ-TYPE-IDEA`): filling `idea` also sets
`flag: NEEDS-OPERATOR`, because a mint of one does and a fill is the mint that never ran.

Vocabulary parity is load-bearing here (`REQ-VOCAB-PARITY`): the verb must land in
`manifest.toml`, `CONTEXT.md`'s vocabulary line, and `guide.rs` in the same change, or the
parity test fails — correctly.

### B2 — the loud arm: absence announced where absence is looked at

reconcile skips-and-announces a stateless issue and carries a typeless one silently. Two
changes, deliberately different in kind:

1. **On the card** — the head block renders a missing facet as `` `?` ``, which is
   indistinguishable from noise and is exactly deployah's point: the board read fine
   because the facet the board displays was fine. A typeless Request renders an explicit
   marker instead. This is the arm that satisfies "announced once, at the moment it
   becomes true" **by construction**: reconcile edits a card only on drift, so it is
   written once, it persists until repaired, and it clears itself the moment B1 runs.
   No cycle state required — which matters, because reconcile holds none.
2. **In the cycle log** — one line mirroring the `no state label — skipped` precedent.
   Unlike Part A's noise this is self-clearing and actionable, and its population is
   bounded by hand-filings, not by thread count.

`#153` is the first use: fill it `bugfix` per doyle's ruling recorded in its own body.

### Tasks (B)

- **B1** — `type` verb (fill-only, refuse-when-present, IDEA rule) + manifest + CONTEXT
  vocabulary line + guide + `REQ-VERB-TYPE-FILL`.
- **B2** — card marker + reconcile announce + `REQ-TYPELESS-LOUD`.
- **B3** — fill `spt-bs-releases#153` as `bugfix` once the verb ships.

---

## Gate

`cargo test --lib --bins --locked` green, `traceable-reqs check` exit 0, every new
requirement in `traceable-reqs.toml` with `doc`/`impl`/`unit` evidence tagged in the same
commit. Ship per `docs/RELEASE-RUNBOOK.md`; deployment on this node is DRI authority.
