feat: declare rust as the module's identity capability (phase 5, D16) #5

Merged
whitlocktech merged 1 commits from feat/phase-5-capability into edge 2026-09-17 09:22:14 +00:00
3 changed files with 40 additions and 1 deletions

View File

@@ -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 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. 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 `/<id>/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 ## Build and check
```bash ```bash

View File

@@ -12,5 +12,5 @@
"admin": ["/rust"], "admin": ["/rust"],
"player": ["/rust"] "player": ["/rust"]
}, },
"capabilities": ["servers", "killfeed", "leaderboard", "presence", "wipes"] "capabilities": ["rust", "servers", "killfeed", "leaderboard", "presence", "wipes"]
} }

View File

@@ -148,3 +148,20 @@ test('the modules protocol version agrees with the manifest it ships beside',
assert.strictEqual(typeof sidecar.PROTOCOL_VERSION, 'number') assert.strictEqual(typeof sidecar.PROTOCOL_VERSION, 'number')
assert.ok(sidecar.PROTOCOL_VERSION >= 1) 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',
)
})