Commit Graph

604 Commits

Author SHA1 Message Date
f1fb2d40b4 Merge pull request 'docs(modules): core offers a contribution, never a slot name' (#165) from docs/teams-slot-contributions into edge
Reviewed-on: #165
2026-08-19 06:18:55 +00:00
d8af33c805 Merge pull request 'docs(teams): cancel phase 10, and say what that leaves behind' (#164) from docs/teams-phase10-deferred into edge
Reviewed-on: #164
2026-08-19 06:18:10 +00:00
d10f5809b3 docs(modules): core offers a contribution, never a slot name
Amends MODULE_API 1.6.0 in place - it has only ever been on edge, the same rule
the eighth and ninth members were given - and it is a correction rather than an
addition.

As first written, the inverted slot direction had core fill three literal
uo.guild.* names. That worked for module-uo and silently did nothing for anyone
else: a module declaring clan.detail under its own id got an empty page and no
error, because "a fill for a slot nobody declared is not an error" is exactly
the rule that makes an unknown name invisible. It also put a module identifier
inside core, in string literals the Sec 5.2 checker masks by construction.

Sec 3.7a now documents declareModuleSlot(id, name, { core }) and the three
contributions core offers - team.activity, team.forum, team.notify - as a
table, with the rules that follow from the direction: the member is optional, a
slot that asks for nothing stays empty, more than one slot may ask for the same
contribution, and asking for one core does not offer THROWS at the declaration
rather than rendering empty forever.

TEAMS.md's two accounts of the inversion (Part 3's supersession note and the
phase 3 amendment) say the same thing.

Also corrects the UI kit's count in Sec 3.4 and Sec 3.7a: Slot made it nine in
phase 3 and three places still said eight.

Found by phase 11 while writing the chapter that teaches this shape to an
audience outside this org.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 01:16:07 -05:00
88cc49225a docs(teams): cancel phase 10, and say what that leaves behind
The org lead cancelled the capability layer on 2026-08-19, deferring it until a
second integration is wanted or it is asked for by name. Phase 11 is now the last
phase of the bet.

The same argument that put phase 10 last is the argument for not doing it yet:
with one integration built, the refactor would extract a capability surface from a
single implementation and have nothing to check the extraction against. It is
cheaper and better-informed the day a second platform exists, because that
platform is what proves which of the five capabilities the seam needs.

Three places pointed forward at it and now say what is true instead:

- Sec 7.2 and Sec 7.3 both justify the Admin -> Teams panels by "phase 10 makes
  the platform a registry lookup". The decision survives its reason: an operator
  should not have to know which platform is configured to find the panel, and that
  holds whether or not the registry is ever built.
- Sec 8.2 keeps the Matrix comparison and the capability table, with a note that
  no registry is built either. The research did its job by keeping core's calls
  phrased as eligibility questions rather than as Discord operations; what is
  absent is the indirection, so `discord` is named directly in the bridge, the
  voice provisioner and the command dispatcher.

The phase entry keeps its body rather than deleting it, because the argument for
the layer is what a future phase would start from.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 00:27:22 -05:00
6f1f406fe1 Merge pull request 'docs(teams): phase 9 as built — roles, not overwrites, and three things §7.3 named that do not exist' (#163) from docs/teams-phase9-voice into edge
Reviewed-on: #163
2026-08-19 05:15:43 +00:00
c87034d7fe docs(teams): phase 9 as built — roles, not overwrites, and three things §7.3 named that do not exist
Amends `TEAMS.md` §7.3 inline, marks phase 9 done in Part 12, and adds the
`team_integrations` row to `BACKEND_DESIGN.md`'s schema table. Pairs with
**website#159**.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 00:10:18 -05:00
d00ead09b6 Merge pull request 'docs(teams): phase 8 as built — the gate §7.2 could not check, and the key it could not hold' (#162) from feature/teams-phase8-notifications-bridge into edge
Reviewed-on: #162
2026-08-19 01:32:22 +00:00
71f0b7ad90 docs(teams): phase 8 as built — the gate §7.2 could not check, and the key it could not hold
Amends §7.2 inline and marks phase 8 done in Part 12; adds the
team_integration_config row to BACKEND_DESIGN.md's schema table.

Two of the amendments are things the tree disproved rather than choices:

- §7.2's DDL cannot hold its own default row. MariaDB coerces PRIMARY KEY
  columns to NOT NULL, so `team_id NULL` is unrepresentable and the override
  mechanism has no base case. Confirmed against a real MariaDB (error 1048).
- §7.2's visibility gate has no data source on either side and cannot have one:
  the streams carry no visibility, a forum thread is members-only by
  construction rather than by a column, and core cannot see a channel's
  permissions. The gate becomes an attributed operator acknowledgement.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 20:25:41 -05:00
f5c121b02a Merge pull request 'docs(teams): phase 7 as built — five amendments to §7.1' (#161) from feature/teams-phase7-slash-commands into edge
Reviewed-on: #161
2026-08-19 00:15:15 +00:00
953f7fcd20 docs(teams): what the phase 7 rig walk proved, and the two defects it found
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 19:08:37 -05:00
c196d03d31 docs(teams): phase 7 as built — five amendments to §7.1
The command that proves the seam is the MODULE's `/guild`, not core's `/team`:
§7.1 was written before phase 3 settled that Teams is a contract primitive with
no core surface, and a core `/team` publishes the same invented noun that got
core's Team pages deleted. Its deep link comes from `pageUrlTemplate` for the
same reason — `/teams/:slug` does not exist.

The re-register nudge is its own bot endpoint rather than a ride on
`/internal/config`, whose body carries the decrypted bot token. `actor` carries
`role` beside `isStaff`, since a module with its own audience rungs cannot place
a caller from a boolean. And "deregistration is free" needed a second half: it
holds across the restart an uninstall asks for, not across the runtime toggle,
so liveness is asked at both the pull and the dispatch.

MODULE_API.md stops saying `registerSlashCommands` throws and documents it —
every member of 1.6.0 is live now.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 18:53:56 -05:00
30d964c090 Merge pull request 'docs(teams): queue the integration kit as phase 11, last before the cutover' (#160) from docs/teams-phase11-queue into edge
Reviewed-on: #160
2026-08-18 23:19:26 +00:00
9f90a99362 docs(teams): queue the integration kit as phase 11, last before the cutover
The kit is the instruction book for putting a different game on this platform,
written for an audience outside this org. Teams expands the contract that book
teaches against, so the book is the last thing the bet owes before `edge` becomes
`main` (org lead, 2026-08-18).

One sentence in it is already wrong rather than merely incomplete.
`book/02-website-module.md` tells a reader that core declares a slot and a module
may only fill one. Phase 3 inverted exactly that, and by phase 6 module-uo declares
three — a new game's module cannot implement Teams at all without the inverted
direction.

Two shapes are genuinely new and worth teaching: the inverted slot, and
`registerTeamProvider` as the first registration where core calls the module and
waits — with the asymmetry that every call fails stale except `projectRoster`,
which fails closed, because for a visibility question "keep what you have" means
serving the roster unprojected.

The phase explicitly does NOT enumerate the contract. The kit already teaches four
members and has never mentioned notification streams, announce legs or post hooks,
all of which predate Teams. MODULE_API.md is normative; the kit teaches one path
and links out.

Its ordering is awkward and is stated rather than smoothed over: it is written
before the cutover and can only merge after it, because CI clones the pinned sha
and checks the template against that core's MODULE_API_VERSION — and 1.6.0 does
not reach `main` until the cutover lands.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 18:18:26 -05:00
703f0dbe67 Merge pull request 'docs(teams): phase 6 as built — four deviations and a ninth contract member' (#159) from docs/teams-phase6 into edge
Reviewed-on: #159
2026-08-18 23:09:39 +00:00
5e284d5468 docs(teams): phase 6 as built — four deviations and a ninth contract member
Part 6 gains an as-built header rather than a rewrite, so the reasoning that
produced the original design stays legible beside what the build learned.

Four deviations. There was no web notification settings screen to add the Team
list to — `/auth/me/notifications/*` was built for the app in M7 and had zero web
consumers, which is survivable for push and not for a sink whose whole argument is
the web-only user. Email defaults to `off` rather than `digest`, on the org lead's
call: digest-by-default would start mailing every member of every Team the moment
an operator connects Gmail. Roster events tickle but do not email. And a ninth
member joined MODULE_API 1.6.0.

`pageUrlTemplate` is the member, and it exists because phase 3 left core with no
Team page and therefore no way to link to one. It joins 1.6.0 in place under the
rule set in phase 2 — a contract owes a bump only once it has landed on `main`,
and 1.6.0 has only ever been on `edge`.

Two further build decisions are recorded where they belong: the digest computes at
send time and keeps no queue (§6.4), and one-click unsubscribe is a stateless HMAC
whose whole capability is muting one (user, Team) pair (§6.4).

BACKEND_DESIGN gains the table, the two `/auth/me` routes and the unsubscribe
endpoint — the only write in the public tier and the only route with no `siteMode`,
because the mail went out before the site went into maintenance.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 14:35:49 -05:00
04eb859a48 Merge pull request 'docs(teams): phase 5 — discussion, the edit window, and reports that route around leadership' (#158) from docs/teams-phase5 into edge
Reviewed-on: #158
2026-08-18 18:37:19 +00:00
aed4ec4166 docs(teams): phase 5 — discussion, the edit window, and reports that route around leadership
TEAMS.md §5.4, §5.5.7 (new), §5.6 and the Part 12 phase entry; BACKEND_DESIGN.md's
schema and admin route tables.

**The largest change is a decision, not a description.** §5.6's first rule said
"a leader may also see and act on reports for their own Team, but staff always
receive them". The org lead settled on 2026-08-18 that the leader half is
**decided against, not deferred**: the gap the section exists to close is that
leaders moderate their own forum and a Team's leaders are exactly the people who
will not report their own Team, so a leader-visible queue hands a complaint about
a leader back to them — and a read-only leader view still tells them who reported
what. Recorded as an amendment rather than by editing the sentence away, because
the reasoning for the original is what makes the correction legible.

**§5.6's `content_reports` DDL does not work as written, and the amendment says so
rather than quietly swapping it.** With `status` in the unique key, CLOSED rows
collide with each other: dismiss a report, let the behaviour recur, dismiss the
second one, and the UPDATE lands on a tuple that already exists — so the queue
starts throwing duplicate-key errors on the first repeat reporter. The shipped
table keys on a generated `open_marker`, the same encoding
`team_forum_grants.active_marker` uses. Three smaller departures are recorded
beside it: `handled_note`, the two username snapshots §2.10 asks for everywhere
else, and a real CASCADE on `team_id`.

**New §5.5.7 for `teams_forum_edit_window_minutes`** (0–1440, default 15), and the
rule under it: the window is resolved on the server TWICE — the read path stamps
`canEdit`/`editableUntil` so a client knows whether to draw the control, the write
re-derives it from `created_at` before allowing anything. The read is advice and
the write is enforcement, because a time-bounded permission must not take its
clock from the party it bounds. That is also why the key is not published: the
client needing the number is the admin screen, and the client needing the decision
already has it per post.

**§5.4 gains three notes its route table does not carry**: thread creation splits
authority by TYPE rather than widening the leader gate (and reports it as two
booleans, since one would make a client guess which right it described); post
moderation is its own route whose validator accepts all eight actions so the model
can say "pin applies to a thread, not to a post"; and a reply's three refusal codes
are chosen to be distinguishable — 404 absent, 400 announcement, 409 locked — with
locked refusing staff too.

The Part 12 entry records what the phase disproved, its four acceptance criteria,
that it spans ONE repo where phase 4 needed two, and the single defect the live rig
found. It also notes that phase 4 shipped `uploads` with the default off, so §5.6's
"pull reports forward if uploads is enabled anywhere" never triggered.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 13:29:56 -05:00
36dbe9c985 Merge pull request 'docs(teams): phase 4 — the forum's access model, switches and image policy' (#157) from docs/teams-phase4 into edge
Reviewed-on: #157
2026-08-18 14:18:22 +00:00
0e665078e8 docs(teams): phase 4 — the forum's access model, switches and image policy
Records what building phase 4 settled, and what it disproved.

The structural correction first: TEAMS.md 3.1 gave the forum a CORE page and phase
3 deleted every core Team page. The ROUTES were unaffected — they are all /player
and /admin — but the participant surface had no home, and 5.4's route table did not
notice. Settled the way phase 3 settled the activity feed: module-uo declares a
second place on its guild page and core fills it, so the phase spans two repos
rather than the one the plan named.

Two slots rather than one, because a slot holds one component and the first fill
wins; the panel navigates by search param because a thread must be linkable and
core cannot mount a route on a page it does not own.

Two findings from the sanitiser worth not re-deriving: `rel` has to be on the
allowlist for the transform that WRITES it to survive, or every forum link ships
without noopener; and the bare-URL linkifier runs after sanitising, over escaped
text only, which is the property that makes it safe rather than an injection point.

Also recorded: the upload sweep runs regardless of the current image mode, which is
the mechanism behind the dialog's promise that disabling uploads does not delete
what is already there; the two routes the table lacked; and the org lead's decision
that all three proposed acknowledgement additions ship.

BACKEND_DESIGN gains the four forum tables and the reasoning a reader of the schema
alone would miss — why the guard is at the route and never at the data, why no
stored body ever contains an <img>, what `uploads` mode hardens, and what the
acknowledgement actually records. MODULE_API's inverted-slot section gains the rule
a module needs: one slot per PLACE, not one per page.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 07:25:05 -05:00
7121b1a028 Merge pull request 'docs(teams): phase 3 — Teams is a contract, not a surface' (#156) from docs/teams-phase3 into edge
Reviewed-on: #156
2026-08-18 02:09:32 +00:00
d78cc99c80 docs(teams): Teams is a contract, not a surface — record the correction
The org lead's correction to Part 3, and the inverted extension-slot direction
it forces.

TEAMS.md: §3.1's routes, §3.4's two slots and §3.5's three nav entries are all
marked superseded in place, and Part 12's phase 3 entry gains the amendment
explaining why — core does not own the word for a Team, so the module that owns
the vocabulary owns the page. The five corrections found by building are kept
alongside it.

MODULE_API.md: 1.6.0's list swaps the two client slots for
registry.declareModuleSlot + Slot in the UI kit, and a new §3.7a documents the
inverted direction: what forced it, the enforced namespace, why core's fills are
applied at mount rather than eagerly, and why a fill for an undeclared slot is a
no-op where §3.7's unknown slot throws.

BACKEND_DESIGN.md: the by-external-id lookup route.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 20:58:40 -05:00
fd9c02130c docs(teams): Team pages and the activity feed, and what phase 3 disproved
TEAMS.md gains a dated amendment on phase 3 with five corrections, all found by
building the thing it describes:

  - §3.2 and §3.4 contradict each other about `team.member.row`'s props, and
    §3.2 wins because it is the security rule. A client slot can only receive
    what the browser was sent, so §3.4's `{ memberKey, userId, displayName }`
    means publishing both identifiers in every public roster, module installed
    or not. The slot is redeclared with what core can honestly supply.
  - §3.3's projection is an EIGHTH MODULE_API member where 1.6.0 listed seven.
    Settled by the org lead: 1.6.0 is amended in place, on the rule Protocol 4
    was given in phase 2 — a contract owes a bump only once it has reached
    `main`.
  - "the module declines" needed splitting in two before it could be built. No
    module at all withholds nothing and must serve the roster whole; a module
    whose rungs could not be consulted must serve none of it. Only the second
    fails closed, or bare core shows an empty roster on every Team page.
  - the module answers with member KEYS, not rows, so it can narrow what is
    published and cannot widen it.
  - core's five activity kinds are four until the forum lands, and a Team's
    FIRST roster emits no join items at all.

§2.11's route table gains the activity endpoint it never had, and MODULE_API.md
documents `projectRoster`, the inverted fail-closed semantics that make it
different from every other provider call, and `ctx.teams.activity.push`'s item
shape and its four contractual properties.

BACKEND_DESIGN.md: the seventh Team table, its retention, and the three public
routes' new behaviour — `enabled` on the index, the slot props on the single
Team, the per-caller row projection on the roster, and the feed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 20:17:05 -05:00
cbc492ea52 Merge pull request 'docs(link): roster members carry guild rank' (#155) from docs/protocol4-guild-rank into edge
Reviewed-on: #155
2026-08-17 22:48:31 +00:00
0622ed00e0 docs(link): roster members carry guild rank
Amends Protocol 4 in place rather than bumping it: the protocol has not reached
`main`, and a bump is owed only once a protocol has been released.

The roster shipped as the standard actor object, which carries no rank. Teams
phase 2 found the consequence -- the website could learn leadership only from the
board's single `leader` field, so it could name exactly one leader while a UO
guild routinely has several, and TEAMS.md §2.5 treats multiple leaders as the
normal case.

Roster members now carry `rank` (0-4, 4 being Leader) plus `rankCliloc`, or
`rankName` where a shard's custom rank definitions use literal names. Rank is on
roster members only -- it is a property of a mobile's membership of THIS guild,
not of the mobile, and every other actor the bridge writes is a bystander, a
killer or a governor.

Both files carry the trap this found, because it is the kind of thing a consumer
gets wrong silently: **`PlayerMobile.GuildRank` returns Leader for anyone at
GameMaster or above, whatever their real rank.** It is a gameplay convenience so
staff can operate a guild stone, and the true value has no accessor -- so the
bridge omits the rank entirely for a staff account rather than publish a
leadership claim it knows is false. An absent rank therefore means "not known",
and a consumer must read it as neither 0 (which silently demotes them) nor
leadership (which republishes the lie).

INTEGRATION.md also states the other half plainly for an outside integrator:
`guild.update`'s single `leader` is the founder-leader, not the set of leaders,
so "who leads this guild" is a read of the roster's ranks.

Recorded too: the sidecar needs no change and no store migration, because it
treats roster members as opaque values and never reads a field inside one. That
is the forwarder design paying off, and it is worth having written down the next
time someone adds a member field.

Pairs with servuo-plugins (the emitter) and Module-uo (the ingest and the
provider).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 17:44:04 -05:00
e68affbcf5 Merge pull request 'docs(teams): Team core, MODULE_API 1.6.0, and what phase 2 disproved' (#154) from feat/teams-phase2-team-core into edge
Reviewed-on: #154
2026-08-17 22:19:17 +00:00
680a4866ac docs(teams): Team core, MODULE_API 1.6.0, and what phase 2 disproved
Documents phase 2 of docs/website/TEAMS.md across the three files that had to
change, and records the five places building it disagreed with the design.

## MODULE_API.md — 1.6.0

The Team surface becomes contract: `api.registerTeamProvider(...)`,
`ctx.teams.publish` / `ctx.teams.reconcile` / `ctx.teams.activity.push`,
`api.registerSlashCommands(...)`, and the two client slots. Additions only, so
minor; module-uo's `coreApi: "^1.3.0"` still resolves.

Per the org lead's decision, one 1.6.0 covers the whole surface rather than a
minor per phase -- so the document names the phase against each member, and the
two that cannot work yet are marked as present-and-throwing rather than left to
be discovered at runtime.

`registerTeamProvider` gets the fullest treatment because it is the first
registration where core calls the MODULE and waits for an answer. The envelope,
the 10-second budget and the refusal semantics are all contract, not
implementation: they are how a module says "I cannot answer" without core hearing
"there is nothing". `ctx.teams` is documented as push-only, with the reason there
is no reader — a module answers questions about Teams, it does not ask them.

## BACKEND_DESIGN.md

The six Team tables, the rename rule, the active-only uniqueness encoding, the
per-column account-deletion decisions, and all eighteen routes across the three
tier tables.

Two entries there exist to stop a future reader "fixing" them: why
`team_forum_grants` does not use the obvious generated column, and why the two
columns TEAMS.md never mentioned have to exist.

## TEAMS.md — five amendments, marked as amendments with their date

  - **§2.5's SQL and §2.10's decision cannot both hold.** MariaDB refuses ON
    DELETE SET NULL on a base column of a stored generated column (1901), so
    §2.5's `active_user` forces the CASCADE that §2.10 exists to prevent. §2.10
    wins; the marker is re-encoded for identical semantics.

  - **`team_forum_grants` lands in phase 2**, so the four-path resolver is written
    once and its non-contamination tests are real.

  - **Two columns the document did not contemplate**, both serving §2.4's gates:
    `roster_synced_at`, because sync state is per MODULE and gate 3 leaves one
    Team behind while the others sync; and `members_empty_since`, gate 4's
    per-Team quarantine.

  - **`leader` on the member shape is not path 2.** Taking §2.3 and §2.5 both
    literally gives one column two writers, and the roster writes first — so a
    refused `getTeamLeaders()` silently demoted everyone. Found by its own test.

  - **§2.8.2's matcher needed two narrow widenings**, both real impersonation
    vectors the whole-word rule missed: a term matches a name word's singular
    ("Guild of Moderators"), and a run of single-letter words is compared joined
    ("G.M."). Neither re-admits substring matching.

Pairs with website (Teams phase 2) and Module-uo (the provider).

Refs docs/website/TEAMS.md Part 12 phase 2

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 15:32:21 -05:00
11696d14e3 Merge pull request 'docs(link): protocol 4 — guild membership on the wire' (#153) from feat/teams-phase1-guild-roster into edge
Reviewed-on: #153
2026-08-17 19:28:04 +00:00
b369e728c2 docs(link): protocol 4 — guild membership on the wire
Adds v4.md as the spec of record for `guild.roster` and `guild.leave`, and
corrects the two older documents that Protocol 4 makes wrong.

PROTOCOL_2.md §10.1 already described this design — hold a member-serial set,
diff it each sweep, emit join/leave — and 2.0 then shipped only the half needing
no new state, folding membership into the board signature as a serial *sum*. The
section has read ever since as though the whole thing were built. It now says
which half shipped, and carries the correction that doing it produced: a sum is
not a safe stand-in for a set, because one member joining and another leaving
between two sweeps offset each other and the guild reads as unchanged.

INTEGRATION.md gains both kinds in the event catalogue, the `roster` key on
GET /guilds, and the three things an integrator gets wrong otherwise — that
`guild.leave`'s `who` is a bare serial rather than an actor object (the mobile has
already left, so there is nothing to attribute), that `acct` is genuinely optional
on a member, and that a guild with no `roster` key is not the same as one with an
empty roster.

v4.md documents what the phase found as well as what it built: the missing store
migration and the user_version decision, why the roster lives in its own column
rather than inside the guild.update snapshot, why a split roster is reassembled in
memory rather than appended to the column, and why guild.leave gets no board
projection at all. §6 records that the reassembly bug was invisible to every unit
test — they all exercised single-frame rosters — and only the live rig caught it.

TEAMS.md is amended where this phase disagreed with it: Phase 1 spans five repos,
not four, because installer/backup.rs justifies skipping the sidecar database on
reasoning the migration falsifies. The user_version decision is recorded there too,
since the design of record did not contemplate a migration mechanism at all.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 13:01:05 -05:00
3bfa309a40 Merge pull request 'docs(teams): use the correct presentation of the project name, and match both forms' (#152) from docs/teams-name-presentation into main
Reviewed-on: #152
2026-08-17 10:07:22 +00:00
6f8b722acf 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:05:20 -05:00
c3e9a8aeb5 Merge pull request 'docs(teams): design of record for platform Teams & community integration' (#151) from docs/teams into main
Reviewed-on: #151
2026-08-17 10:00:45 +00:00
f944a89660 docs(teams): replace the draft upload warning with the org lead's wording
The drafted text was a placeholder the doc explicitly flagged as needing the org
lead's ownership. Replaced with the supplied wording, which is better in three
ways: it is factual rather than legalistic, it enumerates the specific
responsibilities being accepted (moderation, storage and backups, legal
compliance, community policy) instead of gesturing at them, and it states plainly
that RunicGateway provides no hosted storage or content moderation services.

Recorded as two surfaces rather than one, because they behave differently: a
settings help text that is always on screen and explains the setting, and a
confirmation dialog shown only when changing the mode to uploads, which is what
the acknowledgement actually records.

The dialog carries two checkboxes and the API still takes one `acknowledge: 1`.
Recording two booleans would add nothing — there is no reachable state where an
operator agreed to one clause and not the other and proceeded — while the stored
version is what answers the question that matters later: which text did they
agree to?

Three additions are proposed on top and marked as droppable, since none is
liability language and none changes what is being agreed to: that uploads are
attributed and staff-removable (the reason the attribution table exists), that
disabling uploads later does NOT delete files already uploaded, and that anyone
with forum access can upload — including manually granted accounts with no linked
game identity. Also adds the advisory `remote` mode needs, which the upload
wording correctly does not cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz
2026-08-17 05:00:09 -05:00
111b412fd5 docs(teams): design of record for platform Teams & community integration
Teams become a core platform primitive rather than a mechanism for generating
Discord infrastructure: a module stays authoritative for who a Team is and who
belongs to it, and core owns everything the platform attaches to it — pages,
roster, activity, forums, notifications and optional external integrations.

Investigated against the working tree rather than the brief, which corrected
three of its premises:

- guild.update carries member/online COUNTS, not a roster, and one leader. Every
  membership-derived feature has no data source today, so a protocol change
  (PROTOCOL_VERSION 3 -> 4, guild.roster + guild.leave) is the gating phase.
- There is no module->core news hook to generalise. ctx.posts is read-only and
  both registerPostHook and registerAnnounceLeg run core->module, so the activity
  feed needs its own ingestion member modelled on ctx.push.publish.
- Core has no SSE at all; it left with the module cutover. Live roster status is
  module-projected through an extension slot rather than a new core transport.

Also settled: the four-path permission model (game membership / leadership /
forum access / external access) with the non-contamination invariant; envelope-
returning provider methods so module unavailability is structurally staleness
and never an authoritative empty; reserved-name screening with auto-hide and an
admin-approval gate on the three actions that publish untrusted game-sourced
strings; forum admin controls with a renderer-owned image path; abuse reporting;
email as a third notification sink; and a capability-based integration contract
rather than a shared interface, with the Matrix research behind that choice.

Part 10 sorts every artifact into contract / core-internal / module-owned / wire
protocol, because almost none of this is contract: the surface is ten members,
and the ~15 tables are core-internal and off limits to a module even though the
module is what populates them.

Proposes MODULE_API_VERSION 1.6.0 (minor, additions only) and an 11-phase plan.
Design only — nothing is implemented, and MODULE_API.md remains normative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz
2026-08-17 03:53:19 -05:00
d4beba1a5d Merge pull request 'docs(tree): sync website/PROJECT_TREE.md' (#150) from chore/sync-website-tree into main
Reviewed-on: #150
2026-08-12 23:17:29 +00:00
runic-docs-bot
fc3dd30feb docs(tree): sync website/PROJECT_TREE.md from RunicGateway/website@e0c961c [skip ci] 2026-08-12 23:10:47 +00:00
02fd9f4c2d Merge pull request 'docs(modules): record the cutover, and retire what it makes untrue' (#149) from docs/module-system-cutover into main
Reviewed-on: #149
2026-08-12 23:05:56 +00:00
cfd1800347 docs(modules): record the cutover, and retire what it makes untrue
The module system reached `main` on 2026-08-12 (website#150) and `edge` was
deleted. Several present-tense claims across two plans became false at that
merge; this is the pass that closes them.

MODULE_SYSTEM.md's status banner was the loudest: it still announced "in
implementation - Phase 2's core scaffolding is landing, PRs 1-7 of 9 done",
three phases stale. It now says SHIPPED and gives the shape of what landed.

The substantive addition is 2.9's "as landed" section: the four decisions that
settled the cutover (none of them a code question), and what the merge does to a
LIVE deployment - no DROP TABLE anywhere, one guarded and already-verified
column drop in 228 changed files, no newly-required env var, and a declared
module starting on the same first boot rather than waiting for an operator
click. That paragraph is the one a future cutover will want, and none of it was
written down anywhere before.

Phase 5 is marked COMPLETE in the phase list (2.11.1 already recorded slice 3
closing it), decision 11 carries its landing date, and 2.11.1's "the pin is one
of the things the cutover has to revisit" is resolved in place - the sha did not
move, only the branch label, so the coreApi equality assertion stays armed.

The Android note is the one that changes meaning rather than tense. It said the
`gameAccountSignup` fix "must land before that cutover"; the org lead released
that gate instead, so the note now records a LIVE regression - the shipped app
hides game-account creation from every user - rather than a scheduling item. An
accepted break and a forgotten one differ only by whether someone wrote it down.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 18:03:34 -05:00
33838bdf10 Merge pull request 'docs(modules): correct F3 in the acceptance record' (#148) from docs/kit-acceptance-f3-correction into main
Reviewed-on: #148
2026-08-12 22:42:15 +00:00
c33a8a5509 docs(modules): correct F3 in the acceptance record
Only the Gitea release workflow carries the publishing placeholders. The report
said both flavours; the GitHub one uses GITHUB_REPOSITORY and friends and has
nothing to rename. Found while fixing it in the kit (Integration-kit#4), which is
where the single checklist row landed.

Also records what that implies about a run like this: every finding was
re-derived before being acted on, and it was not ceremony — this one was wrong in
a detail and F5 understated its problem in a way that changed the fix. A cold
agent's report is evidence, not a verdict, which is the standard the rest of the
plan already applies to a passing suite.

Separate PR because #147 was already merged; a push to a merged branch is silent.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 17:41:37 -05:00
4eec823741 Merge pull request 'docs(modules): close Phase 5 — the acceptance run, and the page shell it found' (#147) from docs/module-phase5-slice3 into main
Reviewed-on: #147
2026-08-12 19:34:02 +00:00
3116e7bbf6 docs(modules): close Phase 5 — the acceptance run, and the page shell it found
Slice 3 of Phase 5 (MODULE_SYSTEM.md 2.11.1), and the phase's last slice.

docs/modules/kit-acceptance.md is decision 5's deliverable: a cold agent given the
Integration Kit and the documents it links to — never core's source, never
module-uo — built a working module for a second game, which was then installed
into a real core and taken through MODULE_API.md 7.7's browser smoke. Verdict
recorded whichever way it went, and it went **yes, with caveats**: one pass, no
core source, and three of the four normative documents never opened.

The finding that justifies the two-stage shape is the one the agent structurally
could not reach, because it had no core to render against. A module page built
exactly as the kit teaches renders OUTSIDE the site: PublicLayout supplies the
chrome and not the body, and the `shell-... page-body` wrapper every core public
page writes for itself is two class names that appear in no contract. That is
3.4's own stated failure — "a module page that does not look like the site it is
installed in" — reached by following 3.4.

Fixed in core rather than documented at the reader, so the class names stay
core's private business and the theming workstream keeps its freedom to rename
them: PublicLayout takes an opt-in `shell` width, MODULE_API_VERSION 1.5.0
(website#148, merges first).

- MODULE_API.md 1.1: 1.5.0's entry, and a new bump-table row — adding an
  OPTIONAL prop or argument is minor. "A member's signature changes" is major
  because a call already written changes meaning, and an optional prop changes
  none; the table now says what it means rather than leaving it to be argued.
- MODULE_API.md 3.4: the shell prop, why a module names a width and never a
  class, and the eight-vs-seven miscount the run also turned up — the kit had
  faithfully carried it out of the contract into the template, which is the
  never-re-specify rule working exactly as designed on a wrong input.
- rust-dryrun.md: coreApi ^1.3.0 -> ^1.5.0, as a dated correction per decision 33.
  It is the only complete module.json in the kit's reading path and nothing
  checks a JSON block inside a Markdown file, which is the reusable half.
- MODULE_SYSTEM.md 2.11.1: slice 3 recorded, plus the third finding worth
  generalising — a check whose failure message asserts a diagnosis has to be
  right about it. `check:swagger` failed on a pristine template on Windows
  (CRLF) while blaming the routes, green on the Linux runner forever.
- Decision 34: core owns the page body as well as the chrome.

The banner does not come off. Decision 32 makes that a person's to remove, this
run exercised the website-module half only (the module has no sidecar, so
chapters 3 and 4 were never tested), and an agent does not skim or give up.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 14:28:26 -05:00
c6f4c2478f Merge pull request 'docs(modules): record the kit's extra checks and slice 2' (#146) from docs/module-phase5-slice2 into main
Reviewed-on: #146
2026-08-12 18:36:41 +00:00
abd04f04fd docs(modules): record the kit's extra checks and slice 2
Decision 2 of §2.11.1 still said the kit's CI "checks three things"; it runs five.
Two were added as the things they guard came into existence, and the pattern is
worth naming rather than leaving as CI archaeology: each exists because a class of
claim in that repo had nothing looking at it. checkRenameSites (slice 0) holds the
template's rename checklist against the tree in both directions.
checkChapterPaths (slice 2) requires every path a chapter names in backticks to
exist — not a markdown link, so the link check never saw those, and not code, so
nothing else did.

Its anchor list is STATED, not derived from the tree, for the reason API §3.6 gives
about the template's own build guard: a derived list cannot fail when what it
derives from changes, so a renamed `template/` would stop being checked at the
moment every mention of it became wrong. An anchor matching nothing fails the
build, the same rule §5.2's exemptions follow.

Also records Phase 5 progress (slices 0-2 landed; only the acceptance run is
outstanding) and slice 2's verification, which is the shape decision 5 scales up:
chapter 1's walkthrough was RUN against a real core on `edge`, including the three
failures the chapter tells a reader to cause on purpose, and the chapter now quotes
core's own messages for them.

Integration-kit half: Integration-kit#3.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 13:21:46 -05:00
5badb91a96 Merge pull request 'docs(modules): settle Phase 5 — the Integration Kit, and the sidecar rule' (#145) from docs/module-phase5-plan into main
Reviewed-on: #145
2026-08-12 14:40:54 +00:00
89aff5c26b docs(modules): settle Phase 5 — the Integration Kit, and the sidecar rule
Phase 4 closed; this is the plan for Phase 5, the Integration Kit, settled with
the org lead on six decisions.

MODULE_SYSTEM.md
- §2.11.1 (new): Phase 5's shape — measured starting state, the six decisions,
  the four slices, the acceptance mechanism.
- §2.11: layer 2 rewritten. The sidecar is the default and only path to a game,
  and it is a non-blocking dumb forwarder that persists before it forwards. A
  game that already ships a remote-control surface gets a thin sidecar, not none.
- §2.10: a MODULE_API_VERSION bump now carries a pass over the kit.
- Part 3: decisions 28–33.
- The Phase 5 line in Part 2's phase list: STARTED 2026-08-12.

MODULE_API.md
- §2.7 gains one prohibition: a module does not open a connection to a game
  server from the website process — a game socket, RCON, a query port, an
  engine admin API — with the three reasons it exists (the website is the
  internet-facing process, the sidecar owns the durable copy, neither side can
  stall the other). Normative prose with no CI behind it, stated so that every
  second module does not re-decide it.
- §1.1: the 1.4.0 entry. No member changed; minor deliberately.

modules/rust-dryrun.md
- §2 "Talking to the game" rewritten from "No sidecar" to a thin RCON sidecar,
  with a dated Correction note recording what the exercise originally concluded
  and why it was overruled. Corrected in view rather than silently rewritten.

Pairs with website#… (the 1.4.0 bump, into edge); either order.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 09:38:07 -05:00
91ac28e11d Merge pull request 'docs(modules): close Phase 4 — de-UO the backend reference, record the acceptance run' (#144) from docs/module-phase4-slice4 into main
Reviewed-on: #144
2026-08-12 14:19:16 +00:00
42867d7ef8 docs(modules): close Phase 4 — de-UO the backend reference, record the acceptance run
Phase 4 slice 4. BACKEND_DESIGN.md had never been de-UO'd: Phase 3 rewrote core's
code and README, but §5.2's identifier check reads code, not prose, so nothing
ever looked at the design document. It still described 27 shard_* tables, 20 UO
route rows and the shard visibility ladder as core's, a phase after core stopped
being able to serve any of them.

Moved, text unchanged:

  BACKEND_DESIGN §3   six shard_* schema sections (226 lines) → modules/uo/SCHEMA.md
  BACKEND_DESIGN §4   13 public + 7 admin UO route rows       → modules/uo/API.md
  BACKEND_DESIGN §6.5 the audience ladder (70 lines)          → modules/uo/API.md §4

Core keeps the seam and gains the eight /admin/modules routes it had never
documented. §6.5 becomes "Module-owned audience boundaries": core's security
boundary ends at authentication, roles and the session, and a module that serves
game data brings its own.

Also fixed on the way: users.router.js was still listed as 15 routes (it is 9 —
six went to the extension slot), and the push section still promised
config/shardStreams.js "moves out with it" four slices after it left.

The acceptance table now carries results. Criterion 2 was proved for real against
module-uo v0.3.0 on an empty database, which turned up the uninstall ordering
defect fixed in website#146.

AI disclosure: this contribution was AI-assisted (Claude Code).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 09:16:29 -05:00
49ab5a1266 Merge pull request 'docs(website): record phase 4 slice 3, and the boot-order defect it created' (#143) from docs/module-phase4-slice3 into main
Reviewed-on: #143
2026-08-12 13:02:00 +00:00
102ea6db19 docs(website): record slice 3, and the boot-order defect it created
MODULE_SYSTEM.md §2.7.2 gains the slice 3 write-up: why the id and version are
written out in MODULES (the offline no-op has to be a file read, not a fetch),
the four decisions, and the table of what the smoke proved against the real
v0.3.0 release on a fresh database.

Two sections are corrected rather than extended. §2.5's "declarative module set"
promise did not anticipate that the two install surfaces need a rule about who
wins — the declaration owns the volume, the row owns whether a module runs — and
decision 4's "resolution runs before the server starts" is true of the SCAN, not
of the process: it runs inside start(), which is what buys it the database.

MODULE_API.md §2.6 no longer says the fragments are replayed by ensureSchema().
They are, for every caller except the server, which scans the volume later than
it ensures the schema and so replays them itself. That was a live defect for an
afternoon: it announced itself only as the "no module scan in this process" skip
line, which is correct output for `npm run seed` and means the opposite in a
booting server, and on a database that already had the tables the module started
perfectly. Nothing in the contract moves, so MODULE_API_VERSION is unchanged.

BACKEND_DESIGN.md: the boot sequence in the tree, and MODULES beside the
module_source_hosts setting it installs through.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 07:57:21 -05:00
245be5dc91 Merge pull request 'docs(website): phase 4 decisions 5-6, slices 1-2, and a rule main currently gets wrong' (#142) from docs/module-phase4-slices into main
Reviewed-on: #142
2026-08-12 09:11:27 +00:00