feat(teams): ingest guild rank, and report every leader rather than one #10

Merged
whitlocktech merged 1 commits from feat/protocol4-guild-rank into edge 2026-08-17 22:48:56 +00:00
Member

The module half of the Protocol 4 rank amendment. Same wire version — Protocol 4 is unreleased on edge, so it is amended rather than bumped.

Spec: docs #155 · pairs with servuo-plugins #13

Stacked on #9. This branch is cut from feat/teams-phase2-provider, so until #9 merges the diff here shows its commit too. Merge #9 first; the diff then collapses to the two commits below.

What

shard_guild_members gains rank, rank_cliloc and rank_name, and the provider answers the question it previously could not: getTeamLeaders() returns every member at rank 4, not just the board's single leader_serial. That limitation was the entire reason the wire grew a per-member rank.

The board's leader_serial is folded in as a floor rather than replaced — it comes from a different frame, so on a shard whose roster has not been re-emitted since the amendment it is the only leadership signal there is, and moving to ranks must not lose it.

NULL rank is a real state, and it is load-bearing

The shard withholds the rank for a staff account, because ServUO's PlayerMobile.GuildRank reports Leader for anyone at GameMaster or above whatever their real rank. Every layer preserves that:

  • the ingest stores NULL rather than defaulting to 0 — a demotion this code would have invented;
  • leader requires an integer rank ≥ 4, so absence is never leadership;
  • the leaders query compares on rank, and NULL is excluded by the comparison.

Reading a missing rank as either 0 or "leader" republishes exactly the lie the shard went out of its way not to send.

Rank labels

Three sources in order: a custom rank's literal string → the operator's cliloc table → the five standard names. The last exists because the cliloc table is populated only if someone ran the client-file extraction, and a roster on a shard that has not should still read "Warlord" rather than nothing. A failing lookup falls back rather than failing the roster — a label is decoration, and losing it must not lose the data.

rank is backticked everywhere, like int on shard_online: reserved in MySQL 8, merely a keyword in MariaDB, so it parses bare here and must not be relied on to.

The fragment carries ALTERs as well as the CREATE. No production install has this table — it is new in an unreleased protocol — but edge deployments do, from the roster work that landed before the amendment, and CREATE TABLE IF NOT EXISTS adds a table and never a column. Same gap the sidecar's own store hit with guilds.members.

Verification

Unit tests stub the db layer, so the round trip was proved separately: the verbatim roster frame captured from the live ServUO run was fed through the real ingest into MariaDB and read back through the provider.

stored:    0x1F5 rank=4   0x1F6 rank=3   0x1F7 rank=2   0x1F8 rank=1
           0x1F9 rank=NULL (the GameMaster)             0x2E0 rank=4

provider:  leaders = [0x1F5, 0x2E0]      <- two, which the board alone cannot express
           labels  = Leader / Warlord / Emissary / Member, with no cliloc table
           0x1F9   = not a leader, no label

9/9 checks. Suite 413 → 421 tests, all passing.

One thing to note about #9

That same run found a latent bug in #9 as originally pushed: the provider's db layer built its query helper as core.db.query(...), and the facade has no db member. Every call threw, and the provider's own error handling turned the throw into a perfectly valid { ok: false } refusal — so core would have accepted it, held its projection and reported staleness. A provider that answers correctly and never returns data, forever, with nothing louder than a warning in the log. Invisible to the unit tests because they stub every db function.

Fixed on #9's own branch (c6929c6) so that PR is correct standing alone.


AI-assisted: written with Claude Code. Commits carry Co-Authored-By: Claude <noreply@anthropic.com>.

