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', + ) +})