docs(teams): use the correct presentation of the project name, and match both

"Runic Gateway" (two words) is the correct presentation; "RunicGateway" is
accepted only where the name has to condense to a single token — the Gitea org,
a package name, a URL segment. Three prose uses in TEAMS.md had the condensed
form for no reason, including the upload warning text, and are corrected. The
`RunicGateway/<repo>` slugs throughout android/PLAN.md are condensed by
necessity and are left alone.

This turned out to matter beyond presentation. The reserved-name matcher (2.8)
specified whole-word matching over a normalisation that case-folds, strips
punctuation and collapses whitespace — under which "RunicGateway" is a SINGLE
word and would never have matched the two-word term "Runic Gateway". The
condensed form is the one an impersonator reaches for, precisely because it is
what the org and every URL already use, so the check would have missed its most
likely input.

Multi-word terms are now additionally compared with whitespace removed on both
sides, so `Runic Gateway` matches `RunicGateway`, `runic-gateway`, `Runic_Gateway`
and `RUNIC GATEWAY` alike. The widening applies only to terms CONTAINING
whitespace, which keeps it clear of the single-word terms where whole-word
matching is doing the false-positive work: `admin` is still compared as a word
and still does not fire on "Badminton". Listing the condensed form as a separate
reserved term was the alternative and was rejected — it is a second thing to keep
in sync and would still miss the hyphenated and underscored variants.

The same reasoning applies to a deployment's own BRAND_NAME, which is free text
and may well be spaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz
This commit is contained in:
2026-08-17 05:04:22 -05:00
parent f944a89660
commit ceab67f285

View File

@@ -569,6 +569,16 @@ runtime configuration):
| The project | `Runic Gateway` — impersonating the software project is as much a problem as impersonating the operator |
| Operator additions | a `teams_reserved_terms` setting, comma-separated, for anything a particular community needs |
> **The project's name has two legitimate presentations, and both must match.** **Runic Gateway** (two
> words) is the correct form; **RunicGateway** is accepted only where the name has to condense to a
> single token — the Gitea org, a package name, a URL segment. A reserved term is therefore stored in
> its correct two-word form and matched against *both*, which §2.8.2's whitespace-insensitive
> comparison gives for free. Listing the condensed form as a separate term would be a second thing to
> keep in sync, and it would still miss `runic-gateway` and `Runic_Gateway`.
>
> The same applies to a deployment's own brand: `BRAND_NAME` is free text and an operator may well
> have set a spaced name whose condensed form is what a would-be impersonator types.
**Not `filter_words`.** That table exists but is **bot-owned** (its own pool, `bot/src/model/`,
never read by the website — `MODERATION_APPEALS.md` §2 is explicit that the two sides share no live
FK) and it is a profanity filter, which is a different question with a different answer. Reusing it
@@ -587,6 +597,18 @@ Normalisation: case-fold, strip punctuation and repeated characters, collapse wh
**no leet-speak folding** in v1 (`4dm1n`) — it multiplies false positives, and the consequence of a
miss is a hidden-by-a-human Team rather than a breach.
**A multi-word term is additionally compared with whitespace removed on both sides**, so a term stored
as `Runic Gateway` matches `RunicGateway`, `runic-gateway`, `Runic_Gateway` and `RUNIC GATEWAY`
alike. Without this the whole-word rule fails on exactly the case that matters: `RunicGateway` is a
*single* word and would never match a two-word term, so the condensed form — the one an impersonator
would reach for, because it is what the Gitea org and every URL already use — would sail straight
through.
That widening applies **only to terms containing whitespace**, which keeps it away from the
single-word terms where whole-word matching is doing the false-positive work: `admin` is still
compared as a word and still does not fire on "Badminton". A two-word term is specific enough that
running the letters together cannot collide with ordinary vocabulary.
#### 2.8.3 What a match does: hide, never reject
A Team whose name trips the list is created **normally** and then **auto-hidden**: `hidden = 1`,