1 Commits

Author SHA1 Message Date
ceab67f285 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
2026-08-17 05:04:22 -05:00

View File

@@ -466,7 +466,7 @@ Two reads, OR'd. `viaGrant` is reported **even when membership also holds** —
coexist; the UI presents membership as the current reason while the grant is kept as audit history"
requirement, and it falls out of not collapsing the two booleans into one.
- A grant may name **any** RunicGateway account, including one with no linked game identity of any
- A grant may name **any** Runic Gateway account, including one with no linked game identity of any
kind. That is the point: letting an unlinked guildmate into the forum must not be a staff ticket.
- Granting **never** writes `team_members`. Revoking never writes `team_members`. A test asserts a
byte-identical `team_members` row set across a grant/revoke cycle.
@@ -505,7 +505,7 @@ Four hops, three of them stored in different places, and the design's most commo
confusion — "this account is linked" answers only *one* of them.
```
RunicGateway account users.id
Runic Gateway account users.id
│ hop 1: linked GAME identity module-owned: shard_account_links(account → user_id)
game account / character team_members.member_key, team_members.user_id
@@ -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`,
@@ -1175,7 +1197,7 @@ setting is, the second appears only at the moment of change and is what the ackn
> - Complying with applicable laws and regulations
> - Establishing policies for your community
>
> RunicGateway does not provide hosted storage or content moderation services. All uploaded content
> Runic Gateway does not provide hosted storage or content moderation services. All uploaded content
> is stored on your own infrastructure.
**(b) Confirmation dialog** — shown only when changing the mode *to* `uploads`: