docs(rust): phase 9 plan of record (PLAN.md §24, D47-D58)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-23 01:27:35 -05:00
parent a4c4476c3e
commit de25ed3276

View File

@@ -987,7 +987,7 @@ Each phase ends with its findings written down, as every workstream here does.
| 7 | **Site-owned permissions** (R2). ✅ **Built 2026-09-21 — as built and findings in §20.** Protocol 4: one verb carrying the whole desired set, diffed by the plugin and drained in bounded steps; groups mirrored as groups; drift reported with adopt and revoke; the `PermissionExists` pre-check as the mechanism; the permission hooks as a live drift *signal* rather than the record. Seven org-lead decisions (§20.0), four defects a browser walk found and a fifth a test did (§20.5). **The site's half is walked end to end, including a restart that emptied the store; the acceptance line below is NOT met** — it needs a second, non-admin Steam account on the rig (§12.5, §20.7) | all 3 + docs | A grant made on the website gates a third-party plugin in-game, survives a wipe, and behaves the same against Oxide's JSON store and Carbon's Protobuf/SQLite one |
| 7b | **Mod configuration from the site** (R18). ✅ **Built 2026-09-22 — as built and findings in §21.** Protocol 5: a recursive walk rooted at the framework's own `ConfigDirectory`, a form generated from the values, a raw tier, an explicit reload target, and a write that backs the set up, reloads, watches `OnPluginLoaded` and **restores everything automatically** when it does not arrive. Four org-lead decisions (§21.0), a span-splicing editor so no untouched float is ever rewritten (§21.1), and **four defects a browser found that 179 green tests did not** (§21.5) — the worst being that every save of the bridge's own config was refused while the page said otherwise. **The site's half is walked end to end and the plugin compiles and loads on the live Oxide rig; the acceptance line below needs the sidecar and the game on one host** (§21.4) | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why; a nested `<Mod>/x.json` is found and reloads the right plugin |
| 8 | **Android leg B** (R10), and the half of R2 a player may see. ✅ **Built 2026-09-22 — as built and findings in §22.** One drawer row under the player group shaped like `module-uo`'s own (the code card first, then what the code got them), gated on `rust` and `PLAYER`; the four refusals kept four pieces of advice; and **one new website route**, `GET /player/rust/permissions`, because phase 7 gave an operator every view of in-game privilege and a player none. Three org-lead decisions (§22.0), the scope arithmetic answered on the server so `inScope` exists once (§22.1), and **the rig moved the sidecar inside the game container** ([`INSTALL_RIG.md`](../../rust-link/INSTALL_RIG.md)), which retired the firewall wall phases 6, 7 and 7b each stopped at | Android-app + Module-Rust + docs | A player links from the app |
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* **Plan of record in §24** (D47–D58), incl. the 100-clan ceiling the game imposes (D55). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook — **the catalogue is §10**, including the in-game-popup question | Module-Rust + docs | The offline raid alert reaches the player whose base it was, and nobody else |
| 11 | **Android leg C** (R10). Inbox and notification preferences for Rust triggers | Android-app | A Rust notification arrives on a phone and can be switched off there |
| 12 | **Events: budgets, option sources and the leases** (§9). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised | Module-Rust + 2 | A leased value is observed changing in the running game and restored, per key; `rust.group.membership` expires without core asking |
@@ -3407,6 +3407,102 @@ own Integration-kit PR alongside this one.
---
## 24. Phase 9 — Teams from first-party clans (plan of record, 2026-09-23)
R5's phase. Rust's **own** clan system becomes core's Team provider: a clan is a Team, its page is
this module's, and core's three contributions (the notification control, the activity feed and the
forum) land in places the module names. It was preceded by the presence fix (§23), which this
phase's roster question produced, and it builds on that fix's settings table.
Three repositories and this document: [Rust-Plugins][rp], [Rust-Link][rl] and [Module-Rust][mr],
all into `edge`. Android is not in this phase; R10's legs trail by one, and the Team surface reaches
the app with core's own Team screens rather than a Rust-specific one.
### 24.0 The decisions this phase needed
| # | Decision |
|---|---|
| **D47** | **The uMod Clans plugin becomes OPTIONAL.** It moves out of `overlay.toml`'s `requires_plugins` and into phase 17's optional tier, which **amends R6**. The two systems share no data (the plugin never touches `ClanManager`), so a server running both splits its players across two unrelated clan systems. First-party clans are the only Teams source, and the admin surface warns when the plugin is loaded that its clans are not Teams. |
| **D48** | **A clan roster is shown to clan members and staff by default.** One fleet-wide operator setting with three rungs: `members` (the clan's own linked members, plus staff) · `signed_in` · `public`. It is §23's rule applied to a roster: a roster says who is in a clan and, inside its audience, who of them is on. **Widening it warns that it publishes online status too**, because core's `projectRoster` can withhold rows but not fields. |
| **D49** | **The clan activity feed carries membership only (founded, joined, left, removed), and every item is members-only.** Written with `visibility: 'members'` on every item. |
| **D50** | **Clan score and colour are in.** Score sorts the Clans tab and colour is shown as a swatch. **Deferred:** clan logos (images over the bridge into `ctx.uploads` is asset-bridge-shaped work that would roughly double the phase) and a Discord `/clan` command (stays in phase 16). |
| **D51** | **Protocol 6 is one board and five events.** A `clans` snapshot, re-sent on connect and on the 60-second cadence, plus `clan.created`, `clan.disbanded`, `clan.member.added`, `clan.member.left` and `clan.member.kicked`. The sidecar files by `type` (§8.1), so it only bumps its version. |
| **D52** | **`externalId` is `<serverId>:<clanId>:<createdMs>`.** The clan id alone is not an identity: the game's clan database is `clans.<version>.db` with the version hard-coded, so a Facepunch bump starts a fresh file whose ids restart at 1. Without the creation time, the new clan #1 would inherit the old clan #1's Team, its forum and its members-only history, and core would read the swap as a rename. |
| **D53** | **`getTeams` is `complete: true` only when every server's board is fresh**, supported and untruncated. Otherwise it answers `complete: false`, which core reads as "add and update, remove nothing". One server being offline must never archive its clans. When no server's board is fresh it refuses. |
| **D54** | **Leadership comes from the board, diffed one snapshot against the next**, and is published as `team.leader.added` / `team.leader.removed`. The game raises no promote or demote hook, so this is correct to within one cadence (about a minute). A leader is any member whose role has rank 1, and there may be several. The snapshot is shaped so phase 17 can sharpen it with the uMod plugin's `OnClanUpdate` rather than replace it. |
| **D55** | **The board is bounded, and its ceiling is the game's.** The game offers no "list every clan" call; the only listing is the clan leaderboard, which **the game clamps to 100 clans by score**. The org lead accepted that ceiling (2026-09-23) over reading the game's private SQLite schema directly. A server at 100 clans or more, or whose board would not fit on one line of the game link, reports `truncated: true` and the answer becomes partial: Teams there are added and updated but never removed. A **non-local clan backend** (Nexus) is refused with a reason rather than guessed at. |
| **D56** | **The pages are `/rust/clans/:externalId`, with a Clans tab on each server's page.** `pageUrlTemplate` substitutes only `{externalId}`, which rules out a route nested under the server. Three module slots are declared: `rust.clan.header` (core's `team.notify`), `rust.clan.detail` (`team.activity`) and `rust.clan.forum` (`team.forum`), one per place, as `module-uo` does. |
| **D57** | **Linking or unlinking an account calls `ctx.teams.reconcile`**, so a member's `userId` appears straight away instead of on core's next sweep. |
| **D58** | **The clan LIST is public; the roster is not.** Each clan's name, colour, score and member count names no player, so the Clans tab shows them to anyone (org lead, 2026-09-23). Rosters, leaders and who is online stay behind D48's audience. |
### 24.1 Facts the phase rests on
Read from the game's own assemblies (`Rust.Clans.Local.dll`, `Rust.Clans.dll` and `ClanManager`
in `Assembly-CSharp.dll`, decompiled off the Oxide rig), not from a catalogue:
- **`clan.enabled` defaults to `true`**, so every server has first-party clans unless an operator
turns them off. `ClanManager.ServerInstance.Backend` is null until the backend initialises.
- **The backend API has no enumerate-all call.** `IClanBackend` offers `Get`, `TryGet`,
`GetByMember`, `Create`, `ListInvitations` and `GetLeaderboard`. `GetLeaderboard(limit)` runs
`ListTopClans`, which is `SELECT clan_id, name, score FROM clans WHERE deleted IS NULL ORDER BY
score DESC LIMIT ?` with the limit clamped to **10..100**. That clamp is D55's ceiling.
- **The local backend is synchronous under an async signature.** Every `ValueTask` it returns has
already completed, so the plugin can read results on the main thread without blocking. A
backend whose task has not completed is treated as unsupported rather than waited on.
- **A leader is any member whose role has `Rank == 1`, and several are allowed.** The game only
stops you removing the last one. **Clans cannot be renamed**: `IClan` has no `SetName`.
- **Hook sites.** `OnClanCreated(LocalClan, ulong)` fires in `LocalClanBackend.Create` after the
commit. `OnClanMemberAdded(long clanId, ulong steamId)` fires inside
`LocalClanDatabase.AcceptInvite` with a **bare id**, before the cached `LocalClan` has been
refreshed. `OnClanMemberLeft`, `OnClanMemberKicked` and `OnClanDisbanded` fire from `LocalClan`.
**The founder's own membership fires no `OnClanMemberAdded`**, so `clan.created` implies it.
- **The uMod Clans plugin raises a hook with the SAME name.** It calls `OnClanDisbanded(string tag,
List<ulong> members)`, the same name and arity as the game's `OnClanDisbanded(LocalClan, ulong)`.
The bridge declares the game's types precisely, so a call with the plugin's types does not match
it. This is checked on the rig, which runs the plugin.
### 24.2 What the plan builds
**The plugin (protocol 6).** The `clans` board carries, per clan: `clanId`, `createdMs`, `name`,
`color` (`#rrggbb`), `score`, `maxMembers`, and `members` (each `steamId`, `rank`, `role`,
`joinedMs`, and `name` when the framework knows one). It also carries the board-level facts D53
and D55 need: `enabled`, `backend`, `supported`, `truncated`, and `umodClans`, which is whether the
optional plugin is loaded (D47's warning). A member's `LastSeen` is deliberately **not** sent: it
is presence (§23). The five events carry `clanId`, `createdMs`, the clan's `name` and the Steam
ids involved. A membership event also schedules a board re-send a couple of seconds later, so the
roster that follows it does not wait a minute.
**The sidecar.** `PROTOCOL_VERSION` becomes 6. Nothing else changes.
**The module.**
- **The store:** `rust_clans` and `rust_clan_members`, replaced per server from each board, plus
`rust_clan_boards`, which records per server when the board's `t` last advanced by the
**website's** clock. Freshness is judged by that, so clock skew between the game host and the
website cannot make a stale board look fresh.
- **The provider:** `getTeams`, `getTeamMembers`, `getTeamLeaders` and `projectRoster` (D48), plus
`pageUrlTemplate: '/rust/clans/{externalId}'`. `userId` comes from the module's own link table,
`online` from its own presence board, and `rankLabel` from the clan's role name.
- **The push:** each `clan.*` event is published to core (`team.created`, `team.disbanded`,
`team.member.added`, `team.member.removed`) and written to the Team feed as a members-only item
(D49), with a `dedupeKey` from the event's own id so a replay is a no-op. A board that changes
the set of clans asks core to reconcile, and a board that changes a leader publishes it (D54).
- **The pages:** the Clans tab (public list, D58) and `/rust/clans/:externalId`, whose roster is
projected by the same function core calls. The roster setting is a second section on the
existing **Rust visibility** admin page, which also lists any server running the uMod plugin.
- **The feed:** the five kinds are `staff` class in the public feed's allowlist. They reach the
public only through core's members-only Team feed.
**The docs.** This section as built, `rust-link/PROTOCOL.md` §12 (protocol 6), the R6 amendment
and `overlay.toml`'s requirement list.
### 24.3 Done when
The phase row's criterion: *the clan page is ours, core's contributions land in places we named,
and every slot empty still reads correctly.* Walked against the Oxide rig with real clans, plus a
Carbon load of the same plugin file.
---
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
---