From 8df850f73e9bdd7e56c4766c8e95d582338bfc3f Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 16 Sep 2026 21:55:17 -0500 Subject: [PATCH] feat: declare `rust` as the module's identity capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 is the Android app's leg of this module's read path (R10), and it gates its Rust navigation on one capability string the way `module-uo`'s five shard rows gate on `shard`. There was no such string here: the five this module declared all name a SURFACE, and core flattens every started module's capabilities into one list, so `servers` is a word another module could declare tomorrow and silently reveal these screens on a site that does not run Rust. `rust` is the string only this module can mean. It is asserted against `module.json`'s own `id` rather than a literal, so the two cannot drift. The README says why it is not redundant with `id`: `id` is a mount prefix, and MODULE_API.md §2.9 forbids a client inferring a route from a capability. Gating on `id` would quietly make those the same thing. Decided by the org lead as D16, 2026-09-16. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 --- README.md | 22 ++++++++++++++++++++++ module.json | 2 +- server/test/entry.test.js | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c914029..439adfa 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,28 @@ events, the live map, Discord commands — arrives phase by phase. **Nothing is has something behind it:** a declared trigger nothing emits and a declared slot nothing fills are both surfaces an operator can configure and then wait on, which is worse than an absent one. +### What a client feature-detects on + +`module.json` declares six capability strings, and `GET /api/v1/public/modules` hands them to any +client that asks — the website's own nav, and the Android app (`docs/modules/rust/PLAN.md` R10). +Five of them name a surface: `servers`, `killfeed`, `leaderboard`, `presence`, `wipes`. + +The sixth is `rust`, and it names **the module itself**. It looks redundant beside `id`, and it is +not, for two reasons worth writing down before somebody tidies it away: + +- **A client that asks "is this module installed" has nowhere else to ask.** Core flattens every + started module's capabilities into one list, so `servers` alone is a word another module could + declare tomorrow and silently reveal this one's screens. `rust` is the string that can only mean + this module, and it is the single gate a whole navigation group hangs on — exactly the job `shard` + does for `module-uo`. +- **`id` answers a different question.** It is a *mount prefix* (§2.1 requires it to equal the + directory core loads the module from), and `MODULE_API.md` §2.9 is explicit that a client must + never infer a route from a capability. Gating on `id` would quietly make the two the same thing, + and the day a client builds `//servers` from it, the contract that lets this module move its + own pages is gone. + +An unknown capability is absent, and no route is ever derived from one. + ## Build and check ```bash diff --git a/module.json b/module.json index b481b67..e71eb8b 100644 --- a/module.json +++ b/module.json @@ -12,5 +12,5 @@ "admin": ["/rust"], "player": ["/rust"] }, - "capabilities": ["servers", "killfeed", "leaderboard", "presence", "wipes"] + "capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes"] } diff --git a/server/test/entry.test.js b/server/test/entry.test.js index 6711c99..0dd71ef 100644 --- a/server/test/entry.test.js +++ b/server/test/entry.test.js @@ -148,3 +148,20 @@ test('the module’s protocol version agrees with the manifest it ships beside', assert.strictEqual(typeof sidecar.PROTOCOL_VERSION, 'number') assert.ok(sidecar.PROTOCOL_VERSION >= 1) }) + +test('an identity capability is declared, and it is the module id (phase 5, D16)', () => { + // Core flattens every started module's capabilities into ONE list, so a client + // asking "is this module installed" needs a string only this module can + // declare. `servers` is not that string — it names a surface, and another + // module could name it too — which is the whole reason this one exists beside + // the five surface words. + // + // It is asserted against `manifest.id` rather than against the literal "rust" + // so that the two cannot drift: the day the id changes, the capability a + // client gates a whole navigation group on has to change with it. + assert.ok( + manifest.capabilities.includes(manifest.id), + `module.json must declare "${manifest.id}" as a capability — it is the only string a client can` + + ' use to tell this module apart from any other, and the Android app gates its Rust rows on it', + ) +}) -- 2.49.1