UltimateGuildRoster-v0.4.0
What's new
Changelog
All notable changes to Ultimate Guild Roster. Work in progress sits under [Unreleased]; the
version number is decided at release prep.
[0.4.0] - 2026-10-17
Changed: the log is cleared by type (LOG-07, todo #4, docs/design/event-log.md "Clearing by type")
- The Log tab's three clear buttons and the date-range dialog are replaced by one "Clear by type" button
that any guild member may use on their own copy. It opens a dialog with a tick box per category and its
count, asks with the total, and clears up to the moment of the question (
EventLog:Clear(categories, upTo, bound),EventLog:Counts). Every type of a cleared category recordslogClearedAt, soAddand the GRM import never bring an older line of it back (EventLog:Cleared). - Three categories are split to match GRM at a minimum: rank into promote and demote, leave into left and
kicked, alts into alt links and main changes. Migration 8 (
SCHEMA_VERSION8) moves each saved log and chat switch onto its parts with the same value. The 20 filter chips fit two rows at the pane's width by shorter labels,CHIP_PAD2 and a 100 px search box (only measured with the harness's text metric). - Peer Review F2 (thread dc2520d3): a roster comparison's change is stored even when the game's guild log
dates it before a clear, since it is new by construction (
AddChangesdoes not askCleared). Peer Review N1 (thread 8d43f4c2): such a change arriving while the question is up is not removed, becauseClearlooks only at the entriesCountscounted (EL-23b). F3 is a known cost: a live line in the same server second as a clear is refused. - Peer Review A6 (thread 8d43f4c2):
Tests/settings_spec.luareads the category list out ofdocs/design/settings.mdrather than keeping a third copy, and assertsSavedVariables.CATEGORIESmatches it in order.
Added: SY-54, whole-client login catch-up over the hash protocol
- The last
pendingsync spec, which "waited on the protocol", is written: three examples inTests/sync_two_peers_spec.lua. A new client with nothing ends with every set's canon equal to an established client's, and receives a rename marker with noby(an old group holding only a tombstone) with the sender's canon (Peer Review thread c04b4fd1 reply 8444c31d, item 2); two clients each holding a no-push part the other lacks both end with the union and equal canons after one announce; and a third client relays an officer's part to a fourth while the officer is quiet. - No code change: the protocol it waited on was already built. Example 1 and 3 passed on their first run.
- The union example went red in the harness's default
"sync"delivery (run 20261017-141226.201): Alice's offer reached Bob inside his ownBroadcastItemHashessend, before DeltaSync opens the collect window after that call returns, so the offer was dropped. The game never delivers a reply inside the sender's own send, so SY-54 runs withwow.echoMode = "queued"and pumps each second, and passes (run 20261017-141343.635). The relay example's whole-set compare with the quiet officer was dropped, with the reason in the spec: the fourth client's own no-push login history never reaches a client that is not syncing. - Whole suite green, run 20261017-141723.062 (1601 passed, 0 failed, 2 pending). Not verified in game.
Added: sister guilds share members and the log, each guild's in its own store (SY-65, docs/design/sync.md "Sister guilds")
- Sharing with sister guilds is back on, the way decision 8 has it: the hash list goes out over GreenWall
and negotiation and data go by whisper.
Sync.SHAREDis{ members = true, log = true }, which also turns the GreenWall bridge on inSync:Start. - Each guild's copy of a shared set is its own DeltaSync item,
"<set>@<GuildId>"(Sync:WireKey,Sync:GuildId:ComputeHashV2of LibGuildRoster's"Faction-GuildName"key in lower case). Our own guild's wire key reads our set; a sister guild's copy is kept atsync.sisters[<guild key, lower case>][<set>], andSync:Mergecalls no applier and sends noUGR_SYNC_APPLIEDfor it, so a sister guild's leaver never becomes our former member and their log lines never enter our log (the case Peer Review S2 found, thread 51800e63). Nothing shows a sister guild's copy yet. Sync:IsSetPeeris replaced bySync:TakesandSync:Serves: plain set keys stay between home members; our guild's wire key is served to a sister guild's members and never taken from them; a sister guild's wire key is taken from that guild's members only and served to nobody.answerLeg3merges a leg-3 give only from someone it takes from,askLeg3gives only to someone it serves, and a live push is taken only for a plain key.Sync:MyHasheslists our guild's wire keys and our copy of each configured sister's after the plain sets;hasContentaccepts exactly those keys.- Peer Review S1 (thread 51800e63) fixed in UGR:
Sync:OnOfferReceivednow drops, from the table DeltaSync's P2P reads next (DeltaSync.lua_HandleOffer), every hash-list item we would not serve the sender and every hash-offer item we would not take from them. A sister member's offer of a plain set is never recorded, so it cannot crowd a guildmate's out of a dispatch. KNOWN COST: that the hook may editdata.itemsis what DeltaSync's code does today, not a documented contract (DeltaSync thread f3ec1986). - KNOWN COST: the bridge sends at most 10 messages a broadcast, sorted by key, and the list is the plain sets plus two entries per guild; with three or more sister guilds the keys sorting last may not be bridged (not measured against GreenWall's limit).
- Specs: SY-65 in
Tests/sync_sets_spec.lua(twelve examples), written red (run 20261017-124944.753: 13 failed,WireKeymissing and the bridge off).Tests/sync_spec.lua's bridge example now asserts the bridge is on, andTests/sync_hash_scan_spec.luaallowsSync:GuildIdas a hash site, as it doesSync:Bucket, since it hashes an identifier and never content; both changes say why in the spec. Whole suite green, run 20261017-125844.563 (1588 passed, 0 failed, 3 pending);Sync.lua687/687 lines.
Fixed: a sister guild's copy is dropped when the guild leaves the sister list (SY-67)
- Once a guild left LibGuildRoster's sister list, its copy at
sync.sisters[<key>]stayed in the saved data for good (self-audit, thread 6efe3151 item 4).Sync:PruneSistersnow drops every copy whose guildGetSisterGuildKeysno longer names, case-insensitively, and releases any open pull of that guild's wire keys (OnItemCompleted), so DeltaSync's slot is not held to its timeout;Sync.askingentries now keep the provider's name as DeltaSync gave it for that. It runs on LibGuildRoster'sOnSisterConfigChangedand at the start ofSync:Announce(so at login too), and does nothing while LibGuildRoster's home key is unresolved, when its list reads empty and every copy would go. - Spec SY-67 in
Tests/sync_sets_spec.lua(five examples, through LibGuildRoster's realSetSisterGuildNames): red first (run 20261017-134100.170, 4 failed: the copy stayed,PruneSistersmissing, nothing released), green in run 20261017-134129.123. Whole suite green, run 20261017-134508.434 (1598 passed, 0 failed, 3 pending);Sync.lua706/706 lines. - Peer Review follow-ups (thread a93c588f). N1:
PruneSistersalso drops nothing while LibGuildRoster holds no list at all (GetSisterGuildsTs() == 0, its saved data lost or fresh), which read as an empty list and dropped every copy before gossip could bring the list back; an officer emptying the list leaves a stamp above 0 and still drops them. N2:UGR_GUILD_CHANGEDreleases every open pull's DeltaSync slot before it resetsSync.asking, through onereleasehelper shared withPruneSisters. Three new SY-67 examples, red first (run 20261017-160019.106), and SY-51's 'merges nothing from the old guild's provider's leg 4' now expects the guild change's one release and still asserts the stale leg 4 completes nothing. P1: a mutant withoutstring.loweron the listed keys turns the case example red (run 20261017-160127.998). - Peer Review S3 (thread 777f80a0):
PruneSisterscalledGetSisterGuildsTsunguarded, so a LibGuildRoster too old to have it raised there. It now feature-detects the method, asCompat.CanWriteNotesdoes, and drops nothing without it: an unknown stamp must not read as an empty list. New SY-67 example, red first (run 20261017-172411.387,Sync.lua:390calling a nil method), green in run 20261017-172727.611.
Changed: SY-66 runs on the harness's GreenWall model (WoWAPITesting pin 42bebe3)
Tests/wowapimoved from 6bfbb37 to 42bebe3, which addsenv/greenwall.lua, GreenWall's own relay behaviour, built from UGR's request (WoWAPITesting thread 566f9c60).Tests/greenwall_relay.lua, UGR's stand-in, is deleted (in writ's trash) and SY-66 installsenv.greenwallwith Test Guild and Sister Guild, delivering withwow.pumpAddonMessages().- The model sends the hash list inside
Sync:Announce, so SY-66 now counts GreenWall sends from before the announce. It counts a send only when GreenWall'soksays another guild was reached, and a red run names each refusal's reason and size. Red first in run 20261017-135418.726: the count started after the announce, so it saw no sends. Green in run 20261017-135521.314. - The model's four differences from the stand-in, checked against DeltaSync's bridge: a
too-longrefusal is split and retried (DeltaSync.lua_BridgeHashList, :2187);ok == false("no-peers") counts as not sent (:2200); the sender's own echo and the home-guild copies are dropped (OnGreenWallMessage,if echo or guild, :2223); and the 10-second dedupe only drops an identical repeat. No finding for DeltaSync. Not verified in game. - Whole suite green, run 20261017-135923.536 (1598 passed, 0 failed, 3 pending);
Sync.lua706/706 lines. - Peer Review N1 and N2 (thread dc18be64): SY-66 counts only the reader's own relay sends (
from), and now also asserts that nothing a refused send carried was lost, by decoding each bridge message with AceSerializer and checking every refused key went out in a send that reached. Not "nothing refused": that went red in run 20261017-163054.944 on sixtoo-longrefusals, which are DeltaSync's designed halving split. Green in run 20261017-163303.284. - Peer Review N1 (thread 777f80a0): that check could not see a key DeltaSync drops past
maxMessages, because DeltaSync then never calls GreenWall, so the key is in no send at all. SY-66 now records every item key the reader's host hands_BridgeHashListduring the announce (watching the real call) and reports as lost each one no send that reached carried. Green in run 20261017-171647.207; a mutant capping the reader's bridge at one message turns both SY-66 examples red with seven lost keys (run 20261017-171952.288).
Fixed: sister-guild sync follow-ups from Peer Review (thread 6efe3151)
- G1: with 'Only send to ranks at or above mine' on, we offered a set to players we then refused to
answer, so their negotiation hung on every broadcast. I had the rank check in
OnDataRequestand not in the offer filter.Sync:Servesnow holdsIsSendPeerandOnDataRequestcallsServesalone, so the offer and the answer are one predicate. With the setting on, a sister guild's member is served nothing (their rank is not in our roster; SY-14: an unknown rank refuses). Red run 20261017-131258.988. - N3: a pull of a sister guild's copy now ends after one round. We give a copy's source nothing, so one round's leg 4 brings everything of theirs that is newer; asking again ran every round for nothing when our copy was the newer one. Red run 20261017-131412.564 (two asks).
- N1: a comment at
Sync:OnOfferReceived's filter says it covers only the hash protocol's two message types, so a switch to DeltaSync's numbered protocol must extend it. N2 is written into sync.md as a KNOWN COST: a sister guild's copy is never passed between home members. - SY-66 in
Tests/sync_two_peers_spec.lua: two whole clients in two guilds that list each other as sister guilds, the hash list carried byTests/greenwall_relay.lua(a GreenWallAPI v2 stand-in, until the harness models GreenWall). A sister member's join, leave and log line reach oursync.sisterscopy with the author's canon; nothing reaches our sets, members, former members or config; both ways round. It passed on its first real run, since SY-65's code already existed; a mutant that stores a sister copy in our own sets turns it red (run 20261017-131207.882). - Self-audit, a closed leak: with the rank setting on, a pull from a lower-ranked guildmate used to hand
them our groups in leg 3, because
askLeg3checkedSync:Serves, which then began withIsPeerand ignored rank. That is a send SY-14 forbids. Since G1,Servesholds the rank rule, so we give a lower-ranked guildmate nothing in leg 3 (Peer Review, thread f97de5af). I had not specced or noted that, and it brought N3's wasted rounds back for home peers. The one-round rule is now general:onLegfinishes after leg 4 whenever we do not serve the provider, which covers both cases. Spec in SY-65, red with three asks (run 20261017-132737.758), green with two (leg 1 and leg 3). - Whole suite green, run 20261017-133140.035 (1593 passed, 0 failed, 3 pending);
Sync.lua689/689 lines.
Fixed: no data set is taken from a sister guild; the GreenWall bridge is wired and off (SY-64, docs/design/sync.md "Sister guilds")
Sync:IsPeeralready passed a sister guild's members (IsInAnyRoster), and the receive path took any set from any peer. So a sister guild'sconfig(the approved list) could merge into ours and grant admin here, and since nomemberspart names the guild it was observed in, a sister guild's leaver would become our former member throughMembersadopt and be relayed to our guild (Peer Review S2, thread 51800e63). NewSync.SHARED(empty) andSync:IsSetPeer(setKey, name): a sister member is a peer for no set.OnDataReceivedandOnDataRequestcheck it;Sync:Askchecks it and releases a refused offer withp2p:OnItemCompleted(DeltaSync frees the slot by item key alone,P2PSession.lua:904-922).- DeltaSync v4.6.0 / MINOR 26 (thread 3551240e) can send the hash broadcast to the co-guilds over
GreenWall.
Sync:Startpassesgreenwall = C.FOLDER(GreenWall's addon id must be the TOC name, not the namespace) only whileSync.SHAREDnames a set, so today it is off: a bridged list could only start offers that are always refused. Sharing waits on a sister guild's data having its own store. - Specs: SY-64 in
Tests/sync_sets_spec.lua(four examples) andTests/sync_spec.lua(one). First written with members and log shared and the bridge on (red run 20261017-120946.020, green run 20261017-121344.178); rewritten the same session, with the reason in the spec comment, once adopt was traced: red run 20261017-123136.813 (4 failed), whole suite green run 20261017-123506.960 (1576 passed, 0 failed, 3 pending).
Tests: harness pinned to WoWAPITesting 6bfbb37
- The harness now installs
C_GuildInfo.GuildControlGetRankFlagsandguild.setRankFlags(UGR's request, inbox 44877338).Tests/member_history_sync_spec.lua's stand-in is gone; MD-44 sets rank flags throughguild.setRankFlagsand still removes the call for the older-client case. The only other entry crossed (around_each_filecomment) asks nothing. Whole suite green on the new pin (run 20261017-122509.836, 1576 passed, 0 failed, 3 pending).
Added: member history syncs (writ todo #14, docs/design/sync.md "Member history syncs", MD-37 to MD-44)
- Decision 11 for what the roster comparison records. New members-set parts of the "observation" class
(
Sync.ClassOf(setKey, groupId, partId)now takes the part id): any client may save them; a delete is still the admin gate.hist:join:<YYYYMMDD>,hist:left:<YYYYMMDD>,hist:rank:<YYYYMMDD>:<rankIndex>carry{ name, date, kind, actor }(andrankIndex,rankName);infocarries{ name, guid, class, race, level, rankIndex, rankName, publicNote, seen }. Sync:SavePartgainsnopush(sixth argument): observations reach other clients by negotiation only.Modules/Roster.lua: each join, rejoin, leave (part kind "left", "kicked" or "leftUnknown") and rank entry is shared throughMembers:ShareObserved(only when no part is held under that id).Members:ShareInforuns after every comparison for each current member, on a leave (whereseencounts) and in the live join, rank and level handlers. Entries go in throughMembers:Record, which replaces another client's copy of the same event instead of adding a second.Modules/Members.luaapplier: adds each received observation the record lacks assource = "sync",verified = false, in date order; never changes an entry this client recorded; the same family within one day is the same event ("joined" and "rejoined" count as one kind; a leave and a rejoin on consecutive days stay two entries).infofills a former member's fields and a current member's nil ones (type-checked). A group for nobody whose newest join or left part, officer's or observed, is a leave is adopted as a former member.applyDateno longer fills a peer's observation with an officer's date.- Tombstones:
ClearJoinDate/ClearPromotionDatealso tombstone the observed ids of that event within one day; the applier removes the entry a tombstoned id names unless it is verified or another live part of that event remains. - Backfill:
Members:ShareHistory, once per guild after the first complete comparison (tracking.historyShared, a new AceDB default, no migration: migrations run inOnInitialize, before the guild key resolves), saves in oneSync:SavePartsbatch a part for every dated entry of every record (sync-added and officer-corrected entries excluded) at its ownsetAt, and oneinfoper record at its newestsetAt. - Officer notes travel only to ranks that may view them (Peer Review P1, thread 81e1e4c4): a fifth set,
onotes, grouponote:<name lower-cased>, partnote={ name, text }, saved with push off by a player who can view officer notes, only when changed. A player who may not view officer notes neither lists, offers, asks for, takes nor answers it (Sync.luaholdsSet); one who may answers and pushes it only toSync:IsNotePeer: a send peer at or above the player's rank whose rank flags do not bar viewing (ns.Compat.RankCanViewOfficerNote, flag 11 ofC_GuildInfo.GuildControlGetRankFlags).Sync.APPLIERS.onotesfills a former member'sofficerNote. sync.md's log section records that officer-note text in log lines must follow the same rule when log sync is built. - A v0.3.0 client (read at the tag, not run) stores and relays the new members-set parts without applying
them, and never offers or takes
onotes, which it does not list. - Refinements written into sync.md: a received "joined" is held once any "joined" is, a "seenAtRank" once a rank entry at that rank is (install-day first sightings would otherwise be extra joins).
- Specs:
Tests/member_history_sync_spec.lua(new), red first (runs 20261017-110518.019, 14 failed; 20261017-114820.140, 4 failed for theonotesset), then green (whole suite 20261017-115938.101, 1571 passed, 0 failed). Changed because the login comparison now writes the members set:bans_specBN-01 andgrm_import_specIM-12 compare before/after;sync_protocol_spec,sync_sets_spec,sync_tab_specempty the members set after login;sync_sets_spec's set list (SY-43) and applier list gainonotes. The harness has noGuildControlGetRankFlags; the spec stages a stand-in. - Harness pin moved
dd620e5to549bbc9. - Not verified: in game; with a real v0.3.0 client; whether a non-officer reads other ranks' real flags;
a re-announce between two online harness clients did not start a negotiation (offer arrived after the
collect window), so MD-39 starts it as
onSyncAccepteddoes.
Added: the macro tool, a Macro tab and its run dialog (todo #13, docs/design/macro-tool.md MC-07, MC-11 to MC-17)
- Vishiswaz's request.
Modules/MacroQueue.luagainsMACRO_BYTES(255, a chosen bound; the real macrotext limit is not measured),Scan(action, now)(readsMacroRules:Rulesonce per pass and queues each member a rule matches, never one at or above the player's rank or the player),MacroText(items),Peek(),Drop(item),ClearFinished()and theUGR_MACRO_QUEUE_CHANGEDmessage. Peer Review Q1 (thread 18d2d5bc):Checknow sends it when an item's status changes andStartBatchalways does, so the tab repaints a landed action and an open run dialog re-lists. APICompat.lua:Compat.GuildActMacrobuilds/gremove,/gpromoteor/gdemotefrom the client'sSLASH_GUILD_UNINVITE1/PROMOTE1/DEMOTE1globals (names confirmed from the classic_era source by Peer Review; the enUS fallback spellings are not verified).GuildRemoveMacronow calls it.GUI/MacroRun.lua(new): the run dialog, a session-owned form dialog holding a secure button. Its macro text is empty at rest. Out of combatpreClicksettles the queue, then peeks; if the list differs from the one shown it re-lists and runs nothing, otherwise it starts the batch and carries exactly the itemsStartBatchreturned. In combat it does nothing.postClickempties the text again. Peer Review F1, M1-M3 and N1 on thread 18d2d5bc. No saved macro is created or edited.GUI/Tabs/Macro.luaandGUI/MacroRuleDialog.lua(new): the tab (Remove / Promote / Demote chips, Scan, Rules, Run, Clear finished, the queue list with right-click Remove from queue) and the rule editor. Wired intoTAB_ORDERbetween bans and sync,GUI/Tabs.lua, the guild window's top tabs,MainWindow:RefreshLocale,/ugr macro, the locale file and all five TOCs.- Spec assertions changed only because a tab was added:
load_spec(TAB_ORDER),bans_tab_spec,window_spec(tab counts),guild_tab_spec(top-tab keys, Sync's slot),slash_spec(BUILT_IN),toc_spec(EXPECTED_FILES). - Not verified in a client: that one press runs several guild commands, that a PreClick attribute reaches the same click, the 255-byte bound, the tab icon, and that the combat-start hide lands before lockdown.
Tests/alts_tab_spec.lua: its spec ids are now AL-35 to AL-37, becausedocs/design/alts.mdused AL-17 to AL-19 twice (Peer Review D3, thread d090e16c). Names only, no assertion changed.
Fixed: one click of the macro run button could carry out a batch the dialog never showed (Peer Review X1, X2, thread 18d2d5bc)
- X1:
W:CreateSecureActionButtonregisters its button for the down and the up (LibAceGUIWidgets-1.0.lua:3794), so one physical click can runPreClickandPostClicktwice, and the secure handler acts on one half only (classic_eraSecureTemplates.lua:805-808). The down started the listed batch, the down'sPostClickemptied the text and re-listed the NEXT batch, and the up'sPreClickstarted that next batch, which the up's click then ran. For a remove batch that removes members the officer was never shown. Whether the down reachesPreClickin a client is not verified; the fix is right either way.GUI/MacroRun.lua: the down'spreClickstarts the batch, the down'spostClickdoes nothing, the up'spreClickkeeps the batch (MacroRun.downPressed), and only the up'spostClickempties the text and re-arms. An up with no down before it is still a whole click. A down whose up never comes leaves its batch for the next press'sSettle; the newMacroRun:Rest()empties the button onOpenand at combat start, and the queue's change message does not re-arm between a down and its up. - The Roster tab's kick dialog had the matching flaw: its
postClickhid the dialog on the down, so a mouse click (secure action on the up) put the dialog away before Remove ran (not verified in a client). It now hides on the up only (GUI/Tabs/Roster.luaKickDialog). - X2:
MacroQueue:SettlesendsUGR_MACRO_QUEUE_CHANGEDwhen it marks an item failed, so the Macro tab stops showing it as waiting. - Specs, red first (run 20261017-094853.692, four failures for the reasons above), then green (run
20261017-095056.933, 84 passed): two new MC-15 examples drive the down and the up
(
Tests/macro_tab_spec.lua), one Settle example inTests/macro_queue_spec.lua, and the kick example inTests/roster_menu_spec.luanow clicks the down (dialog stays, Remove armed) before the up.GUI/MacroRun.lua,Modules/MacroQueue.luaandGUI/Tabs/Roster.luaare at 100% line coverage (run 20261017-095427.649).docs/design/macro-tool.mdgains "One click, one batch". - Follow-ups from Peer Review on thread 8edab7cf: the combat-start comment in
GUI/MacroRun.luanow cites warcraft.wiki.gg API_InCombatLockdown (lockdown begins afterPLAYER_REGEN_DISABLEDfires, soRest()'sSetActionapplies at once; not measured in a client) (F1). The abandoned-down example inTests/macro_tab_spec.luaasserts behaviour instead of the internaldownPressedfield: after combat starts, a lone up presses (oneSettle) (F3; green run 20261017-100819.548). An up-only option forW:CreateSecureActionButtonwas requested from LibAceGUIWidgets (inbox d761a47e), so the two hand-written workarounds can go when it ships (F2). - LibAceGUIWidgets v0.6.0 (MINOR 44) shipped it: the macro run button and the kick dialog's Remove
button now pass
clicks = "up", onePreClick/PostClickpair per click. The down/up handling stays for a player on an older copy, which ignores the option. New assertions on the registration ({ "AnyUp" }) in both specs, red first (run 20261017-173125.025). - Peer Review, thread 18d2d5bc: on an older library, a down whose up never came left its batch on the
button, and a later up with no down (a press begun off the button and let go on it) skipped the press
and ran that text even after the queue had changed, so an item the officer had since taken out could
still be carried out. The queue's change message now forgets a pending down and empties the button
(
MacroRun:Rest()) before it re-arms, so that up is a whole click over the list then shown. New MC-15 example, red first (run 20261017-173640.989: the abandoned remove stayed on the button), green in run 20261017-173941.958. - Changed the same day (spec changed with its reason in the MC-15 comment): that up no longer presses
at all. The secure handler acts on the up for a mouse click but on the DOWN for a key-binding click
with
ActionButtonUseKeyDownon (classic_eraSecureTemplates.lua:805-808); there the down had already run its batch, and the up's press started the next batch and recorded it sent, ranks and all, with nothing to run it. A queue change now marks the click abandoned (MacroRun.abandoned), and its up empties the button and does nothing; the officer clicks again over the new list. Neither client retries the down's batch or records any rank twice. Red first (run 20261017-180227.782), green in run 20261017-182343.812.
Added: "Allow all ranks" in the Lowest officer rank list (Discord thread 56775f07)
- Vishiswaz: "sometimes guild masters add new ranks. This way they would not have to change this
setting."
sync.officerRankmay now be saved as"all"(Admin.ALL_RANKS), read byAdmin:OfficerRank()as the guild's lowest rank at that moment, so a rank added later is inside the gate;Admin:AllRanks()is new. A saved rank index keeps meaning that rank, so no migration. The guild info lock still wins (Settings:Setrefuses, the dropdown shows the lock's rank greyed). Every reader goes throughOfficerRank(), so the ban list, approved list, macro tab and sync follow. The setting is per client and does not sync. v0.3.0 reads"all"as rank 1 (fails closed). Modules/Admin.lua,Modules/Settings.lua(accepts"all"before the ranks load),GUI/SettingsPanel.lua(the entry, sorted last),Locale/enUS.lua("Allow all ranks"),docs/design/settings.md. Specs red first (run 20261017-095944.363, 11 failures), then green (run 20261017-100048.984, 89 passed): a new "Allow all ranks" block inTests/admin_gate_spec.lua, two ST-05 examples inTests/settings_spec.lua, and the ST-12 values example inTests/settings_panel_spec.luanow expects the entry last (that is the change asked for).- The guild info block carries it too (writ todo te506e75f786fdf73): with Allow all ranks in force
Sync:GuildInfoBlock()givesUGR{o=all}instead of today's lowest rank number, which would have put a rank added after the paste outside the lock.Sync.ParseGuildInforeadso=all(lower case, theoletter only) asAdmin.ALL_RANKS;Sync:GuildLock()turns it into the guild's lowest rank as it is read; the newSync:GuildLocksAll()says the lock isall, soAdmin:AllRanks()is true under it and the dropdown shows "Allow all ranks" greyed.Sync:CheckGuildInfo()countso=2becomingo=allas a change even when rank 2 is already the lowest. v0.3.0 readso=allas nooand falls back to its own setting (fails to the local setting, never open). The admin_gate_spec example that pinned the oldUGR{o=2}as a known cost now expectsUGR{o=all}(that is the change). Specs red first (run 20261017-102925.862, 4 failures), then green (run 20261017-103027.479, 87 passed); the CheckGuildInfo example was added after the code and was not seen red. - KNOWN COST: with it on, every rank sees and holds the ban list, may change the approved list, and may do every admin action.
Changed: a chat line that already opens with the sender's nickname no longer shows it twice (ALT-09b, Discord thread c59e3510)
- Vishiswaz: "Can we do anything to prevent seeing the same name twice in chat for someone using both
add-ons?" TOG Tools' name prefix writes the sender's nickname into the message
(
TOGTools/Modules/NamePrefix/NamePrefix.lua:157), so a UGR reader saw it in the message and beside the name.Nicknames.SenderNameFilterand the fallbackChatFilternow pass the message toChatTag(name, msg), and the tag leaves the nickname out (keeping the main) when the message, after spaces, opens with one bracket and that nickname in any case, the next character not a letter or digit, or with the nickname directly followed by a colon. A bare leading word keeps the tag (Peer Review F3, thread 87e0b05f: a nickname such as "Ok" vanished from "ok, pulling"; red run 20261017-101543.195, green run 20261017-101556.927). The message is never edited. A secret message is not read (Compat.CanRead, new, over the client'scanaccessvalue). Not on a whisper the player sent (CHAT_MSG_WHISPER_INFORM), where the message is the player's own and starting it with the recipient's nickname is addressing them (found in this session's self-audit; red run 20261017-101252.442, green run 20261017-101313.310). - Spec CT-07 in
Tests/chat_tags_spec.lua, red first (run 20261017-100921.341: the nickname was still drawn), then green with the nickname specs (run 20261017-101046.752, 35 passed).docs/design/alts.mdgains the rule.
Removed: the docked-window fallback on the guild window's Members tab (MD-33; Peer Review N1, thread 0514da61)
- No released LibGuildRoster could reach it: the click handler and the panes both arrive in MINOR 34
(GuildRoster 1.3.0), so
GuildTab:Enablereturned before the fallback on any library without panes. Removed:GuildTab:AddButton,Label, thebuttonstable, the docked case ofPanelPage,MainWindow'sdockhelper,dockedToandCloseDocked, thepageargument ofMainWindow:Open, and the locale key "Open Ultimate Guild Roster beside the guild window.". On a library with the click handler but no panes UGR registers its click, menu and tooltip and puts nothing on the page. Tests/guild_tab_spec.lua: the MD-33 block now pins that (no frame added to the page, the window opens free-standing). MD-34's check that no button went on the page counts the page's children instead, since thebuttonstable is gone. The MD-33 and MD-35 "library without panes" setups now unregister the pane path'sOnGuildTabPageShownhandler, because the newEnablereturns early and no longer overwrites it; in gameEnableruns once against one library. Red run 20261017-094752.969 (two failures, the old button), green run 20261017-095137.097 (185 passed).docs/design/member-details.md: MD-33 describes the removal, and the pane paragraph no longer says the pane hosts a TabGroup.- The
Rekeyspec, also numbered MD-33 (0.3.0's entry calls it that), is now MD-36 inmember-details.md,Tests/members_spec.luaand theMembers:Restorecomment. Name only.
Added: alt links sync again (todo t9d0544d16dc3fd6a, Discord thread 01e41d4e, docs/design/alts.md AL-20 to AL-34)
- Operator: "you're telling the customer we aren't syncing log or alt data. that's wrong, we need to
sync that data." v0.3.0 stopped syncing alt groups when they moved to LibGuildRoster's alt store;
groups still come from the library, and UGR now syncs its fed copy as
config/altparts (admin class,Sync.luaClassOf) and feeds received groups back into the library. Modules/Alts.lua:Alts.KeyFor; a view state (read-time checks, newest canon wins per character,viewOf,settle, which retires a key only where the library still credits it to UGR);commitsaves views, never raw parts; edit plans carryold;HeldAt,Apply(throughSync:SaveParts),FeedAll; the applierOnSyncedregistered asAPPLIERS["config/alt"].Migrations.lua: migration 7 "alt links sync" moves the saved copies into parts; a copy filed under a character other than its main is parked inparkedAltGroups, since it would fail the read-time check.Constants.lua:SCHEMA_VERSION = 7andALT_GROUP_MAX = 75, shared byAlts.MAXand the migration.SavedVariables.lua: thealtGroupsdefault is gone.- Found while turning green: the applier walked
ipairs({before, now}), which stops at a nilbefore, so a newly arrived group never re-checked the groups it overlapped. Fixed. - KNOWN COST: two officers who link different characters to the same owner at the same moment write
one part, and the older link is lost (same owner, same id). An import's knock-on shrink or tombstone
of another group pushes, through
SavePart/DeletePart. - Specs: new
Tests/alts_sync_spec.lua(AL-20 to AL-30, AL-34), AL-31 and AL-32 insync_two_peers_spec.lua(AL-32's v0.3.0 client is modelled as this build without the applier), AL-33 inmigrations_spec.lua. Changed, each with its reason in a comment:alts_spec,alts_auto_spec,grm_import_spec(IM-13) read the config part;migrations_spec's version-6 examples run the list to 6;sync_sets_spec's applier key list gainsconfig/alt;nicknames_specandnickname_display_specreceive groups as pushed parts, since the admin gate refusesApplyat rank 2. Red run 20261017-095950.848 (25 failed), whole suite green with 100% on every touched file, run 20261017-102516.849 (1550 passed, 0 failed, 3 pending). Not verified in game, against a real v0.3.0 client, or for list order over the real DeltaSync wire.
Docs: alt-link sync design, Peer Review D4 and D5 (thread d090e16c)
docs/design/alts.md: a view's owner when it wins no main is its first won character in the stored part'smembersorder, the same on every client because receivers store the part verbatim (D4; that the wire keeps a list's order is not verified, and AL-26 now asserts the key on a received group). The unlink KNOWN COST now covers any save: a re-save restamps its group as newest, so a newer group its author has not received loses the shared character (D5).
Docs: the 0.3.0 entry and page corrected after release
- Peer Review G1 (thread ec30d4c5): the CurseForge page's rename lines (Bans tab, and the two under
0.3.0) now say "with GuildRoster 1.3.0", since rename detection needs the GUID that library version
supplies. The 0.3.0 section below gets the same note. The same condition is added where the page
promised the Write alt notes button (it writes through
Members:SetGameNote, which refuses without a GUID,Modules/Members.lua:544), renames recorded while offline, and opening from the guild window. Discord got a correction post for the release announcement. - Peer Review F2 and F2-B (thread 0514da61):
Modules/Alts.lua'sreturn nilafter the owner-key loop. GuildRoster 1.3.0'sCanonNametrimmed before stripping a stray colon, so"E :"was filed under owner key"E "and the real library reached that line; thealts_specexample briefly drove the real library with that name (run 20261017-083511.780). GuildRoster then fixedCanonNamein its working tree as 1.3.1 / MINOR 35 (GuildRoster thread f1827f75), and the suite loads that sibling file with no pin, so the example went red (run 20261017-084342.988,alts_spec.lua:108). It is now a defensive pin again: it replacesGetAltGroupon the real library object for one name and depends on no colon handling (run 20261017-084451.780, 63 passed). The line stays, because a library that reports a group with no matching owner key must read as no group. - Docs:
docs/design/sync.mdno longer exempts zone, online status and last online from syncing (Peer Review H6, thread 715d94a8); decision 11 says all data UGR uses syncs, and the cost of syncing presence is recorded as a KNOWN COST for the design to bound. What is built: last online syncs as theseenday in a member'sinfopart; zone and online status are planned and not built, and every passage ofsync.mdnow says that rather than that they do not sync.docs/design/sync-redesign.md's "Read from the game, never synced" paragraph is marked superseded in place. Its "From LibGuildRoster" line now names only sister guilds' presence, anddocs/design/alts.mdmarks its two "not synced" lines as v0.3.0, superseded by decision 11 (Peer Review A6 and N1, thread 47377ab4). - Spec change, with its reason (Peer Review A1, thread 47377ab4):
alts_spec's "registers no sync applier: alt groups never sync" pinned the behaviour the operator rejected (decision 11: alt links sync). It is now apendingstating the wanted behaviour, until todo t9d0544d16dc3fd6a designs the record shape; the file header says the same. - Comments:
GUI/GuildTab.lua's header names the MD-33 removal todo;GUI/ExportWindow.lua'ssetfield points atExportWindow.SETS, which also holdsdeaths. - Whole suite and coverage on the v0.3.0 tree, before the
alts_specchange: 1468 passed, 0 failed, 3 pending; 7052/7052 lines over all 39 shipped files (coverage run 20261017-083350.400). After the change, a coverage run that took the whole suite: 1468 passed, 0 failed, 3 pending, withModules/Alts.lua448/448 (run 20261017-083909.366); the other files' figures from that run were not read. - Self-audit F1 (thread ec30d4c5): the 0.3.0 section still said the sync was "being built", that SY-41 to SY-62 were "next", and that the alts port had no whole-suite run. Those lines now say what shipped, and the "log syncs" design bullet says the log set has no writer in 0.3.0. The v0.3.0 GitHub release body was published from the old text and is not changed by this.
Event log clear modes, the import dialog's prompt, race sync trace
- Removed:
EventLog'srangeandallclear modes andEventLog:Clearable(Peer Review F1, thread ebe08e19). Nothing outside Tests called them; the Log tab clears only the rows it shows, soClearnow takes"filtered"with rows and raises on any other mode. The A4 range-error spec went with the mode. Specs that cleared everything withClear("all")(admin_gate, export_window, log_tab) now pass every entry as rows. New spec: the module exposes noClearable, and"range","all"and"most"raise; red first (run 20261017-085212.223).docs/design/event-log.mdupdated. - The A2 snapshot clear (only the entries counted when the question opened are removed) is now proven: a mutant that cleared every entry at OK turned both A2 specs red (run 20261017-085346.791), green after restoring (run 20261017-085422.537).
- New spec: in combat, one Escape on the range dialog's confirmation leaves the range dialog and the window up. It was green on its first run against the sibling LibAceGUIWidgets working tree, so it was never seen red.
- The GRM import dialog now opens through
ns.UI.Prompt(Peer Review F3/F4, thread ebe08e19), so a guild change closes it like every other UGR question. It is markedfree: closing the window or the Members-tab pane leaves it up, as U5 / MD-34 decided (thread d0f49e1b), andns.UI.ClosePrompt(all)closes a free prompt only whenallis true (GUI/UI.lua's own guild-change handler passes it; it sat in the Log tab's handler until Peer Review N1, thread ebe08e19, so a prompt the Log tab never opened no longer depends on that tab). Four specs ingrm_import_spec, red first (run 20261017-085600.692), including one UGR prompt replacing another (the old one's hide runs once, the new one is tracked). A first-time import offer closed by a guild change counts as "Not now" for that guild (docs/design/grm-import.md). - Docs:
docs/design/roster-tracking.mdsays a member record's race does not travel over sync (only date, name, rank name and note parts do), and that Refresh would correct a non-token at the next comparison if one ever arrived (Peer Review, thread b65fa228). The Refresh race fix, its RT-44 spec and the locale spec's exemptions fromns.Migrations.listwere already in the code. RT-44 is now proven: with the guard put back tor.race == nilit went red on the stored "Night Elf" (run 20261017-091101.085), and green again once restored (run 20261017-091129.178). - Whole suite under coverage: 1470 passed, 0 failed, 4 pending;
GUI/UI.lua,GUI/Tabs/Log.lua,GUI/Tabs/Bans.lua,GUI/Tabs/Sync.lua,GUI/MainWindow.lua,GUI/ImportDialog.lua,Modules/EventLog.luaandAPICompat.luaat 100% (run 20261017-090705.451).
[0.3.0] - 2026-10-16
Removed: the minimap button
The operator, 2026-10-16: "we need to remove the Minimap Button (for now), as our main UI will be
through the guild&communites UI". The way in is now the guild window's Members tab
(GUI/GuildTab.lua), the key binding and /ugr.
Core.luaregisters no LibDataBroker launcher and no LibDBIcon button;ns.dataBroker,ns.SetMinimapShown,Constants.LDB_NAMEandConstants.ICONare gone, and so are the settings page's "Show minimap button" switch and its four locale strings.profile.minimapis no longer a default. No migration deletes a saved one, so a player's hide choice is still there if the button comes back (savedvariables_specpins that).- LibDBIcon-1.0 is no longer a dependency of any TOC or of
.pkgmeta, and the Forever TOC no longer embedsLibs/LibDataBroker-1.1andLibs/LibDBIcon-1.0; theLibsfolder is deleted. The test env drops the LibDBIcon library row and the eviction of the two embedded libraries. - Specs changed first, with this reason:
toc_spec(no LibDBIcon dependency or embed on any client),window_spec(no launcher, no show/hide call, no saved switch),load_spec,savedvariables_specandsettings_panel_spec. They ran red (run 20261016-222222.700), then green (run 20261016-222432.720, 175 passed).Core.lua,Constants.lua,SavedVariables.luaandGUI/SettingsPanel.luaare at 100% line coverage (run 20261016-224204.718). - Two
alts_specexamples cover the last two lines ofModules/Alts.lua: a merge with the newer group's character named first keeps the older group, and a group the library reports under a name with no owner key for it reads as no group. Both pin today's behaviour, so neither ran red. - Peer Review (thread 0514da61): F1,
window_spec's two library lookups sat behindif ldb then/if icon then, which no longer hold, so they never ran; they now assert both libraries are absent. N2, comments and spec titles that still named the minimap (GUI/MainWindow.lua,Locale/enUS.lua,locale_strings_spec,guild_tab_spec) anddocs/FEATURES.mdSET-01 / UI-03,docs/design/settings.mdST-02 anddocs/design/member-details.mdare corrected. N1: the MD-33 button path inGUI/GuildTab.luais reached by no released LibGuildRoster (the click handler and panes both arrive in 1.3.0); its comment says so, and removing it with its specs is a todo.
Changed: the sync is rewritten on data sets (docs/design/sync.md)
The first build gave every field of every member its own DeltaSync item on numbered P2P, so the key
count grew as members times fields, and several workarounds (the equal-time tie-break stopgap, the
handshake wrappers, three MINOR 25 asks of DeltaSync) followed from that one choice
(docs/design/sync-redesign.md, "What was wrong"). The operator's design of 2026-10-16 replaces it:
- Four data sets (
members,log,config,bans), each one DeltaSync item with one master canon. - Part canons stamped once at save as
<DTS><hash>, the hash over the content alone; group, bucket and set canons derived from stored canons (Sync:Derive), as TOGBank's request log does. - Union with tombstones for every set; the greater canon wins one id.
- Buckets by a hash of the group id: 32 to start, doubling past an average of 100, never shrinking.
- DeltaSync's hash P2P, with a four-leg exchange that runs both ways in one negotiation and stops after 3 rounds.
- A line relayed by any guild or sister-guild member is taken; the admin gate applies at save.
- The log syncs, trued up from the game's guild event log, with duplicates removed by a content key
and tombstones rather than a time window. Not built in 0.3.0: the
logset has no writer, so the event log stays local to each player (below). - Alt groups move to LibGuildRoster.
Specs SY-41 to SY-63 were written red first (below; SY-54 is pending). The 3-round limit and the bucket numbers are Claude's proposals. Two choices the operator has not made are banked: any client may save an observed log line, and the log's Clear becomes a guild-wide delete (bank :4804). One DeltaSync host for all four sets instead of a separate ban host (bank :4805).
A Sizes section, from the real saved data (1,029 members, 84 log lines in two days) and the wire
encoding: legs 1 and 2 stay small, but DeltaSync's delivery watchdog bounds a whole negotiation at
180 seconds from the accept (P2PSession.lua:595), about 120 KB, and a GRM-imported roster is about
226 KB. So no leg is one send of more than Sync.LEG_BYTES (32 KB), batches merge as they arrive, and
a bucket the asker holds nothing in is answered with its groups (SY-63). Claude's proposals, bank
:4843. The game event log's cap is dropped from Not verified at the operator's word.
Tests/sync_sets_spec.lua (SY-41 to SY-48) is written, red: part canons, derived canons, the empty
set, merging and its convergence both ways, buckets, receiving from any peer, and who may save. Its
header pins what the design left open: a group with no parts and no tombstones is not stored (so two
clients holding the same data hold the same groups), Merge returns the changed group ids sorted, and
AUTHORS is keyed by the part's kind and called with the player, the group id and the content.
The core is built and every store is moved onto it (the last bullets below). The saved data moves to the new format on load, and none of it has been tried in game.
Sync.luais rewritten in place on the data sets:Set,Stamp,Derive,Canon,Merge,Bucket,SavePart,SaveParts,DeletePart,DeleteGroup,IsPeer,IsSendPeer,Push,MyHashes,Announce,OnOfferReceived,Held, andCheckBanAccessin place ofCheckBanHost. The savedsyncdefault is{ sets = {}, records = {} }.Mergefinds a set's applier underAPPLIERS["<set>/<group prefix>"]beforeAPPLIERS[<set>], becauseconfigholds bothadmin:andrules:groups.Modules/Admin.lua's approved list is on theconfigset asadmin:<name>groups, andTests/admin_gate_spec.luais rewritten to it. Before this,OnEnablestopped with an error for every player below the officer rank, because the approved list still called the removed record API (Peer Review P1, thread e2e12b6e).Admin:Approverefuses a bare name until the player's realm resolves ("Your realm has not loaded yet. Try again in a moment."), except on WoW Forever, where names are regional. Before the realm resolves LibGuildRoster leaves a bare name bare, so the entry was filed asadmin:<name>. Once the realm resolved,KeyForgaveadmin:<name>-<realm>, so the entry could not be listed or removed and it synced forever (Peer Review L2, thread e2e12b6e;admin_gate_spec, red first). The comment oncanonnow says that NormalizeName appends the connected-realm name (Peer Review L1).Approve("Unknown")says "Enter a name." rather than the realm message, since LibGuildRoster returns "Unknown" bare whatever the realm (Peer Review F4, thread 33dfc47f; red first).- Re-saving a part whose content is unchanged no longer stamps it again or pushes it to the guild (Peer Review N1). A whole-group delete over a group that already had a part tombstone now leaves two clients with the same stored data (Peer Review N3). Both fixes were made before their specs, which is the wrong order for this repo; the specs were added after and pass.
- The four legs, the 3-round limit, the ban gate on legs, live push and batching are built:
Sync:Ask,OnDataRequestanswering legs 1 and 3,OnDataReceivedtaking legs 2 and 4 and live pushes,onSyncAcceptedand a gatedhasContentinInitP2P.Tests/sync_protocol_spec.lua(SY-49 to SY-53, SY-63) passes 28 of 28 with the examples below (run 20261016-143415.069). - Leg 2 also carries
diff, the buckets that differ, beyond sync.md's first table: a bucket the provider holds nothing in otherwise never told the asker to give its group there. sync.md's table and a spec example now pin it. That example passed on its first run because the code came first. - Fixed (Peer Review C1, thread 95c2d3f6): a negotiation moved a differing group only from the side with the greater group canon. A group canon is its newest DTS plus a hash, so when two officers edited different parts of one member, the newer part on the lesser side never synced, and the pair re-ran three rounds on every broadcast. A differing group is now both wanted and given. The new SY-50 example (each side holding one newer part, both canon orders) ran red first. The leg-3 and leg-4 examples were changed to match: they had pinned the one-way rule.
- Fixed (Peer Review F6): leg 3's
wantlist and every leg'stombsrode whole on the last message, so a leg 3 wanting 1,000 groups was one message of about 45 KB. They are now split across the numbered messages like the groups, and the provider collectswantuntil the last one. The new SY-63 example ran red first. - Peer Review F7: a new SY-52 example pins that a provider who fails the admin gate answers no bans leg. The code already did this, so the example passed on its first run.
- Every store is moved onto the data sets.
Modules/Members.luasavesjoin:andpromo:parts (verified dates only) andcnoteparts of a member's group;left:parts come only from the GRM import; nicknames are anickpart in the samemembersset;Modules/Bans.luasaves to thebansset and the separate ban lane is gone (bans ride the one host, gated byCheckBanAccess); the macro tool's rules areconfiggroupsrules:<action>and the admin gate's approved list isconfiggroupsadmin:<name>; the GRM import saves its whole batch with oneSaveParts;Guild.lua'sadoptPendingno longer calls the removedReceive(Peer Review F1, thread e2e12b6e); the Sync tab readsHeld()and the in-step count; migration 5 (SY-62) moves each guild's saved records and bans into themembers,configandbanssets, drops alt records and removed or dateless join/promo records, parks what it cannot file (an oldlog:record among them) insync.parked, and deletessync.numbers*,banSync,sync.recordsand the oldbanstable. Thelogset has no shipped writer yet (the event log is not synced), so it is always empty. Alt groups no longer sync. The transitionalSync.KEYSandCheckBanHostshims are removed.Tests/sync_records_spec.luais deleted and the older sync specs are rewritten or retired, each with its reason in the spec header. SY-54 (two clients catching up with each other) is pending. - Fixed (NK-03): deleting a part or a group in the same second as its newest save could leave the part
alive, so a nickname set and cleared twice in one second came back. A delete is now stamped no
earlier than the newest part it removes (
deleteDts). The newsync_sets_specSY-41 example ran red first. - Fixed (Peer Review N1, thread bf8341d7): a provider's leg 2 carrying a tombstone that is not a time
raised a Lua error inside the comm handler. Such tombstones are now dropped where leg 2 is read and
before a whole group's canon is derived. Two
sync_protocol_specexamples ran red first. - Fixed (Peer Review P1 and F3, threads 95c2d3f6 and bf8341d7): a batched leg was answered when the
message numbered last arrived, so parts arriving out of order lost the parts after it, and a repeated
first part reset what had been collected. A leg is now answered once every part has arrived, in any
order, and a repeated part counts once (
allIn). The new SY-63 example ran red first. - Peer Review F10: a mutation run (run 20261016-151035.317) removed the provider's admin gate and leg
2's
diffin turn, and the SY-52 and SY-50diffexamples each went red, so both examples catch the bug they were written for. Two SY-53 examples (the whisper path of a push, an unknown message type ignored) were added for coverage; they passed on their first run. - Whole suite after the store move: 1424 passed, 9 failed, 3 pending (run 20261016-154831.920); the 9
are the guild-window pane specs MD-34 and MD-35, whose code is not written yet.
Sync.luaandModules/Admin.luaare at 100% line coverage. Nothing has been tried in game. - Fixed (Peer Review F1 and R1, thread bf319dc5): the provider collected a leg 3's parts per round, so a
half-finished leg 3 from the same asker could complete a later negotiation early with a partial want
list, and when the part count differed the old leg's wants were answered too.
Sync:Asknow gives each negotiation an id,nid(a number drawn once per session, a dot, and a per-session counter; no timer and no clock, and the random part keeps ids from before a/reloadfrom matching), every leg-3 part carries it, and the provider collects bynid. A leg 3 with a missing or malformednid(Sync.NID_MAX32 characters) is ignored. Three new SY-63 examples ran red first (run 20261016-155912.899): answering before the second negotiation's part 1, answering 4 wants instead of 1, and answering a leg 3 with nonid. sync.md's leg-3 row shows the field. A client withoutnidgets no leg-3 answer; the hash protocol has not shipped, so no released client sends one (not verified against a released build). - Fixed (thread 00173eb7 F4, the asker's twin of F1): the asker matched legs 2 and 4 by round only, so a
part from an earlier negotiation at the same round could count toward the next. Leg 1 now carries
nidtoo, the provider echoes it on every leg-2 and leg-4 part, andonLegdrops a part whosenidis not the open negotiation's. A leg 1, 2 or 4 with a missing or malformednidis ignored, so a client withoutnidgets no leg-2 answer either.sync_protocol_specpins it: the echo example, the stale leg-2 and leg-4 part examples, and the missing/malformed/foreignnidexample.nid's draw ismath.random, which Blizzard's own UI calls in all five clients' source trees (Blizzard_SharedXMLBase/TableUtil.lua:518;:479in the Forever tree). - Fixed (Peer Review G1): a guild change left open negotiations in place, so a leg 4 from the old guild's
provider merged into the new guild's set.
UGR_GUILD_CHANGEDnow clearsSync.askingandSync.answeringwithSync.advertised. Two SY-51 examples ran red first (run 20261016-160543.442). - Peer Review R2: no change. Only the settings page's "Approve a player" box calls
WhyNotApproveandApprove(GUI/SettingsPanel.lua:103-104), with what the officer types, so the game's localized "Unknown" name cannot reach it from a unit or a roster row. Sync.lua585/585,Modules/Admin.lua100/100 (coverage run 20261016-161249.669, whole suite 1444 passed, 0 failed, 3 pending).- Peer Review F6 (thread 00173eb7):
/ugr bansaskedns.Bans:CanSee()while the tab strip and the guild window's icon tabs askMainWindow.Offered, so the two could disagree if the rule changed. The slash command now asksMainWindow.Offered("bans")(Slash.lua:84-87). The newslash_specexample ran red first (run 20261016-162839.587) and green withbans_specandbans_tab_spec(run 20261016-164522.551). Peer Review S1:bans_spec's BN-11 is now titled for sync's wire, since the ban host is retired. - Fixed:
Members:Quietlyturned the members set's applier off, ran its function, then turned it on, so a function that raised left the applier off and every members part received for the rest of the session was dropped. The switch is now a count, released throughpcalland the error raised again witherror(a, 0)(Modules/Members.lua:181-200), so a nested call cannot turn it back on early either. Twomembers_specMD-19 examples ran red first (run 20261016-164338.861) and green (run 20261016-164818.965). The re-raised error keeps its message but not the original stack. The comment onMembers:Rekeynow says the members-set parts stay under the old name on a rename, a gap filed as a todo. - Peer Review (thread e2e12b6e): the
sync_sets_specexample on applier keys asserted onlyn >= 4, so a module that forgot to register would sync nothing and stay green. It now asserts the exact sorted list{ "bans", "config/admin", "config/rules", "members" }(Modules/Bans.lua,Modules/Admin.lua,Modules/MacroRules.lua,Modules/Members.lua). Red first with Admin's registration removed (run 20261016-164533.590), green restored (run 20261016-164709.459). Thelogset has no applier, as above.docs/design/sync.mdnow givesSync:SaveParts(batch)(it said(setKey, batch)) with the batch's{ set, group, part, content, dts }entries, names the"<set>/<prefix>"applier key, and says the asker drops a leg-2 or leg-4 part whosenidis not the open negotiation's. - Fixed (todo #12): a rename or realm transfer moved the member's record but left their members-set
group (join, promotion and leave dates, custom note, nickname) under the old name, so parts received
later for the old name found no record and later edits split across two groups.
Members:Rekeynow re-files the group locally (refile,Modules/Members.lua:798-811): each live part is copied to the new name's group with the newname, stamped bySync:Stampat the old part's own DTS with itsbykept, and the part and group tombstones carry over, straight intoSync:Merge. Every client sees the rename in the roster and derives the same copies, so nothing is saved through the admin gate and nothing is pushed. The old group is kept, because a delete would be stamped at each client's own clock. The decision is indocs/design/member-details.md(MD-33) anddocs/design/sync.md. Fourmembers_specMD-33 examples ran red first, including two clients ending with equal members-set canons.Members:Restore(a former member rejoining under a new name) re-files the same way; its MD-33 example ran red first (run 20261016-175512.703) and green withroster_tracking_spec(run 20261016-175550.324). A rejoin under a new name now tells the alt and ban lists too (the entry below). - Fixed (self-audit thread c04b4fd1 F2, ROSTER-09): a former member who rejoined under a new name kept
their alt group and their ban under the old name, so the rejoin hooks (
Alts:OnMemberRejoined,Bans:OnBannedPresent) looked under the new name and found neither.Members:Restorenow tellsAlts:OnMemberRenamedandBans:OnMemberRenamedwhen the name changed, beforeAPPLY.rejoin(Modules/Roster.lua:388-398) runs those hooks. Both rename paths go through one local function,tellRenamed(Modules/Members.lua:91-96), called fromRekeyandRestore; the fix had first been written intoAPPLY.rejoinas a second copy, and was merged there. ROSTER-09's tworoster_tracking_specexamples ran red first. Whole suite 1468 passed, 0 failed, 3 pending (run 20261016-224930.539, withModules/Alts.luaat 448/448);Modules/Members.lua456/456 andModules/Roster.lua447/447 (run 20261016-224204.718). - Fixed (Peer Review R1, thread 306a2e93): a banned player who left and came back under a new name was
not flagged as banned.
Roster:Difffound the former member by GUID but asked the ban list about the new name only. It now asks about the former name too (Modules/Roster.lua:222-224). RT-16 inroster_tracking_specwas the red spec (failing at:316in run 20261016-183446.265) and is green in run 20261016-184441.765. - Every rename path above needs the member's GUID: a rename is matched by GUID
(
Modules/Roster.lua:313) and a rejoin with no GUID by the same name and class (:203). LibGuildRoster reads the GUID only from 1.3.0 (MINOR 34, per Peer Review G1, thread ec30d4c5), so with GuildRoster 1.2.1 a member back under a new name arrives as a new member, unbanned and without their alts or dates. The CurseForge page says "with GuildRoster 1.3.0" on those lines. - Fixed (self-audit thread c04b4fd1 F1): after a renamed rejoin the old name's group is kept, and its
newest dated part is a leave. A client that never saw the rename (a fresh install's first sync, or a
late part saved under the old name) found no record for that group, so the applier made a duplicate
former member under the old name, and the late part never reached the renamed member. A re-file now
writes a rename marker into the new group: part
from:<old group id>, content{ from = <old group id>, name = <new name> }(markerOf,Modules/Members.lua:629-643, written at:808). It is stamped at the newest DTS the old group holds (parts, part tombstones, group tombstone) with thebyof the old group's newest live part, so every client that re-files the same old group derives the same marker (Peer Review 9f6acf34). A group no record is filed under goes to the held record whose group holds its live marker (heirOf,:658-670; current members first, then former, each in name order) and is re-filed into it. If the old group arrives first, the former member it makes is markedadopted, and it is taken back once the new group and its marker show up (takeBack,:745-758). A chain of renames copies earlier markers like any part. No tombstone is ever made for this, so a client that receives a tombstone before it sees the rename cannot drop the copied parts (Peer Review's hazard, reply 7ff93c2b). The saved shape does not change:adoptedis a new field where absent means false. An earlier version of this fix matched the old group by finding exact re-filed copies of its parts. Peer Review G1 (thread c04b4fd1) showed that "copy join date to alts" makes byte-identical join parts, so a departed alt's group would have been merged into another alt and synced out. The marker replaced that heuristic and its per-apply matching cost. Known costs: a rename made before this build carries no marker, so its old group can still make a duplicate; duplicates made by earlier builds carry noadoptedmark and are never taken back; and any guild member's client can send a false marker, as with any other part, because receiving is not gated. Specs inmembers_spec(marker shape, DTS andby, two re-filing players deriving one marker, the G1 case, a rename chain, no marker meaning no re-file) ran red first (run 20261016-182722.284, 6 failed; G1 failed atmembers_spec.lua:1145with no former X) and green (run 20261016-182922.266).Modules/Members.lua453/453 (run 20261016-183446.265).
Changed: alt groups live in LibGuildRoster's alt store (todo #11, docs/design/alts.md)
Modules/Alts.luais rewritten to the designalts.mdandalts_specalready described. Groups are read from LibGuildRoster, keyed by owner. A bare name another addon fed reads as that member. An owner that did not list itself still counts as one of the group's characters. A main counts only while UGR fed it and the group still lists it. One character with no main is not a group. New:Alts:SameAccount(the library'sIsSameAccount) andAlts:HeldAt.- One save path writes an edit's groups to UGR's saved copy and to the library, takes the saved
characters out of every other copy, and fires one
UGR_ALTS_CHANGEDwith the sorted owner keys it touched. A shrunk copy changes the library only where the library still credits that owner to UGR. When two groups merge, the one with a main wins, then the larger, then the older, then the lesser owner key. - On a LibGuildRoster without any of the six alt functions, everything reads and writes the saved copy alone and nothing raises.
- Removed:
Alts:Store, the old merged view, the"<player>:<now>"and"rejoin:A+B"ids and thealtGroupcache. - Migration 6 (
Migrations.AltGroupsByOwner,SCHEMA_VERSION6) re-keys the saved alt groups by owner. - The GRM import reads a group's time through
Alts:HeldAtand saves throughAlts:Apply, with no record ids and no stamping of its own.Nicknames.MayAuthorasksAlts:SameAccount, as alts.md says. alts.md's nickname storage text and NK-01 to NK-11 now match the members-setnickpart. - Spec changed with a reason:
Tests/alts_spec.lua:503registered a secondUGR_ALTS_CHANGEDhandler writing the same variable, so every message counted twice and no implementation could pass. The second registration is removed, with that reason in the spec. - Red first (run 20261016-163757.982):
alts_spec's 61 examples failed on the missingns.Alts.SameAccount, AL-19's 7 on the missing version 6, andgrm_import_spec's 2 alt-group examples. Green: run 20261016-182152.779 (203 passed, the alts, migrations, nicknames, import, Alts tab, nickname display, chat tags and saved-variables specs), and run 20261016-182406.403 (259 passed, every other spec that readsns.Alts). The whole suite and coverage ran later (run 20261016-224930.539: 1468 passed, 0 failed, 3 pending;Modules/Alts.lua448/448). Nothing has been tried in game, including the order addons feed the library at login, a known cost in alts.md.
Changed: the Log tab's filter chips fit in two rows (EL-26, EL-19)
The operator, 2026-10-15: "the toggle tabs take up too much space, we need to condense it and make the
buttons smaller with less words, so we can get it down to 1, 2 rows at most". Each chip now shows a
one-word label from the locale (chip_<category>: Joins, Leaves, Rejoins, Promos, Ranks, Levels,
Public, Officer, Custom, Names, Returns, Bans, Banned, Alts, Guild, Deaths, GRM) with the full name in its
tooltip, is padded Log.CHIP_PAD (4) a side instead of the library's 10, and the search box is
Log.SEARCH_W (140) wide instead of 200. Offline, the search box and all 17 chips take 2 rows at 560
pixels, down from 6; the list hangs from the bottom of the filter row, so it starts that much higher.
EL-19 now expects the short labels (reason in its comment). Peer Review F5 (thread 00173eb7): a new
category with no short label would silently fall back to its long name and break the two-row fit, and
EL-19/EL-26 do not catch that (a mutation run with death removed left both green, run
20261016-164700.798). The list is now public as Log.ShortLabels(), and a new EL-26 example checks it has
an entry for every SavedVariables.CATEGORIES category and none other, each matching its chip_<category>
locale string. It ran red first (run 20261016-164024.604) and green with locale_strings_spec (run
20261016-164746.524). That run also restored the imported (GRM) label, which a red-first mutation from
another session had left out. GUI/Tabs/Log.lua is at 100% line coverage
(run 20261016-161034.390, which also ran the whole suite: 1444 passed, 0 failed, 3 pending). Not verified:
the harness's fixed 7-pixel character width is not the game font, and 560 is a guess at the pane's width,
so a narrower pane in game could still wrap to a third row.
Tests: harness pin moved to dd620e5 (the game's guild event log, whispers, sender-name filters)
Tests/wowapinow pins WoWAPITesting dd620e5, which includes be92d12's model of the guild event log.Tests/roster_tracking_spec.luadrops its hand-madeinstallLogand drives RT-38 to RT-42 throughguild.setEventLog,guild.eventLogQueriesandguild.deliverEventLog; RT-40 removesQueryGuildEventLogfor its one example, since the harness now always has it.- The stand-ins named in the nickname and right-click menu entries below are gone: the menu spec reads
env.wow.tells,env.wow.friendsandenv.wow.ignores, andTests/env_ugr.lua's sender-name filter stand-in is replaced by the harness's own (env.wow.senderNameFilters,env.wow.hearChat).env.unloadremoves UGR's filter, because the harness keeps those registrations across a reset. - MD-29 had gone red on the new pin because its whisper stand-in recorded
ChatFrame_SendTelland LibGuildRoster now callsChatFrameUtil.SendTell; it readsenv.wow.tellsnow and is green. - Whole suite 1504 passed, 9 failed, 2 pending (run 20261016-131323.268); the nine are the MD-34 and MD-35 pane specs, written red and not built yet. Coverage was not re-run: no addon code changed.
Added: an Alts icon tab on the guild window's top edge (MD-35, a preview)
The operator, 2026-10-15: "lets put the alts tab in there now, and we can see what it looks like and whether we need to do anything with it". The direction (bank :4417): UGR's tools become icon tabs along the guild window's top edge, right of the portrait, styled like the game's side tabs, while the Members list becomes UGR's main view. Alts is the first, to see how it looks.
GUI/GuildTab.luaGuildTab:AddTopTabs(page, kind)puts one CheckButton perGuildTab.TOP_TABSentry on the LibGuildRoster page (so it hides with the page), anchoredBOTTOMLEFTat the window's (page:GetParent())TOPLEFT,TOP_TAB_X= 62 to clear the 60-wide portrait at -5 (classic_eraCommunitiesFrame.xml:313-316). Registered fromGuildTab:Enable's pane branch onOnGuildTabPageShownand for a page already on screen; a library without panes gets none.- The art is the game's
CommunitiesFrameTabTemplatewhenC_XMLUtil.GetTemplateInforeports it (all five Blizzard trees have the template; Forever's inheritsRightSideTabTemplate), as LibGuildRoster builds its own side tab, else drawn by hand with classic_eraCommunitiesTabs.xml's textures. The template's scripts are replaced: its click calls the window'sSetDisplayMode. GuildTab:OnTopTabClickselects UGR's pane and the tab, or, when the pane already shows that tab, the member list.GuildTab:PaintTopTabschecks the tab exactly while the pane shows it, called fromMainWindow:ShowTabandMainWindow:Unembed.GameTooltipadded to.luarc.json's globals (the tab's hover).- Peer Review (thread cf5f8601): F1, the first click built the last-used tab and only then Alts; now
OnTopTabClicksetsMainWindow.pendingTabwhile it selects the pane andMainWindow:Embedopens on it. F2, a spec that everyTOP_TABSkey is inns.Constants.TAB_ORDER, run on a renamed copy too so it is known to bite. Before a second tab: the art's border overhangs 29 px right, soTOP_TAB_GAP4 would put the next tab under it (Peer Review's figures). Tests/guild_tab_spec.luaMD-35 (13 specs) red first (run 20261015-203138.530,AddTopTabsmissing; F1's spec red in run 20261015-211133.511, building Log then Alts), green whole suite 1510 passed, 0 failed, 2 pending (run 20261015-212513.525);GUI/GuildTab.lua157/157,GUI/MainWindow.lua202/202 (coverage run 20261015-213017.202). Not seen in game: the placement, the side-tab art drawn on a top edge (the art's flap is drawn for a right-hand edge), and the iconINV_Misc_GroupNeedMoreare all unverified on every client.- Peer Review edge note (thread cf5f8601, reply 7b56556e):
OnTopTabClicknow callsSelectGuildTabPaneinsidepcall, soMainWindow.pendingTabis cleared even when selecting the pane raises, and then re-raises the error. Its spec ran red first (run 20261015-214026.487). The stub now takes(_, owner, kind)and the example checks the call, because the zero-argument stub made the language server flag every call in the file.GUI/GuildTab.luais at 100% with the error branch covered (coverage run 20261016-160945.323).
Changed: the guild-window pane has no tab strip and no Roster tab; Log, Alts, Bans, Sync are top-edge tabs (MD-34, MD-35)
The operator, 2026-10-15, on seeing the pane: "this doesn't look good. i spent a LOT of time with LGR getting the borders and stuff right, you need to use that", "you can get rid of the roster tab, it's the main tab", "i don't want the tabs IN the window, I want them across the top like where the funky button is", and "rotate the tab on the top 90 degrees counter clockwise".
GUI/MainWindow.luaEmbedno longer builds an AceGUI TabGroup:MW.tabsis a plain host whosecontentis LibGuildRoster's pane parent, so a tab's content sits inside the page's own borders.paneTab(key)turns Roster (or anything that is not a tab) into Log, andOpen("roster")is ignored while the pane shows.ShowTabattaches raw frames only for the free-standing window, which keeps its TabGroup and Roster tab.Unembedhides the content frames; with no TabGroup in the pane there is nothing to release. TheOnSizeChangedfit hook is gone, since content anchored to the whole pane follows its size.MainWindow.Offeredis the one rule for which tabs a player is offered.GUI/GuildTab.luaTOP_TABSis Log, Alts, Bans, Sync,TOP_TAB_GAP20 (the game's side tabs are 20 apart,CommunitiesFrame.xml:390),TAB_SIZEexposed. The tab art is always drawn by hand (the template path is gone), with the border turned a quarter counter-clockwise (SetTexCoord(1,0,0,0,1,1,0,1), TOPLEFT -11, 29, size 64).PaintTopTabslays the tabs out, hides Bans for a player who may not see it and closes up the tabs after it;UGR_BAN_ACCESS_CHANGEDrepaints them, so a rank change shows at once.- The 9 MD-34 and MD-35 examples written red on 2026-10-15 pass, and a new example covers the ban-access
repaint.
guild_tab_specandwindow_spec88 passed, 0 failed (run 20261016-161306.043);GUI/GuildTab.lua169/169 andGUI/MainWindow.lua195/195 (coverage run 20261016-160945.323, which also ran the whole suite: 1444 passed, 0 failed, 3 pending).docs/design/member-details.mdMD-34 and MD-35 match. Not seen in game: the turned art, its offset, the tabs' spacing on the top edge, the content filling the pane, and the new icons (INV_Misc_Book_09,Ability_Warrior_Disarm,Spell_Nature_Reincarnation) on every client.
Added: nicknames, shown with the main's name (ALT-09, ALT-10; Discord thread c59e3510)
The operator, 2026-10-15: "yes, we need to do nicknames now, it's a good thing to do, and that's in
addition to showing the main like GRM does now. we need to show both". New Modules/Nicknames.lua
(ns.Nicknames, in all five TOCs); design in docs/design/alts.md, "Nicknames and the main's name".
- One nickname per character, saved as a synced
nick:<lower-cased name>record ({ kind = "nick", name, text, share }) throughSync:SaveRecord; clearing saves a removal. Not on the alt-group record asalts.mdfirst sketched: group records are admin-only, and a player sets their own nickname. At most 20 characters (strlenutf8), trimmed;|(the chat escape) and control characters are refused; empty text clears. Shared with the alt group by default: a character with no nickname of its own shows the group's newest shared one. - Set by the character, any character in its alt group, or an officer or approved player
(
ns.Admin). Received only from those senders: newSync.AUTHORS/Sync.MayAuthor, andSync:Accepts(sender, kind, record)now takes the record. The sender is judged, neverby.Sync.KEYS.nickrefuses a malformed record before it is stored. - Drawn WITH the character's name and the main's, never instead of either ("Bobs, alt of Bob"): the member panel's name row plus a Nickname row whose editor has a share box; a Nickname line in the guild window row tooltip; a "Known as" column on the Roster tab; the Alts tab's headings and rows; both tabs' search reads nicknames.
- Chat: the sender's name of guild, officer, whisper (both ways), party, raid, raid warning and
instance lines reads
[Bobby (Bobs, alt of Bob)]: hi, throughns.Compat.AddSenderNameFilter(ChatFrameUtil.AddSenderNameFilter, in all five Blizzard trees); the message text is untouched. On a client without it, the fallbackns.Compat.AddChatFilterputs "(Bobs, alt of Bob) " before the text instead. System online/offline lines are not tagged, because a whisper window matches those lines unchanged after the filters (ChatFrameOverrides.lua :247-270, Classic Era tree). - Settings
names.nicknamesandnames.chatMain(per guild, both on);/ugr nick <text>sets your own,/ugr nickclears it. No migration: the records live in the existing dataset. - Specs: new
Tests/nicknames_spec.lua(NK-01 to NK-10),Tests/chat_tags_spec.lua(CT-01 to CT-06, CT-06 on all five clients),Tests/nickname_display_spec.lua(ND-01 to ND-08). Changed, with the reason in each: roster_tab_spec (the new column; floor checks on larger screens), window_spec (1365x768 screen), settings_spec, slash_spec, toc_spec. - Red run 20261015-194037.818 (31 failed, every one "ns.Nicknames is missing"); green whole suite 1489 passed, 0 failed, 2 pending (run 20261015-200638.556); 100% line coverage on all 13 touched code files (run 20261015-201928.043).
- Known costs: catch-up takes a nickname record only from its owner, the owner's alt group or an admin, so a new client can wait until one of them is online (not verified with two clients). GRM nicknames are not imported. Not seen in game: the chat filter, the column, the editor. (The "Known as" column's raise of the width floor is undone below.)
Fixed: Peer Review on nicknames and the admin gate (thread d395c58c, reply fb7ac64b)
- F1:
Nicknames.SenderNameFiltertags the name the client built, kept whole with its colour, for a current member (arg2) on aCHAT_EVENTSline; nil otherwise. The client runs it last inGetDecoratedSenderName(ChatFrameUtil.lua:977-1019) and draws it only as the link text, so arg1 is unchanged for other addons and the log. On WHISPER_INFORM the tag is the recipient's.Enableregisters it once throughns.Compat.AddSenderNameFilterand falls back toChatFilterper event only without it. The harness has no sender-name filters, soTests/env_ugr.luastages a stand-in (env.senderNameFilters,env.decoratedSender), requested from WoWAPITesting. Not seen in game. - F2:
Sync.luarefuseReplysends replies refused for an unknown kind, a foreign key or the wrong lane throughRefuseReply("kind"/"key"/"lane"), as the rank refusal does, instead of leaving the asker to wait out the 180 s timeout. KNOWN COST: a v0.2.0 client has no KEYS for the newadmin:/nick:kinds and refuses such a reply the old way, so it may wait up to 180 s per key and ask again later. Verified in v0.2.0Sync.lua:498-499andDeltaSyncP2PNumbered.lua:152,:539,:1221-1226; repeat rate and in-game behaviour not verified. Seedocs/design/sync.md. - F3:
Nicknames:SetreturnsSync:SaveRecord's refusal message;/ugr nickprints it. - Q4:
RosterTab.FixedWidthleaves Known as out, soMainWindow.MIN_WIDTHis back to 965 px at scale 1 (1099 put a 16:9 screen under the floor above UI scale ~1.24). The column shows only whileRosterTab.Fits(floor list part plus 134, times the scale, plus the 16 px gutter), re-checked on refresh andOnSizeChanged, throughRowList:SetColumns. NewMainWindow.DEFAULT_WIDTH(1099) is the never-saved size. In-gameOnSizeChangedon the content frame is not verified. - Specs: red run 20261015-204241.006 (20 failed), green 1508/0/2 (run 20261015-211023.275), 100%
coverage on the five files (run 20261015-210336.254);
chat_tags_specrewritten;env_ugr.luasender-name stand-in;sync_spec"refused for its shape"; NK-11;roster_tab_spec,window_spec, ND-06 screen and width changes with reasons.
Changed: one admin gate for every admin action, officers plus an approved list
The operator, 2026-10-15, on a Discord request about clearing the log (thread 5185513d): "it should be
officers plus a list of people allowed to do stuff. that should be for everything, not one thing. if
you're an officer, or on the approved list, you can do all the 'admin' level stuff." New
Modules/Admin.lua (ns.Admin, in all five TOCs); specs in Tests/admin_gate_spec.lua.
- One gate,
ns.Admin, now decides every admin action: editing join and promotion dates and custom notes, linking alts, the ban list (edit, see, the read-only API, who receives bans, running the ban host), clearing the log, editing macro rules, the GRM import, and saving any shared record (Sync:SaveRecord/SaveRecords). A received record's sender is judged by the same gate (Sync:Accepts); the record'sbyis still not trusted. - Removed
Sync:Threshold,CanAuthor,WhyNotAuthor,RanksLoadingand the settingssync.generalRank,sync.customNoteRank,sync.banRank. Despite their "Lowest rank to share ... with" labels, no code picked recipients by them; they decided who may write and whose records are accepted, so all three were admin gates. The sharing gates (IsValidPeerwithlimitSendToMyRank,IsMember,Push) are unchanged. - An officer is anyone at or above
sync.officerRank(default rank 1, or the lowest rank in a smaller guild). It has no AceDB default, because AceDB writes a default back over an empty value at every login (AceDB-3.0.lua:126-128). A saved rank the guild no longer has reads as the default, never as everyone. - The approved list is synced: one record per character,
admin:<lower-cased name>holding{ kind = "admin", name, removed }, so every member agrees on it. Only officers by rank may change it, when saving and when receiving, so a listed alt cannot add others. Removing a name saves a newer record marked removed. - Guild info lock:
UGR{o=<n>}sets the officer rank. An olderg/c/bblock still works and reads as its strictest value. - Settings page: a "Lowest officer rank" dropdown (no "Allow all ranks" entry: picking the lowest rank does that), an "Approve a player" box and a "Remove <name>" button per approved player, greyed for a non-officer with "Only officers can change this."
- Refusals read "Only officers and approved players can change this." (was "Your rank cannot change this."); the ban, import and guild info wordings were changed to match.
- Macro actions (remove, promote, demote) stay on the game's own permissions; only editing the rules is gated.
- Migration 4 (
SCHEMA_VERSION4) carries the strictest of the three old ranks intoofficerRankand deletes them; a missingbanRankcounts as its old default, 1, so nobody gains access by upgrading. - Fixed on the way: choosing "Allow all ranks" for the ban rank was undone at the next login, by the AceDB default above.
- Known costs: members below the carried officer rank who could edit dates, alts or custom notes, or clear the log, by default can no longer do so until an officer lowers the rank or approves them. A receiver that does not yet hold an approval refuses the newly approved character's record until the next sync offers it again (superseded by the data-set rewrite above: a receiver now stores what any guild peer relays, and the admin gate applies only at save). The GRM import no longer reads the game's officer flag, so an approved alt can import.
- Specs changed with a reason in each comment: sync_records, sync_guild_info (rewritten), settings,
settings_panel, guild, alts, alts_auto, alts_tab (its
after_eachalso lacked a rank reset that the old default hid), members, member_panel, event_log, log_tab, bans, bans_tab, ban_api, sync_bans, macro_rules, macro_rules_sync, grm_import (IM-12, IM-12b, IM-18 rewritten), roster_menu, guild_tab, sync_two_peers, toc. - Red run 20261015-183958.310 (200 failed); green whole suite 1458 passed, 0 failed, 2 pending (run 20261015-190555.721); 100% line coverage on all 18 touched production files (coverage run 20261015-191302.953). Not seen in game.
Added: a right-click menu on the Roster tab's member rows (Discord thread 47403b72)
A player asked on Discord for a menu on UGR's own roster rows. GUI/Tabs/Roster.lua menuFor builds it,
opened with W:OpenMenu on a right-click of a member row (not a heading), the Bans tab's idiom.
Specs in Tests/roster_menu_spec.lua.
- Entries: Whisper, Invite to group (online only), Kick from guild, Link alt, Add friend, Ignore, Copy name, Member details. The player's own row has no Whisper, Invite, Add friend or Ignore; a former member's row has no Invite, Kick or Link alt.
- An entry the player can never use is left out; one blocked only for now is greyed with the reason
in its tooltip. Kick appears only when
ns.MacroQueue:WhyNot("remove", name)allows it (the game's own rank rules) and is greyed in combat; Link alt is greyed with the admin gate's reason (it wasSync:WhyNotAuthor("general")'s until the admin gate above replaced it). - Kick never calls
C_GuildInfo.Uninvite, which is protected. It opens a confirmation (RosterTab:KickDialog, built once, frame namesns.Constants.KICK_DIALOG_NAME/KICK_BUTTON_NAME) whose Remove button is LibAceGUIWidgets' secure action button running the client's own guild-remove command (ns.Compat.GuildRemoveMacro, the localizedSLASH_GUILD_UNINVITE1,/gremovewhen the global is missing). The dialog hides onPLAYER_REGEN_DISABLED, because a frame holding a secure button cannot be hidden in combat. - Link alt asks for the alt's name and links it through
ns.Alts:Link; a typed short name is matched to a current member in any case. A refusal is shown over the tab. - New
ns.Compatwrappers, whose comments cite each call's source in all five trees ofF:\Blizzard API Docs:Whisper(ChatFrameUtil.SendTell),InviteToGroup(C_PartyInfo.InviteUnit),AddFriend/AddIgnore(C_FriendList),GuildRemoveMacro. - The harness lacks three game functions the menu calls, so the spec installs stand-ins for them until WoWAPITesting adds them (requested on its inbox).
- Not seen in game, for the release checklist: the secure Remove button, whether the
PLAYER_REGEN_DISABLEDhandler runs before the lockdown, and/gremovewith a Name-Realm name.
Added: the player's own characters are linked as alts (ALT-08, Discord thread 0a1fd728)
A player asked on Discord for their own characters to be tagged as main and alts without doing it by
hand. WoW has no API that lists an account's characters, so Alts:SeeOwnCharacter (run on
UGR_ROSTER_CHANGES) learns them one login at a time.
- Each character seen as a current member is recorded in the guild's
myCharacterstable (account-wide, asUGR_DB.globalis), with the realm time it was first seen. - With
alts.autoLinkOwnon (the default), the character is linked to the account's best partner throughLink, the manual path, so the permission check, the sync record and the log entry are the same as a hand link. The partner is one whose group has a main, then one in a group, then the first seen, then by name. A group with no main gets the account's first-seen character as its main; a main the group already has is kept. - Linked characters are marked done, so one the player unlinked by hand is never linked again. A character that left the guild is never a partner. One try per login per guild, held back while the ranks load and retried once; a refusal for rank saves nothing, and the next login tries again.
- Settings page: "Link my characters in this guild as alts" in a new Alts section.
Tests/alts_auto_spec.lua(16 specs) red first (run 20261015-133431.622), green in run 20261015-140537.395.- The default is on. The operator has not yet said whether it should be; it is one line in
SavedVariables.luato change. Not seen in game.
Added: alt notes written from the Alts tab (Discord thread b4b13b22)
A player asked on Discord for alts' guild notes to be filled in from the alt links. They clarified that the name in "Name Alt" is the main's, so an alt of Bob gets "Bob Alt".
Alts:NotePlanlists the notes a write would change: every alt of every group with a main who is a current member, getting the template with each<main>replaced by the main's short name. A note the player may not edit, one that already reads the text (a write still waiting on the game included) and one past the game's 31-character limit are skipped and counted. The note's text is replaced, not added to.- A "Write alt notes" button on the Alts tab asks once, with the count and what it skips, then writes
exactly that plan through
Members:SetGameNote, unless the guild changed in between. The button is greyed withNotePlan's reason when it cannot write. - Settings (Alts section): "Write alt notes from the Alts tab" (
alts.notes, off by default), the note to write (public or officer, public by default), and the template (alts.noteTemplate, defaultL["<main> Alt"]; refused when its text without the markers is already over the limit). The note settings stay usable while the switch is off, so the template can be set up first. Tests/alt_notes_spec.luagreen in run 20261015-140537.395; its red run was not recorded.- Not verified: whether the server throttles many note writes from one click. Not seen in game.
Added: an Export button in the UGR window (Discord thread 4deddeec)
A player asked on Discord for the export to open from the window, not only from /ugr export. An Export
icon now sits left of the Settings gear in the window's bottom row (GUI/MainWindow.lua, the dressed
row's export entry) and calls ns.ExportWindow:Open(), with a tooltip from the locale.
Tests/export_window_spec.lua EX-21, green in run 20261015-135747.636. Not verified: that the
INV_Misc_Note_01 icon exists on all five clients. Not seen in game.
Tests: the whole suite after these four features
Whole suite 1430 passed, 0 failed, 2 pending (run 20261015-152334.565). GUI/Tabs/Roster.lua,
Constants.lua, Modules/Alts.lua, GUI/Tabs/Alts.lua, GUI/MainWindow.lua and APICompat.lua at
100% line coverage (coverage run 20261015-152903.989).
Changed: the Roster tab hides offline members until the Offline chip is on
A player asked on Discord (2026-10-14) for the roster to open without offline members. The
char.roster.offline default in SavedVariables.lua is now false. AceDB strips a value equal to
its default when it saves (removeDefaults, AceDB-3.0.lua:173), and true was the default until
now, so no character has it saved: every character, including one that had the chip on, opens with
offline members hidden once and has to turn the chip back on. A character that had turned it off
keeps false. Tests/roster_tab_spec.lua UI-11 asserts the default and red first (run
20261015-130112.878); the other Roster tab specs are about the whole list, so their load turns the
chip on as a player would (reason in the spec), and Tests/settings_spec.lua ST-02's expected
defaults follow. Green in run 20261015-130228.124. docs/design/ui.md updated.
Changed: the rank lists' no-limit choice reads "Allow all ranks"
A player asked on Discord (2026-10-14) for the settings page's rank lists ("Lowest rank to share
dates and alts with", custom notes, bans) to call their no-limit choice "Allow all ranks" instead
of "Lowest rank", which read as a rank of its own. Only the label changed: the locale key is now
L["Allow all ranks"] (Locale/enUS.lua, GUI/SettingsPanel.lua rankList), the stored value is
still nil. The ST-12 specs in Tests/settings_panel_spec.lua assert the new word; red first (run
20261015-125908.243), green in run 20261015-125953.559.
Superseded the same day by the admin gate above: those three rank lists are gone, replaced by one "Lowest officer rank" list with no "Allow all ranks" entry (choosing the lowest rank allows all).
Fixed: a member's race is saved in one spelling (Peer Review F3, thread 767f1f41)
The roster stores LibGuildRoster's race token (NightElf, Scourge), but the GRM import stored
whatever GRM had saved, and GRM saves more than one spelling: mostly the token, sometimes a race id
number, the localized race name, an empty string, or a Human placeholder when its lookup failed
(research written up in docs/research/grm-core-roster-sync-alts.md, "Race spellings"). The
export printed the stored value raw, so one column mixed NightElf, Night Elf and 4.
- New
ns.Compat.RaceToken(value)turns a token, a race id or the client's race name into the token (nil for anything else,""included), andns.Compat.RaceName(token)gives the client's name for it. Both readC_CreatureInfo.GetRaceInfo, which every flavour documents, over ids 1 to 200, once per session; 200 being above every race id is not verified against the newest client. - The GRM import stores
RaceToken(r.race)for a member UGR does not hold. An unrecognised value is not stored, so the roster's own race read fills it later for a current member. - The export's Race column writes the race in the client's language, or the stored value when no race matches it.
- Migration 3 (
SCHEMA_VERSION3) converts races 0.2.0 saved, in current and former members of every guild;""is dropped and an unrecognised value is left as it was. It runs before AceDB, at load; thatC_CreatureInfoanswers that early in game is not verified. - Known cost: GRM's
Humanplaceholder cannot be told from a real Human and imports as Human. - Specs red first (run 20261015-123647.759: the import, export and both migration specs), green in
run 20261015-123803.141.
Tests/locale_strings_spec.luaallows the new migration name, as it does the other two, because it is a developer-facing step name. Whole suite 1324 passed, 0 failed, 4 pending (run 20261015-124436.031);APICompat.lua,Migrations.lua,Modules/GRMImport.lua,Modules/Export.luaandConstants.luaat 100% (coverage run 20261015-124146.217). Not seen in game.
Changed: which clients may write guild notes comes from LibGuildRoster (GuildRoster contract f4cd150d)
- LibGuildRoster 1.3.0 (unreleased) adds note editing to its Members tab and publishes its client list
as
CAN_WRITE_NOTES, now the same as UGR's: Classic Era, TBC and Mists, never Retail, Forever or an unknown client.Compat.CanWriteNotes(APICompat.lua) reads it when the library has it, so the two cannot drift, and keeps its own list only for an older library.apicompat_specpins it under all five clients: the library's answer is used, the fallback list is right, and the loaded library agrees with UGR's list. Red first (run 20261016-193727.453, the library's answer not used on all five), green withmembers_spec(run 20261016-193759.564, 134 passed). Whether a note write lands in game is still not verified, here or in the library.
Added: roster tracking reads LibGuildRoster 1.3.0's officers, event log, GUIDs and race (RT-38 to RT-43)
GuildRoster delivered five of UGR's six roster-data requests in its unreleased 1.3.0 (MINOR 34; inbox
thread 2f8b405d, reply 229cb15d) and declined live note changes. Every use is feature-detected, so an
older library tracks as before. Specs first, in Tests/roster_tracking_spec.lua: run
20261015-115520.731 had RT-38, 39, 41, 42 and 43 red and RT-40 green, as written; RT-43 turned out to be
red for the wrong reason (below).
Roster:CompareAtLoginasks for the guild event log once per guild (GR:RequestGuildEventLog) and waits for the answer, so a kick, a leave or an invite while nobody was online names the officer. The answer is taken fromOnGuildEventLogUpdatedalone: the library fires it for the first answer after a request even when the log is unchanged (guildEventLogAsked), so a new guild's log that reads the same as the old one's still arrives (RT-42). A first version also tookGUILD_EVENT_LOG_UPDATEitself; that fallback andRoster.askedLogare removed (Peer Review, thread 767f1f41), and RT-38, 39 and 42 pass through the callback alone (run 20261015-122943.982). If the server never answers, the comparison waits until the next login. A request that could not be sent compares at once. TheUGR_GUILD_CHANGEDhandler resets the request for the new guild.- The library's log rows (
type,player,actor,rank,ago) are mapped toRoster:Diff's shape; the newest matching row wins, as before. A join's inviter comes from itsinviterow. - A live kick's
kickedByand a live rank change'schangedBybecome the change'sby, queued calls included (RT-41). Roster:Refreshreads the race withGR:GetMemberRace(name)only while the record has none (RT-43). The live entry no longer carries arace, because the library's member has none.- Rank list and guild identity stay on
ns.Compat:GR:GetGuildRankscan return a partial list while rank names load, andGR:GetHomeGuildInfo's realm spelling would change every saved guild key.Roster:Rankssays why in its comment. Reasons indocs/design/roster-tracking.md, "Library requests", which also records the declined request: a note someone else edits mid-session is seen at that player's next login. - Specs changed, with reasons in their comments: the
Refreshspec no longer passes a race on the live entry; BN-17 and the GRM import's "member UGR holds" spec now clear the GUID they assume is missing, because the login comparison stores one since 1.3.0; RT-43 set the race table beforeenv.load, which wipes it, so it was red because the race was gone, not because the code was missing. Now set inafterLibs; with the race read removed it goes red on the race (run 20261015-121934.369). - Whole suite 1320 passed, 0 failed, 4 pending (run 20261015-121416.457);
Modules/Roster.lua447/447 (coverage run 20261015-121901.206). Not seen in game, and GuildRoster 1.3.0 is not released, so none of this runs for players yet.
Added: UGR inside the guild window's Members tab (MD-33, MD-34)
A player asked on Discord (thread 82d3cfa0) for UGR's whole window inside the guild window's Members
tab. LibGuildRoster had no slot for a consumer's content there, so UGR asked for one (GuildRoster inbox
cf43f522) and GuildRoster built it into its unreleased 1.3.0 (MINOR 34): RegisterGuildTabPane.
GUI/GuildTab.luaGuildTab:RegisterPaneregisters a pane underns.addon, labelledL["Ultimate Guild Roster"], when the library hasRegisterGuildTabPane;RefreshLocaleregisters it again, which relabels the library's switcher in place.- New
MainWindow:Embed(parent)(the pane'sshown) hosts UGR's TabGroup in the parent the library lays over the member list;MainWindow:Unembed(parent)(itshidden) hands the TabGroup back to AceGUI's pool,W:AttachRawFramestaking the tab content out as it does for the window. The window's tab setup moved into two local helpers (newTabs,startTabs) that both paths share. - One set of tab frames serves both:
Embedcloses a free-standing window, andMainWindow:Openwhile the tabs are embedded selects the tab in the pane instead of opening a window.RefreshActiveTab,RefreshTabsandRefreshLocalenow act on whichever TabGroup is on screen (MainWindow.tabs), not only the window's. MemberPanel:Open(name)with no page docks beside the pane while the tabs are embedded, and closes with it.- Fallback for a library without panes (MD-33, built earlier this cycle): a "Ultimate Guild Roster"
button under the page opens UGR's window docked beside the page (
MainWindow:Open(tab, page),MainWindow.dockedTo), andMainWindow:CloseDockedcloses it when the page is put away. Tests/guild_tab_spec.luaMD-34 (8 specs) red first (run 20261015-143233.003, no pane registered), green in run 20261015-143411.838. MD-33's specs now removeRegisterGuildTabPanebefore enabling, because the button is only the fallback on a library that has panes (reason in the spec's comment).GUI/GuildTab.lua94/94,GUI/MainWindow.lua184/184,GUI/MemberPanel.lua352/352 (coverage run 20261015-143908.196). Not seen in game, and GuildRoster 1.3.0 is not released: the pane's placement is the library's, unseen, and whether UGR's lists fit a pane smaller than the window's width floor is not verified.- Peer Review (thread d0f49e1b):
- U1, sizing: AceGUI's TabGroup lays out its strip and sizes its content only from
SetWidth/SetHeight(AceGUIContainer-TabGroup.lua:359, :453-473), and anchoring alone called neither.Embednow sizes the TabGroup from the pane before the strip is built, and hooks the pane'sOnSizeChanged(once per pane; the library fires no resize event, GuildRoster reply dae91f7c) to size it again. - U5, dialogs when the pane goes:
Unembednow closes the open question (ns.UI.ClosePrompt) and the Log tab's "Clear a range" dialog (ns.Tabs.Log:CloseRange). Before this, the range dialog stayed on screen with no UGR behind it, and its Clear still deleted log entries. Every dialog, and what happens to it when the pane goes: the question and the range dialog close; a member panel docked to the pane closes; the ban dialog, the export window and the GRM import dialog stay open. None of the last three is opened by a tab, and closing the window leaves them open too. The pane has no Escape layer (its proxy would hide the tabs), so one Escape on the range dialog closes the dialog and the guild window together: the client'sCloseWindowshides the open left panel and every shownUISpecialFramesentry in one call (Blizzard classic_eraUIParentPanelManager.lua:1041-1092,UIPanelWindows.lua:25, :31). Accepted: nothing is cleared, since the clear runs only from the dialog's accept. - U4: moving the tabs from one window kind's pane to the other is specced.
- Toggle (minimap button, key binding) while the tabs are in the pane now puts the member list back
(
SelectGuildTabPane(nil, kind), the kind remembered byEmbed). It used to do nothing there. - Six specs: sizing, the two window kinds, the range dialog and question, the ban dialog, the
export window and import dialog, and Toggle. Three were run red before the code (which three was
not recorded). The export/import spec passed as soon as it was written; it is a regression guard. The resize spec resizes the pane itself, not the page, because the
harness fires
OnSizeChangedonly on the frame whose size was set and not on frames anchored to it (Tests/wowapi/env/frames.luaapplySize; requested from WoWAPITesting, inbox df32a280). Removing the hook turns that spec red (run 20261015-145421.185).Tests/guild_tab_spec.lua41/41 (run 20261015-150028.887);GUI/MainWindow.lua199/199,GUI/GuildTab.lua,GUI/MemberPanel.lua,GUI/Tabs/Log.luaandGUI/UI.lua100% (coverage run 20261015-145916.707). - In game only, for the release checklist: whether the lists fit the pane (U2), and whether UGR draws above the pane's background (U3). GuildRoster's advice for U3, if it does not: set the TabGroup's frame level to the pane's plus one after re-parenting.
- U1, sizing: AceGUI's TabGroup lays out its strip and sizes its content only from
Added: member tools on the guild window's Members tab (MD-29 to MD-32)
Players coming from GRM reach member tools from the game's guild window (operator, 2026-10-15, for
a guild that asked for it). The Members tab there is LibGuildRoster's, so UGR uses the extension points
GuildRoster 1.3.0 (MINOR 34) added at UGR's request (inbox thread fa4de08e) and never hooks the
library's frames. Spec first: Tests/guild_tab_spec.lua, written before the code. Its red run was
reported but its run id was not recorded; the first green run is 20261015-111104.636. The spec added
for the "Alt" tooltip line went red with that line removed (run 20261015-113143.558).
- New
GUI/GuildTab.lua(ns.GuildTab, enabled fromOnEnableafter the roster) registers a row click handler, menu items and tooltip lines underns.addon, and only when the library hasRegisterGuildTabClickHandler; an older library leaves the tab as it was. - A left-click on a member UGR holds (current or former) opens the member panel docked beside the Members tab page and parented to it, without opening UGR's window. The click is taken from the library, so it does not whisper; Whisper stays first in the right-click menu. A sister guild's row and an unknown name are left to the library.
- The right-click menu adds "Member details", and for a player who may see the ban list "Ban player" (the ban dialog with the name, class and realm filled in) or "Edit ban".
- The row tooltip adds the ban warning, the main or alt line and the join date, drawn with the
panel's own date wording (
MemberPanel.DateWords). MemberPanel:Open(name, page)docks to the page when given one and re-parents the panel, keeping its strata;MemberPanel:Close(from)leaves a panel docked elsewhere open, so closing UGR's window no longer closes a panel on the guild window, and the page being put away (OnGuildTabPageHidden) closes it. A panel on the guild window stays out of UGR's Escape layer.BanDialog:Open(name)for a player who is not banned now opens a new ban with that name picked.Tests/toc_spec.lualistsGUI/GuildTab.luaafterGUI/MemberPanel.luain all five TOCs.- Whole suite 1313 passed, 0 failed, 4 pending (desk coverage run 20261015-112411.756), with
GUI/GuildTab.luaat 100%;MemberPanel,BanDialog,MainWindow,CoreandenUSat 100% (coverage run 20261015-112135.202). Switches on only once GuildRoster 1.3.0 is released. Not seen in game; least sure of the panel's draw order over the library's page, which it lifts to HIGH strata.
A read-only ban API for other addons
Asked for by FastGuildInvite to import UGR's ban list into its blacklist and to leave the "banned
player is in your guild" warning to one addon (inbox thread a552781e). Spec first:
Tests/ban_api_spec.lua, red before the code (runs 20261014-155130.697 and 20261014-160409.877).
- Three methods on the AceAddon object
UltimateGuildRoster(Modules/Bans.lua, end of file), all reading throughns.Bans, so another addon sees exactly what the Bans tab shows:GetActiveBans()returns a new array of{ name, reason, bannedAt }in the tab's order,{}below the ban rank or guildless, andnilwhile the guild's ranks are still loading (FGI drops its imported bans on{}and keeps them onnil; its reply 1e585326, spec red first in run 20261014-164312.472);IsBanned(name)takes a bare name or Name-Realm;WarnsAboutBans()is true when the player may see bans and the guild's "Banned player present" chat switch is on. No method writes. Bans:CheckReadable()sendsUGR_BANS_CHANGEDwith no id once each time the ban list becomes readable or stops being (the ranks arriving after the roster, a promotion, a demotion), from the existingGUILD_RANKS_UPDATE/GUILD_ROSTER_UPDATEhandlers, fromSettings:Changed(a changed ban rank or guild-info lock), and onUGR_GUILD_CHANGED. The last announced state is one value for the session, guildless counting as not readable, so leaving or switching guilds is announced too; a switch between two guilds whose lists are both readable is announced as well, since the list is another guild's. Peer Review P1 and P2 (thread 8f0023a2) found the guild-leave and setting cases missing from the first build, which kept the state per guild table and was not called fromSettings:Changed; specs red first (run 20261014-162652.865). A reader that imports on the message no longer waits for the next ban to see the list. The Bans tab's own listener ignores the id, so it simply redraws. The readable-to-readable guild switch is spec'd too, driving a second guild the way the event log's specs do (Peer Review reply 7e537804); red with the guild-change clause removed (run 20261015-103250.786).- The list is officer data; the method's comment says a caller must not pass it on below the ban rank.
Nothing can enforce that, and the saved variable
UGR_DBis readable by any addon anyway, so the API gives out nothing that was not already on disk. - Whole suite 1295 passed, 0 failed, 4 pending (run 20261014-165000.455);
Modules/Bans.luaandModules/Settings.luaat 100% (coverage run 20261014-165358.387). Not seen in game.
0.2.0 and 0.1.0 are in CHANGELOG_ARCHIVE.md.
All Relations
- All Relations
- Embedded Library
- Optional Dependency
- Required Dependency
- Tool
- Incompatible
- Include

