From 1444c7741396abdc37f06aa43e3be9ed27001b69 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 1 Aug 2026 00:58:53 -0500 Subject: [PATCH] docs(link): the shard-name fallback, the atlas places shape, and two traps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records what the live Protocol 3.0 smoke test (ServUO + sidecar + website + AVD) turned up, so none of it has to be rediscovered. v3.md §5.3 — the ruleset `shard` field now falls back to the instance's own name when the shard publishes ServUO's stock "My Shard", why that is done at ingest rather than on read (the frame is also broadcast live), and why the backfill snapshot must go through the dispatcher instead of writing state directly: a direct call made it a second writer that skipped the normalization. v3.md §7.4 — an unscored board renders a placeholder row rather than a blank card, and why it is deliberately not shaped like a real entry. PLAN.md §9 M11 — `places` is a list of {facet,label,spawners,maxAlive} OBJECTS, not of place-name strings, and typing it `List` makes the whole detail route fail to decode while the request itself returns 200. Adds the rule that came out of it: decode tests must feed real captured JSON, because the fakes build DTOs in Kotlin and can never catch a wire mismatch. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP --- android/PLAN.md | 14 +++++++++++--- link/v3.md | 27 +++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/android/PLAN.md b/android/PLAN.md index 47782a1..af75c06 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -970,14 +970,22 @@ push, and Play (M6–M8) follow the designed app. (`atlas`). Note the path: `/public/atlas`, **not** `/public/shard` — the atlas is static shard *content*, not live shard *state*, and unlike `/shard/*` it **is** `siteMode`-gated like `/posts` and `/wiki`, so a site in maintenance mode withholds it independently of the sidecar. - Two units/naming traps from `v3.md` §6.3: respawn delays are **seconds** throughout, and - `points` is a *count* on the search route while `spawners` is the *list* on the detail route. + Three traps. Two are units/naming, from `v3.md` §6.3: respawn delays are **seconds** + throughout, and `points` is a *count* on the search route while `spawners` is the *list* on + the detail route. The third is a **shape**: `places` is a list of + `{facet, label, spawners, maxAlive}` **objects**, not of place-name strings — it is the + aggregate the screen exists to show ("Shrines, Isamu-Jima, Yew"), it arrives only on the + detail route, and typing it `List` makes that whole route fail to decode while the + request itself returns `200`. - **Verification** — the five-rung walk (`anonymous`, `logged_in`, `player`, `staff`, `admin`) against a local website on the cutover branch, per [`../link/v3.md`](../link/v3.md) §11 and the shard-visibility smoke harness; plus one pass with **every feature disabled** in Admin → Shard Visibility, confirming the app *hides* each surface instead of erroring on it. Unit tests cover the menu filter (role × feature set), the - `404`/`403`/`503` mapping, and DTO decode for each new shape. + `404`/`403`/`503` mapping, and DTO decode for each new shape. **Decode tests must feed real + captured JSON**, not DTOs built in Kotlin: the fakes under `data/api/fake/` construct objects + directly, so they can never catch a wire/type mismatch — which is how the `places` shape above + shipped past a green suite. - **Excluded**, in the same class as M10's exclusions: the admin *configuration* panels — Shard Visibility, Spawn Atlas and Cliloc import — alongside the hero/CMS block editor, Discord-bot config, uo-link config and OAuth-provider setup. diff --git a/link/v3.md b/link/v3.md index 284f886..cd79d2a 100644 --- a/link/v3.md +++ b/link/v3.md @@ -350,8 +350,11 @@ Sidecar — `store.rs`: singleton `ruleset(id CHECK(id=1), rev, json, updated_t) `main.rs`: new arm in the board-projection match; `web.rs`: `GET /ruleset` served from the store, so it answers during a shard outage (`PROTOCOL_2.md` §12.2). -Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so follow the -`getPresence()` block's explicit form, not the array-only `snapshot()` helper); `shardIngest.js` → +Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so it cannot use the +array-only `snapshot()` helper — but it **must still go through `shardIngest.ingest()`**, as +`ingestEach` does, rather than calling `shardState.setRuleset` directly: the two arrival orders have +to produce the same stored frame, and a direct call quietly made backfill a second writer that +skipped the normalization below); `shardIngest.js` → `shardState.setRuleset`, **not** in `LOGGED_KINDS` (it re-arrives every reconnect and `server.hello` already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_ruleset` singleton table (`rev`, `expansion`, `payload JSON`, `t`); `GET /public/shard/ruleset` behind @@ -360,6 +363,17 @@ already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_rulese Client — NEW `routes/public/Rules.jsx` at `/site/rules`, alongside `/site/champs|guilds|governors|houses`; live via `useShardFeed({ filter: new Set(['world.ruleset']) })`. +**The `shard` field falls back to the instance's own name.** ServUO ships `Server.cfg` with +`Name=My Shard`, so an operator who never edited it publishes that verbatim — which is the shard +saying *unnamed*, not naming anything, and the rules page then reads "My Shard" under a header +carrying the real one. `shardIngest` substitutes `settings.getInstanceName()` (the admin-editable +site title, else `BRAND_NAME` — the same resolution `getPublic().brand.name` uses, so one install +never shows two names) when `shard` is absent, blank, or exactly the stock default, matched +case-insensitively and trim-tolerantly but only as a **whole** value: a shard genuinely called +*"My Shard Reborn"* has named itself and keeps it. Applied at **ingest**, not on read, because the +ruleset is also broadcast live — the same object goes to the SSE fan-out, so a read-time +substitution would be undone by the next reconnect's frame. + ### 5.4 Risk Perf is nil (~3 KB per connect). The only real risk is publishing a secret, mitigated by the explicit @@ -638,6 +652,15 @@ Client — NEW `routes/public/Leaderboards.jsx` at `/site/leaderboards`; a "Loya added to `components/CharacterSheet.jsx`, one edit serving both `PlayerCharacter.jsx` and `AdminCharacter.jsx`. +**An unscored board still renders a row.** Most systems on a young shard have `top: []`, and a page +of blank cards reads as broken rather than as new — so a board with no entries shows a single +placeholder bearing the **instance's own name** with an em dash where a score goes, above the +existing "nobody has earned points here yet" line. It is deliberately **not** shaped like an entry — +no rank, no medal, no bar, muted — because a placeholder that looked like a real standing would be a +fabricated one; the first real entry replaces it outright. Purely presentational: the API keeps +sending an empty `top`, so no consumer ever receives an invented row. Web and app render it the same +way (`Leaderboards.jsx`, `LeaderboardsScreen.kt`). + ### 7.5 What the run against a real shard changed The plan above was written from reading `PointsSystem.cs`. Booting the actual shard (ServUO 57.4, a