docs(rust): phase 9 as built — protocol 6 and first-party clans as Teams
PLAN.md §24.4-24.7: walked on the Oxide rig end to end and on Carbon hook by hook; what the rig found (the refusal-first reconcile, the dropped first feed line and its re-offer, core's 40-character dedupe key, one module per site). R6 amended by D47. PROTOCOL.md §12 specifies protocol 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -221,6 +221,10 @@ upstream pull request adding a read method, not a file reader.
|
||||
|
||||
### R6 — the required base set: Kits, Clans, PopupNotifications and ZoneManager
|
||||
|
||||
> **Amended 2026-09-23 by D47 (§24):** the uMod **Clans** plugin is no longer required. It moved to
|
||||
> phase 17's optional tier, because it shares no data with the game's own clans, which are what
|
||||
> Teams are built from. The required set is Kits, PopupNotifications and ZoneManager.
|
||||
|
||||
**Named 2026-09-15 (org lead), extended the same day by R17.** All four are MIT; three are k1lly0u's
|
||||
and BetterChat's author differs only in the optional tier. All fetched at plan time:
|
||||
|
||||
@@ -987,7 +991,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.* **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 |
|
||||
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* ✅ **Built 2026-09-23 — plan, as built and findings in §24** (D47–D58). Walked on the Oxide rig end to end and on Carbon hook by hook; the game's 100-clan ceiling accepted (D55); **one module per site** became a core rule on the way (§24.5). 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 |
|
||||
@@ -3501,6 +3505,105 @@ The phase row's criterion: *the clan page is ours, core's contributions land in
|
||||
and every slot empty still reads correctly.* Walked against the Oxide rig with real clans, plus a
|
||||
Carbon load of the same plugin file.
|
||||
|
||||
|
||||
### 24.4 As built, 2026-09-23
|
||||
|
||||
Rust-Plugins, Rust-Link and Module-Rust on `feat/phase-9-clans` (into `edge`), and this document.
|
||||
**Core needed one change, and it is not a Teams change:** the org lead's rule that a site runs one
|
||||
module (below), shipped as its own website PR with its own docs PR.
|
||||
|
||||
**The criterion is met on a live rig.** Against the Oxide rig with the protocol-6 sidecar inside the
|
||||
container, and a core at the pinned sha running only this module, a rig-only helper plugin drove
|
||||
the game's own clan backend with synthetic Steam ids:
|
||||
|
||||
- Two clans founded, two members added, one promoted, one leaving, one kicked, one disbanded.
|
||||
Every hook fired (`rg.hooks`: created 2, added 3, left 1, kicked 1, colour 1, disbanded 1).
|
||||
- Core created a Team per clan, synced the rosters with the right leaders, marked the leaver and
|
||||
the kicked member departed, published the promotion from the board diff, and **archived the
|
||||
disbanded clan's Team** on the next complete answer.
|
||||
- The clan page renders signed out with the roster withheld and a sentence saying who may see it.
|
||||
Core's activity contribution is in `rust.clan.detail`; the notify and forum slots are empty for a
|
||||
visitor, and the page reads correctly without them.
|
||||
- The roster gate agrees on both surfaces, the module's page and core's
|
||||
`/public/teams/:slug/members`: hidden from an anonymous visitor and from a signed-in player
|
||||
outside the clan, shown to that player once their link is to a member, and shown to a
|
||||
moderator. Neither leaks a Steam id or an account id.
|
||||
- Admin: a moderator's `PUT` is 403, an unknown rung is 400, and `public` opens the roster to an
|
||||
anonymous visitor.
|
||||
|
||||
**On Carbon** (the other rig, Carbon 2.x): the byte-identical plugin compiled and loaded, and the
|
||||
same helper fired every clan hook with the right counts.
|
||||
|
||||
### 24.5 What the rig found that the plan did not say
|
||||
|
||||
**1. Core's first answer is a refusal, and that is correct.** The `clan.created` event reached core
|
||||
through the ingest cursor (every 5 s) before the first `clans` board reached it through the refresh
|
||||
(every 30 s). Core's reconcile asked the provider and was **refused**: no current board. Twenty
|
||||
seconds later the board arrived and the Teams were created. The alternative, answering from events
|
||||
alone, is exactly what the envelope contract forbids.
|
||||
|
||||
**2. A Team's first feed line would always have been dropped.** Core writes an activity item only
|
||||
for a Team it already holds, and a new clan's Team does not exist until core's next reconcile,
|
||||
debounced by up to 30 seconds. "Founded the clan", the first line of every clan's feed, was
|
||||
dropped on the live run (`teams activity push: dropped items`), as predicted. The module now
|
||||
re-offers the last ten minutes of clan events on every board refresh. On the rig, all four
|
||||
dropped lines landed on the next refresh, each once.
|
||||
|
||||
**3. Core clamps a `dedupeKey` to 40 characters, silently.** `MAX_DEDUPE = 40` in core's
|
||||
activity model. A readable key long enough to be unique (server, clan, creation time, kind, player,
|
||||
instant) is 80 or more characters, so it would be cut short into collisions, and a
|
||||
re-offer would then have deduplicated *different* events into one. The key is a sha1, exactly 40.
|
||||
`MODULE_API.md` does not state the limit; a kit reader would find it the same way.
|
||||
|
||||
**4. `occurredAt` is epoch milliseconds, not a `Date`.** Core reads `Number.isFinite(item.occurredAt)`
|
||||
and replaces anything else with now. A `Date` would have stamped every re-offered line with the time
|
||||
of the re-offer.
|
||||
|
||||
**5. Game ids restart with the database version, as §24.1 said, and the rig showed ids from 1.** The
|
||||
fresh rig numbered its first clan `1`, which is why `createdMs` is in the identity.
|
||||
|
||||
**6. The ceiling is the game's, and it is 100.** `ListTopClans` clamps its limit to 10..100 (D55).
|
||||
|
||||
**7. One Team provider per site, and a load order that made it worse.** Core holds a single provider
|
||||
and a second registration fails that module's **whole load**. Modules load alphabetically, so
|
||||
installing this module beside `module-uo` would have taken `uo` down, not `rust`. The org lead's
|
||||
rule, 2026-09-23: **a site runs one module.** Core's installer now refuses a module whose id
|
||||
differs from the installed one (409) and accepts only upgrades (`MODULE_SYSTEM.md` §2.5, decision
|
||||
35). This is its own website PR, not part of this phase.
|
||||
|
||||
**8. The uMod plugin's same-named hook does not reach the bridge, on either framework.** Raised in
|
||||
both of the plugin's shapes, the bridge's own count for `OnClanDisbanded` stayed at the one real
|
||||
disband, with nothing logged (PROTOCOL.md §12.4).
|
||||
|
||||
**9. A clan event must not move `last_seen`.** The ingest's `touchPlayer` moves it, which is right
|
||||
for a frame that says a player was on. A kick is done *to* somebody who may be offline, and
|
||||
`last_seen` is presence (§23). Clan events note names with an upsert that leaves `last_seen`
|
||||
alone.
|
||||
|
||||
### 24.6 What is not proven here
|
||||
|
||||
- **A real player's name on a feed line.** The synthetic Steam ids have none, so the rig's lines read
|
||||
"A player founded the clan". Named lines need a real player's account on the rig, which is a walk
|
||||
to run beside the §12.5 identity walk.
|
||||
- **Staff do not see the members-only feed.** Core's feed access is membership and grants; a
|
||||
moderator reads the roster (D48 includes staff) but not the members-only lines. That is core's
|
||||
rule rather than this module's, and it is written down rather than changed.
|
||||
- **The 100-clan ceiling was not reached on a rig.** It is the game's own clamp, read from its
|
||||
source, and the truncation path is covered by the suite.
|
||||
- **A Nexus server was not available.** Its refusal is read from the game's backend selection
|
||||
(`NexusServer.Started` chooses `nexus`), not walked.
|
||||
|
||||
### 24.7 Smaller things worth keeping
|
||||
|
||||
- **The rig helper** (`RgClanRig.cs`, rig-only, never shipped) is ninety lines against
|
||||
`ClanManager.ServerInstance.Backend`: create, add (invite plus accept), role, kick, colour,
|
||||
disband, list, and a fake uMod disband. It is the cheapest way to fire every clan hook without
|
||||
players, on either framework.
|
||||
- **swagger-autogen drops a backtick inside a parameter description** and reports success; the
|
||||
module's fragment check caught it (`2 annotation(s) swagger-autogen could not parse`).
|
||||
- `ListTopClans` returns a pooled list that the plugin does not return to the pool. The GC collects
|
||||
it; it is noted here so nobody "fixes" it into a double free.
|
||||
|
||||
---
|
||||
|
||||
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
|
||||
|
||||
Reference in New Issue
Block a user