The module half of the Protocol 4 rank amendment. Same wire version — Protocol 4 is unreleased on `edge`, so it is amended rather than bumped. Spec: docs #155 · pairs with servuo-plugins #13 > **Stacked on #9.** This branch is cut from `feat/teams-phase2-provider`, so until #9 merges the diff here shows its commit too. **Merge #9 first**; the diff then collapses to the two commits below. ## What `shard_guild_members` gains `rank`, `rank_cliloc` and `rank_name`, and the provider answers the question it previously could not: **`getTeamLeaders()` returns every member at rank 4**, not just the board's single `leader_serial`. That limitation was the entire reason the wire grew a per-member rank. The board's `leader_serial` is folded in as a floor rather than replaced — it comes from a different frame, so on a shard whose roster has not been re-emitted since the amendment it is the only leadership signal there is, and moving to ranks must not lose it. ## NULL rank is a real state, and it is load-bearing The shard withholds the rank for a staff account, because ServUO's `PlayerMobile.GuildRank` reports Leader for anyone at GameMaster or above whatever their real rank. Every layer preserves that: - the ingest stores NULL rather than defaulting to 0 — a demotion this code would have invented; - `leader` requires an integer rank ≥ 4, so absence is never leadership; - the leaders query compares on `rank`, and NULL is excluded by the comparison. Reading a missing rank as either 0 or "leader" republishes exactly the lie the shard went out of its way not to send. ## Rank labels Three sources in order: a custom rank's literal string → the operator's cliloc table → the five standard names. The last exists because the cliloc table is populated only if someone ran the client-file extraction, and a roster on a shard that has not should still read "Warlord" rather than nothing. A failing lookup falls back rather than failing the roster — a label is decoration, and losing it must not lose the data. `rank` is backticked everywhere, like `int` on `shard_online`: reserved in MySQL 8, merely a keyword in MariaDB, so it parses bare here and must not be relied on to. The fragment carries ALTERs as well as the CREATE. No production install has this table — it is new in an unreleased protocol — but **`edge` deployments do**, from the roster work that landed before the amendment, and `CREATE TABLE IF NOT EXISTS` adds a table and never a column. Same gap the sidecar's own store hit with `guilds.members`. ## Verification Unit tests stub the db layer, so the round trip was proved separately: the **verbatim roster frame captured from the live ServUO run** was fed through the real ingest into MariaDB and read back through the provider. ``` stored: 0x1F5 rank=4 0x1F6 rank=3 0x1F7 rank=2 0x1F8 rank=1 0x1F9 rank=NULL (the GameMaster) 0x2E0 rank=4 provider: leaders = [0x1F5, 0x2E0] <- two, which the board alone cannot express labels = Leader / Warlord / Emissary / Member, with no cliloc table 0x1F9 = not a leader, no label ``` 9/9 checks. Suite 413 → 421 tests, all passing. ## One thing to note about #9 That same run found a **latent bug in #9 as originally pushed**: the provider's db layer built its query helper as `core.db.query(...)`, and the facade has no `db` member. Every call threw, and the provider's own error handling turned the throw into a perfectly valid `{ ok: false }` refusal — so core would have accepted it, held its projection and reported staleness. **A provider that answers correctly and never returns data, forever**, with nothing louder than a warning in the log. Invisible to the unit tests because they stub every db function. Fixed on #9's own branch (`c6929c6`) so that PR is correct standing alone. --- AI-assisted: written with Claude Code. Commits carry `Co-Authored-By: Claude <noreply@anthropic.com>`.
wtclaude added 2 commits 2026-08-17 22:44:59 +00:00
fix(teams): take query from the core facade, not a core.db that does not exist
Some checks failed
PR Checks / client-build (pull_request) Successful in 23s
PR Checks / server-tests (pull_request) Successful in 29s
PR Checks / frozen-manifest (pull_request) Failing after 40s
c6929c6bae
The Team provider's db layer built its query helper as `core.db.query(...)`. The
facade has no `db` member -- every other *.db.js in this module destructures
`query` from it directly -- so every call threw `Cannot read properties of
undefined (reading 'query')`.

