feat(store): persist world.ruleset and serve it from GET /ruleset #17

Merged
whitlocktech merged 1 commits from feat/ruleset-endpoint into edge 2026-07-28 20:35:33 +00:00
Member

Protocol 3.0 order 2 — docs/link/v3.md §5. Sidecar half of a four-repo change.

This is the first PR into the new edge branch in this repo (cut from main); edgemain is the v3 cutover.

What

The shard publishes one world.ruleset frame per connect describing how it is configured. The sidecar folds it into a singleton row and serves it back:

  • store.rs — a ruleset table with id INTEGER PRIMARY KEY CHECK (id = 1), so the singleton is enforced by the schema and an upsert can target id = 1 unconditionally.
  • main.rs — one more arm in the board-projection match, keyed on nothing (there is only ever one row); it keeps rev alongside the JSON.
  • web.rsGET /ruleset, behind the same gate as every other protected route.

Why store-backed rather than an RPC

Same reason as /guilds and /houses (PROTOCOL_2.md §12.2): a rules page that goes blank while the shard restarts is worse than one that is briefly stale. Verified below with plugin_connected: false.

{"ruleset": null} distinguishes "the shard has never published one" — an older plugin, or Bridge.RulesetEnabled=false — from a published ruleset. The website renders those differently, so it is deliberately not smoothed into {}.

rev (the shard's FNV-1a of the body) is kept so a reader can tell "same ruleset, re-sent on reconnect" from "the operator changed something" without diffing the JSON.

PROTOCOL_VERSION stays 2

The 2→3 bump is an operator-visible hard cutover — web.rs::gate 409s every protected route on mismatch and the website's WS closes on the ws.hello mismatch — so it happens exactly once, at the end of v3 (§4), not per phase. INTEGRATION.md §2 now documents what that means for consumers during this window.

Verification

cargo build and cargo clippy --all-targets clean.

Smoke-tested on loopback against a fake shard (ports 17788/18080, isolated config + DB):

Check Result
GET /ruleset before any frame {"ruleset":null}
Frame ingested, read back full ruleset, nested blocks and arrays intact
Read with the shard disconnected still served — plugin_connected: false (the outage path)
X-UOLink-Version: 1 409
No Authorization 401

Then end to end into the website: backfill snapshot on WS connect, live SSE delivery of a changed ruleset, and the REST read reflecting the overwrite — see the website PR.

Sibling PRs

  • servuo-plugins #3 → the BridgeRuleset.cs emitter
  • website → ingest, the public route, and /site/rules
  • docs → INTEGRATION.md catalog + GET /ruleset, v3.md progress

  • AI-assisted: written with Claude Code (Claude Opus 5), reviewed before opening.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP

Protocol 3.0 order 2 — [`docs/link/v3.md` §5](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/edge/link/v3.md). Sidecar half of a four-repo change. This is the first PR into the new `edge` branch in this repo (cut from `main`); `edge` → `main` is the v3 cutover. ## What The shard publishes one `world.ruleset` frame per connect describing how it is configured. The sidecar folds it into a singleton row and serves it back: - `store.rs` — a `ruleset` table with `id INTEGER PRIMARY KEY CHECK (id = 1)`, so the singleton is enforced by the schema and an upsert can target `id = 1` unconditionally. - `main.rs` — one more arm in the board-projection match, keyed on nothing (there is only ever one row); it keeps `rev` alongside the JSON. - `web.rs` — `GET /ruleset`, behind the same `gate` as every other protected route. ## Why store-backed rather than an RPC Same reason as `/guilds` and `/houses` ([`PROTOCOL_2.md` §12.2](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/edge/link/PROTOCOL_2.md)): a rules page that goes **blank** while the shard restarts is worse than one that is briefly stale. Verified below with `plugin_connected: false`. `{"ruleset": null}` distinguishes *"the shard has never published one"* — an older plugin, or `Bridge.RulesetEnabled=false` — from a published ruleset. The website renders those differently, so it is deliberately not smoothed into `{}`. `rev` (the shard's FNV-1a of the body) is kept so a reader can tell "same ruleset, re-sent on reconnect" from "the operator changed something" without diffing the JSON. ## `PROTOCOL_VERSION` stays 2 The 2→3 bump is an operator-visible hard cutover — `web.rs::gate` 409s every protected route on mismatch and the website's WS closes on the `ws.hello` mismatch — so it happens **exactly once**, at the end of v3 ([§4](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/edge/link/v3.md)), not per phase. `INTEGRATION.md` §2 now documents what that means for consumers during this window. ## Verification `cargo build` and `cargo clippy --all-targets` clean. Smoke-tested on loopback against a fake shard (ports 17788/18080, isolated config + DB): | Check | Result | |---|---| | `GET /ruleset` before any frame | `{"ruleset":null}` | | Frame ingested, read back | full ruleset, nested blocks and arrays intact | | Read with the shard **disconnected** | still served — `plugin_connected: false` (the outage path) | | `X-UOLink-Version: 1` | `409` | | No `Authorization` | `401` | Then end to end into the website: backfill snapshot on WS connect, live SSE delivery of a changed ruleset, and the REST read reflecting the overwrite — see the website PR. ## Sibling PRs - servuo-plugins [#3](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/3) → the `BridgeRuleset.cs` emitter - website → ingest, the public route, and `/site/rules` - docs → `INTEGRATION.md` catalog + `GET /ruleset`, `v3.md` progress --- - [x] AI-assisted: written with **Claude Code** (Claude Opus 5), reviewed before opening. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
wtclaude added 1 commit 2026-07-28 19:41:10 +00:00
Protocol 3.0 §5 (docs/link/v3.md). The shard publishes one world.ruleset frame
per connect describing how it is configured; the sidecar folds it into a
singleton row and serves it back.

Store-backed rather than an RPC, for the same reason /guilds and /houses are
(PROTOCOL_2.md §12.2): a rules page that goes blank while the shard restarts is
worse than one that is briefly stale. `{"ruleset": null}` distinguishes "the
shard has never published one" — an old plugin, or Bridge.RulesetEnabled=false —
from a published ruleset, which the website renders differently.

`rev` (the shard's FNV-1a of the body) is kept alongside the JSON so a reader can
tell "same ruleset, re-sent on reconnect" from "the operator changed something"
without diffing.

PROTOCOL_VERSION stays 2. The 2→3 bump is a hard operator-visible cutover and
happens exactly once, at the end of v3 (§4), not per phase.

Smoke-tested against a fake shard on loopback: frame ingested, GET /ruleset
returns it with plugin_connected=false (outage path), and the route sits behind
the gate (409 on a version mismatch, 401 unauthenticated). cargo build + clippy
clean.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-28 20:35:27 +00:00
whitlocktech merged commit 41811d40af into edge 2026-07-28 20:35:33 +00:00
whitlocktech deleted branch feat/ruleset-endpoint 2026-07-28 20:35:34 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/link#17
No description provided.