The failure mode is the bad part. That throw is caught by the provider's own
error handling and turned into `{ ok: false, reason: 'roster unreadable: …' }`,
which is a perfectly valid refusal -- so core would have accepted it, held the
projection it had, and reported staleness. A provider that answers correctly and
never returns data, forever, with nothing in any log louder than a warning.

Invisible to the unit tests because they stub every db function, so the helper
was never called. Found by running a real roster frame through the ingest and
then asking the provider what it saw, against the real database.

Co-Authored-By: Claude <noreply@anthropic.com>
feat(teams): ingest guild rank, and report every leader rather than one
Some checks failed
PR Checks / client-build (pull_request) Successful in 15s
PR Checks / server-tests (pull_request) Successful in 20s
PR Checks / frozen-manifest (pull_request) Failing after 34s
99d1ca25a7
The module half of the Protocol 4 rank amendment (servuo-plugins, same wire
version -- Protocol 4 is unreleased on `edge`, so it is amended rather than
bumped).

`shard_guild_members` gains `rank`, `rank_cliloc` and `rank_name`. The provider
then answers the question it previously could not: `getTeamLeaders()` returns
EVERY member at rank 4, not just the board's single `leader_serial`. That
limitation was the whole reason the wire grew a per-member rank -- TEAMS.md §2.5
treats multiple leaders as the normal case and core has always supported them.

The board's `leader_serial` is folded in as a floor rather than replaced. It
comes from a different frame, so on a shard whose roster has not been re-emitted
since the amendment it is the only leadership signal there is, and moving to
ranks must not lose it.

## NULL rank is a real state, and it is load-bearing

The shard withholds the rank for a staff account, because ServUO's
`PlayerMobile.GuildRank` reports Leader for anyone at GameMaster or above
whatever their actual rank. Every layer here preserves that:

  - the ingest stores NULL rather than defaulting to 0, which would be a
    demotion this code invented;
  - `leader` requires an integer rank >= 4, so absence is never leadership;
  - the leaders query compares on `rank`, and NULL is excluded by the comparison.

Reading a missing rank as either 0 or "leader" would republish the exact lie the
shard went out of its way not to send.

## Rank labels

Three sources, in order: a custom rank's literal string, then the operator's
cliloc table, then the five standard names. The last exists because the cliloc
table is populated only if someone ran the client-file extraction, and a roster
on a shard that has not should still read "Warlord" rather than nothing. A
failing lookup falls back rather than failing the roster -- a label is decoration,
and losing it must not lose the data.

`rank` is backticked everywhere it is written, like `int` on shard_online: it is
reserved in MySQL 8 and merely a keyword in MariaDB, so it parses bare here and
must not be relied on to.

The schema fragment carries ALTERs as well as the CREATE. No production install
has this table -- it is new in an unreleased protocol -- but `edge` deployments do,
from the roster work that landed before the amendment, and CREATE TABLE IF NOT
EXISTS adds a table and never a column. Same gap the sidecar's own store hit when
`guilds.members` was added.

## Verification

The unit tests stub the db layer, so the round trip was proved separately: the
VERBATIM roster frame captured from the live ServUO run was fed through the real
ingest into MariaDB and then read back through the provider.

  stored:   0x1F5 rank=4  0x1F6 rank=3  0x1F7 rank=2  0x1F8 rank=1
            0x1F9 rank=NULL (the GameMaster)  0x2E0 rank=4
  provider: leaders = [0x1F5, 0x2E0]   <- two, which the board alone cannot express
            labels  = Leader / Warlord / Emissary / Member, with no cliloc table
            0x1F9   = not a leader, no label

9/9 checks. Suite 413 -> 421 tests, all passing.

Refs docs/link/v4.md §2.3, docs/website/TEAMS.md §2.5

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 0d618599cf into edge 2026-08-17 22:48:56 +00:00
whitlocktech deleted branch feat/protocol4-guild-rank 2026-08-17 22:48:57 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-uo#10
No description provided.