Ultimate Guild Roster

Ultimate Guild Roster - Guild Roster Management, Shared Across Your Guild
Back to Files

UltimateGuildRoster-v0.5.0

File nameUltimate_Guild_Roster-UltimateGuildRoster-v0.5.0.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 21, 2026
Downloads
10
Size
314.8 KB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
9273820
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7
  • 12.0.5
  • 5.5.4
  • 2.5.6
  • 1.60.1
  • 1.15.9

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.5.0] - 2026-10-20

Changed: markdownlint runs with every rule on; .pkgmeta drops a key the packager never reads

  • .markdownlint.json no longer disables MD012, MD013, MD031, MD032, MD033, MD040, MD041, MD051 or MD060 (operator ruling 6c26b41b, cited by Peer Review on thread 27f62d35). MD013 has a stated line_length of 120, with tables exempt. MD033 found real defects: bare placeholders such as <name> rendered as nothing. They are code spans now. Long headings keep their titles, and their reference lists moved to a Refs: line underneath.
  • .pkgmeta's curseforge-project-id is gone. The BigWigs packager takes the CurseForge id from -p or from the TOC's ## X-Curse-Project-ID (release.sh, the -p option and the TOC read near line 1351), and all five TOCs carry 1744059.

Added: zone and online status sync (todo #12, Peer Review thread 3b919f51 A4, docs/design/sync.md)

  • A member's record keeps the last zone the game reported (r.zone, from LibGuildRoster's zone, which is last-known and may be nil offline, so a nil leaves the stored one). Roster:Live reads it and Roster:Refresh stores it.
  • The members set's info part carries zone beside seen. Both are presence (PRESENCE in Modules/Members.lua): a change in one alone does not resave info except at a leave, so a member moving between zones puts nothing on the wire, the bound sync.md's KNOWN COST asked for (MD-45). The applier fills a former member's zone and a current member's only when it has none (MD-46).
  • Online status is live on every client; its synced form is seen, which was already built. info has no online flag, because one would be stale the moment it was stamped.
  • The member panel's status line names the last zone after "Last online" and "Left on" when the record has one. Tests/member_panel_spec.lua's former-member example now expects that suffix, the reason stated in the spec.
  • An empty zone is no zone (Peer Review G1, thread 48155858). Lua reads "" as true and LibGuildRoster's login build copies a row's zone as the game gives it, so r.zone = e.zone or r.zone would have replaced a good stored zone with "" and the status line would have ended in " - ". Roster:Live now reads "" as nil, which also keeps it out of the synced info part, and the status line ignores an empty zone, live or stored. Specced in Tests/member_panel_spec.lua, red first.
  • The "" rule is now one function, Roster.ZoneOf (Peer Review F2/F3, thread 6a035fd9), called by Roster:Live, by the member panel's status line, and by Members applyInfo for a received zone, which before copied a peer's "" onto the record and re-shared it. MD-46 in Tests/member_history_sync_spec.lua: a received info part with zone "" leaves the stored zone, red with the guard removed (expected "Silithus", actual ""). The MD-20 comment now says the planted "" stands for LibGuildRoster's login build (F1).

Fixed: the member details panel no longer covers the guild window's side tabs (Discord thread eb04fe87, MD-30)

  • Docked beside LibGuildRoster's Members page, the panel sat on the Communities window's right-hand tab buttons. Derived from Blizzard's own files (identical in the classic_era and live trees): the first tab is anchored at the window's right edge (Blizzard_Communities/CommunitiesFrame.xml:376), and its art is a 64-wide texture at x -3 (CommunitiesTabs.xml), so it reaches 61 past the edge. The panel now docks to the page offset by the page-to-window gap plus MemberPanel.SIDE_TABS_CLEARANCE (61), found by walking up to the window that has ChatTab; with no side tabs (the classic guild page, UGR's own window) the offset stays 0. Specced in Tests/member_panel_spec.lua (MD-30 offset), red first. Reported by Vishiswaz.
  • Not seen in game; 61 clears the whole texture, so a small gap is possible. MoP and Forever trees were not read; they are assumed to share the Communities XML.

Added: the guild log syncs, and a clear sticks for everyone

Refs: todo t7650286ed07f424f, Discord thread 5185513d, docs/design/sync.md "Log sync and clears", LS-01 to LS-25.

  • Operator, 2026-10-18: "you're telling the customer we aren't syncing log or alt data. that's wrong, we need to sync that data." This replaces the 2026-10-17 "no clear travels" rule; sync.md SY-61 and its log section say so.
  • Every line EventLog stores (Add, AddChanges, AddImported) is also saved, in one Sync:SaveParts batch stamped once, as group log:<type>:<YYYYMMDD>:<key fields> (EventLog.LineId), so every observer of one event on one day meets on one group. Lines travel by negotiation, not a live push. EventLog.Lane picks the set: officer-note lines ride onotes, ban lines ride bans (the Bans module ignores them), the rest ride log with a leave line's officer note stripped; banReceived and imported lines stay local. EventLog:ApplyLine appends a received line once.
  • EventLog:Clear is refused below the admin gate (it was gated only in the tab). It saves a clear:<upTo> group in the log set, one part per category, and pushes it once. A received clear is refused unless its author passes the admin gate (Sync.REFUSE["log/clear"]). EventLog:ApplyClear takes each category's latest upTo, removes those lines from the player's log and drops them whole from all three sets (Sync:Drop).
  • No resurrection: the new Sync.REFUSE hook is consulted in Sync:Merge, which every store path goes through (save, live push, catch-up legs 2-4), and refuses a line of a cleared category dated at or before the clear. A cleared line is removed, not stubbed, so no send path can carry it. Growth is one ~60-byte part per ticked category per clear.
  • Also in Sync.lua: Sync:Holds, every log: group in the observation class, clear: groups admin-only, SaveParts(batch, quiet) so the Log tab repaints once per batch, and UGR_SYNC_MERGED sent once per merge.
  • Spec changes, each with the operator's 2026-10-18 decision as the reason: Tests/sync_sets_spec.lua (the applier list gains bans/log, log/clear, log/log, onotes/log; the "log set has no applier" comment is gone), Tests/alts_spec.lua AL-01 (a Link's altLinked line is now the one log group), Tests/event_log_spec.lua ("any rank clears" became "refused below the gate").
  • KNOWN COSTS: each line is stored twice (the player's log and its set) until a migration makes the set the only store; one event seen either side of midnight by two observers becomes two lines; two real events with one key on one day become one line elsewhere; 0.4.0 clients keep cleared lines and waste catch-up rounds until they update; a roster change stored after a clear but dated before it stays local; clears are not applied to sister-guild copies.

Fixed: a member's copy of a log line can no longer turn a leave into a kick

Refs: Peer Review F5, thread 59c1a1a3; B1 to B5, thread 1d546d78; N1, G1 to G3, thread 879457c5; N1, thread 4cf39301; F2 to F5, thread 446a3a40; LS-27 to LS-37.

  • A leave line that names an officer renders as a kick (LEAVE_BY), and any member may save a log line, so one member's copy could put "removed by" on another's voluntary leave on every client. Three checks now share one test, authorTrusted(p) in Modules/EventLog.lua (the part's by is this player or passes ns.Admin:Is):
    • EventLog:ApplyLine fills a held line's missing fields only from a trusted author's copy (F5).
    • refuseLine (Sync.REFUSE for the three line sets) refuses a left or leftUnknown part whose v.by is set and whose author is not trusted, so it is neither stored nor relayed (B1).
    • Sync.REFUSE["log/clear"] uses the same test, and also refuses a clear pushed live whose by is not its sender, compared exactly (B2; G3 -- DeltaSync passes every received sender through NormalizeSender, LibGuildRoster's NormalizeName, DeltaSync.lua:1595-1599, :1623-1624). Sync:Merge takes a fifth parameter from, which only Sync:OnDataReceived's live branch sets; negotiation legs relay parts other players saved.
    • refuseLine also refuses any log line pushed live whose by is not its sender: an honest client never pushes a line live, so this was the one path left that let a modified client forge an officer on a live push (N1, thread 4cf39301, LS-34, seen red first in the session that wrote it). LS-27 to LS-29 now deliver through Sync:Merge with no from, the negotiation path they stand for.
  • EventLog:ApplyClear and EventLog:DropRefused share one local sweep, dropLines(pred) (F4).
  • Sync:Drop's doc comment no longer says every client drops the same groups: that holds for clears only (F3).
  • authorTrusted is false for a part with no by: at Allow all ranks Admin:Is passed every rank without reading the name, so a by-less clear or officer's leave was taken (N1, LS-32).
  • REFUSE judges only parts arriving, so a line stored before these checks stayed held and was served on. EventLog:DropRefused(data) drops every held line refuseLine refuses now from the three sets, and takes the officer off the stored log line received from it (synced). Roster:CompareAtLogin runs it once the roster is complete, never earlier, since the gate reads ranks (G1, LS-33).
  • A refused part is not held, so an officer the roster did not list yet is taken from the next copy once it does (B3, LS-30).
  • KNOWN COST (sync.md "Log sync and clears"): by is trusted as the saving client stamped it. These checks stop an honest client's mistake, not a modified client, which can still name an officer on a part that arrives by negotiation.
  • KNOWN COST (G2): a low rank's own game read that fills the officer on a leave is saved with that player as author and refused by everyone else unless they are an officer or approved, so a kick only low ranks saw in the game's log splits the rest of the guild two ways: a client that already held the leave line keeps it as "reason unknown"; a client that never held it gets no line for that leave at all, because refuseLine refuses the whole part, not just its officer.
  • KNOWN COST (F2, LS-35): the stored line does not record which copy filled its officer, so when a newer untrusted copy this client took before it had these checks is dropped by the sweep, an officer an earlier trusted copy filled on a line received from sync is lost with it. The player's own lines keep theirs. Log sync was never released before these checks, so only a client that ran an unreleased build can hold such a line (Peer Review P2, thread 27f62d35).
  • The sweep runs once per guild, not at every login (Peer Review P1, thread 27f62d35). Judged at every login, it read each author's rank now, so an officer who was later demoted or left the guild had every kick they saved turned back into a voluntary leave on every client.
  • KNOWN COST (F3): which held lines DropRefused drops depends on each client's view of ranks and the approved list, so two clients can hold different sets, as REFUSE already allowed.
  • KNOWN COST (F5): a client whose login comparison never runs (the guild event log request unanswered) never sweeps until one does.
  • KNOWN COST (Peer Review P4, thread 27f62d35): an arriving part is judged by its author's rank on the day it arrives, not the day it was saved, because the record carries no rank. When an officer saves a kick or a clear and is later demoted or leaves, a client that first receives it afterwards (a new member, or one whose sets were wiped) refuses it: older clients show the kick, while the new one has no line for that leave at all (the whole part is refused, so nothing is stored; LS-37), and a missed clear leaves that client holding the cleared lines.
  • Spec change, Tests/event_log_spec.lua EL-16 ("has exactly the designed functions"): the list gains DropRefused, with a dated reason comment; the function is new by design (G1).
  • Specs in Tests/log_sync_spec.lua: LS-27 (F5), LS-28 (B1), LS-29 (own and approved authors; a coverage case, never red), LS-30 (B3), LS-31 (B2), LS-32 (N1), LS-33 (G1), LS-34 (live line sender), LS-35 (F2's cost, pinned). LS-27 was seen red (run 20261019-143728.081-2138368295), LS-32 (run 20261019-161656.803-275384291) and LS-33 (run 20261019-161911.578-1559684992) before their code. LS-28, LS-30 and LS-31 were shown red afterwards by breaking refuseLine's leave clause and the live-sender compare once (run 20261019-162731.714-2466452112), then restored.
  • The once-per-guild sweep is data.tracking.refusedSwept (SavedVariables.lua, a new default, so no migration; a migration could not run it, since it needs the ranks known only at the comparison). LS-36 (a demoted officer's kick stays a kick after the comparison runs again; red run 20261019-190510.788-2225166731, green 20261019-191829.674-846445510) and LS-37 (P4's cost, pinned).

Added: sister-guild alts, linked, shared and shown (docs/design/alts.md "Sister-guild alts", AL-43 to AL-50)

  • Alts:SisterOf(name) names the configured sister guild whose roster holds name. Link accepts such a character when at least one of the two is our current member (two sister characters are refused), and stores the name as LibGuildRoster normalizes it; SetMain takes a sister character already in a group.
  • Alt groups travel to sister guilds as one more shared item, "alts@<GuildId>" (Sync.SLICES.alts): only the alt: groups of config and their tombstones, never the approved list or the macro rules. Our own slice is a read-only projection of our config; a sister guild's copy is kept at sync.sisters[<key>].alts, takes only alt: parts, is never fed into LibGuildRoster, and a merge that changed it sends UGR_SISTER_MERGED(setKey, guildKey, changed).
  • Alts:SisterGroupOf reads a copy by the same view rule as our own groups; Alts:Elsewhere lists the sister characters linked to a name from our group and from every sister guild's group. Alts:Listed carries elsewhere and lists a member only a sister guild's group links (sister entries, which NotePlan skips).
  • The Alts tab draws sister characters under the main as "<name> <<guild tag>>" with class and level from that guild's roster, and opens no panel or menu for them; Nicknames:Tag reads "alt of <main>" for a main in a sister guild.
  • KNOWN COSTS (alts.md): one more hash-list entry per guild against GreenWall's bridge limit; a copy is fresh only while one of that guild's members is online with us; other addons see a sister guild's own links only through UGR, not LibGuildRoster.
  • Spec: Tests/alts_sister_spec.lua (new), written red before the code on 2026-10-19 (red run id not recorded). Green in whole-suite run 20261020-095936.377-3329324809 (1885 passed, 0 failed) with every shipped file at 100%. Not seen in game or over real GreenWall between two guilds.

Added: the TOG Profession Master import brings sister-guild alts (todo #12, docs/design/alts.md TP-12)

  • TOGPMImport:Plan keeps an account's characters in a configured sister guild (ns.Alts:SisterOf) beside our current and former members, stored as LibGuildRoster normalises them. An account with fewer than two such characters, or with none of ours, is skipped as too few: a group of sister characters is their guild's to make. The main is still chosen from our members only, never a sister character.
  • TOGPMImport:HasData uses the same test, so a sister character counts only beside one of ours.
  • Spec: Tests/togpm_import_spec.lua "sister guilds' members (TP-12)", four examples; green in run 20261019-152646.679-4105357631 with Modules/TOGPMImport.lua at 100%.

Changed: the Log tab's search box sits in the top row (EL-27)

  • Operator, 2026-10-18: "put the serch button up after the clear by type button, it will give it more space and make the interface flow better". UI.FilterRow takes an optional { inStrip, width } (the other tabs are unchanged); the Log tab puts a 180-wide box after its last button, so "Search the log" no longer wraps and the chips start their own row at the left edge.

Added: open the log in its own window (LW-01 to LW-09, docs/design/event-log.md)

  • Operator, 2026-10-18: "add a button to open the logs in another window, that can be taller so we can see more rows of data ... we want it to snap to the right side of the G&C window". An "Open in window" button on the Log tab opens GUI/LogWindow.lua: a resizable LibAceGUIWidgets window (560x680 by default, size in profile.logWindow) drawing the same view through the tab's own Log:Render(parent, { inWindow = true }), so the two cannot drift. It docks right of the guild window clearing its side tabs, through UI.SIDE_TABS_CLEARANCE and UI.SideTabsX, moved from the member panel so both use one copy; with no guild window it opens free-standing. With both open, the member panel docks to the log window's right edge and returns to the page when the window closes.
  • It repaints on a new line, a clear, a guild change, UGR_SYNC_MERGED and a chip click in either view, and a language switch rebuilds it. A log switch changed on the settings page shows at its next repaint.
  • Added to all five TOCs after GUI\Tabs\Log.lua; Tests/toc_spec.lua lists it. New Tests/log_window_spec.lua, red first.

Changed: alt notes use the main's nickname (Discord thread d8744ee3)

  • Alts:NoteText fills <main> with the main's nickname (Nicknames:Of) when they have one, else their short name as before, whether or not this player draws nicknames, since the note is read by the whole guild. Asked for by Vishiswaz.

Added: "Set as main" on the right-click menus (Discord thread b1f8288e, docs/design/alts.md AL-38 to AL-40)

  • Alts:SetMain existed and synced, but no control called it, so a main could only be set by the own-characters auto-link or the GRM import. RosterTab.MainItem (GUI/Tabs/Roster.lua) is now the one builder of a "Set as main" item, offered for a current member in an alt group who is not its main, greyed with the admin gate's reason exactly as "Link alt" is, and showing SetMain's refusal over the tab.
  • The Roster tab's menu carries it after "Link alt". The Alts tab gains a right-click menu on alt rows (the Roster tab's, through RosterTab.MenuFor, given the short name rather than the nickname label); headings open none. The guild window's Members-tab menu (GUI/GuildTab.lua) carries it before "Member details".
  • Specs AL-38 (Tests/roster_menu_spec.lua), AL-39 (Tests/alts_tab_spec.lua) and AL-40 (Tests/guild_tab_spec.lua), red first. Asked for by Vishiswaz.

Changed: the Macro tab's queue keeps itself up to date; the Scan button is gone

Refs: Discord thread 1972de6e, docs/design/macro-tool.md MC-18.

  • Scan only ran MacroQueue:Scan over UGR's saved members (it never asked the server for the roster). The queue for the chosen action is now rebuilt on events, never a timer (directive b15a1e31): when the tab is shown, when an action chip is clicked, on the new UGR_MACRO_QUEUE_STALE message (sent after each UGR_ROSTER_CHANGES and UGR_MACRO_RULES_CHANGED), and when the run dialog closes. It never rebuilds while the run dialog is open (MacroRun:IsOpen, a HookScript("OnHide") signal), so a half-finished click is never re-listed; it skips the rebuild when the player's rank cannot carry out the action and shows that reason on the status line.
  • Because the pass now runs by itself: a rule-queued pending item no rule matches any more is taken out; a member already holding an item for that action, in any status, is not queued again ("Clear finished" lets a failed one back); "Remove from queue" and clearing a done item keep that member out until the action's rules change, so a done demotion is never queued a second time.
  • KNOWN COST: a rule change synced from another officer brings dismissed members back as pending. They are listed before Run, never sent unseen.
  • The strip is now Rules, Run and Clear finished. The "Scan" locale keys were removed, as the locale spec fails on unused keys. MC-16 tab specs changed with a CHANGED 2026-10-18 reason (the operator's request, via this thread): no Scan button, a chip click queues on its own, and the rank reason moved to the status line. New MC-18 specs in Tests/macro_queue_spec.lua (including a source check for no timer and no GuildRoster( in the three macro files) and Tests/macro_tab_spec.lua, red first. Asked for by Vishiswaz.

Added: rank checkboxes in the macro rule editor (Discord thread 0a7dda95, MC-19)

  • rule.ranks was already a set of rank indexes (Modules/MacroRules.lua checks and matches it), but the dialog could not edit it. GUI/MacroRuleDialog.lua now shows one "Rank: <name>" checkbox per guild rank from ns.Compat.GuildRanks(). Ticked boxes save as the set; none ticked saves no ranks (every rank); an index the guild no longer has is dropped on save; while ranks are unknown there are no rank rows and the rule keeps the ranks it has. The saved shape did not change, so no migration: rules saved by 0.4.0 read the same. One dialog is built per rank count, as form rows are fixed at build.
  • KNOWN COST: a 10-rank guild gets a tall dialog (18-19 rows). Every rank can be ticked; a rank the player cannot act on is refused when the item is queued, as before.
  • MC-19 specs in Tests/macro_tab_spec.lua, red first. Asked for by Vishiswaz.
  • A rule whose ranks are all gone from the guild, or a synced empty ranks = {}, matches nobody, but opened with no box ticked, which the hint reads as every rank (Peer Review D3, thread 4591b058). The editor now says so in its message line on open. The matcher is unchanged: widening such a rule to every rank could take in officers. Spec "says when none of a rule's ranks are in the guild", red first. A language switch while the editor is open clears that line, as it clears any message in the old language.

Added: the Settings tab (todo #15, docs/design/ui.md UI-23, UI-24)

  • The operator, 2026-10-18: "why don't we create the settings tab". This replaces the 2026-10-14 decision that settings was only a gear. settings is the last entry in ns.Constants.TAB_ORDER, offered to every rank, and the guild window gets a last icon tab for it (GUI/GuildTab.lua, the INV_Misc_Wrench_01 icon Blizzard's own UI uses).
  • GUI/Tabs/Settings.lua feeds the existing options table into an AceGUI BlizOptionsGroup with AceConfigDialog:Open(appName, container), so no control is rebuilt by hand; that container type is the one AceConfigDialog gives a scroll frame. The container sits on the tab's kept raw frame and is never released into AceGUI's pool.
  • AceConfigDialog redraws its own container after each control's set and reads that control afterwards, so GUI/SettingsPanel.lua counts sets and funcs in busy and the tab waits while it is up. The tab redraws when it comes on screen stale and on SettingsPanel:Refresh while shown, never on a plain repaint (which would close an open dropdown).
  • The gear and /ugr settings / /ugr config call MainWindow:Open("settings"). Esc > Options > AddOns still has the same page. SettingsPanel:Open had no callers left and was removed, with its locale strings ("Open the Ultimate Guild Roster page of the game's options.", "Settings cannot open during combat."); the gear's tooltip is now "Open the Settings tab.".
  • Specs, red first: Tests/window_spec.lua UI-23 and the gear, Tests/guild_tab_spec.lua UI-24. Changed with the operator's request as the reason: window_spec, slash_spec, load_spec, bans_tab_spec, guild_tab_spec, toc_spec.
  • KNOWN GAP: a setting changed on the Esc > Options page while the tab is on screen shows on the tab the next time it is shown.

Changed: the settings page's Log and chat section is one line per kind (docs/design/settings.md SET-02)

  • The operator, 2026-10-18, of the settings page: "this is a bad UI/UX, there is way too much wasted space here, can we not consolidate and make it cleaner?". Each event kind was its own titled box holding only its Log and Chat switches, so four kinds filled a screen.
  • GUI/SettingsPanel.lua now draws each kind as one line, flat in the page: a description cat_<c> (the kind's name, width "relative", relWidth 0.5) and then the switches cat_<c>_log and cat_<c>_chat. AceGUI's Flow layout puts the switches beside the name and starts the next kind on the next line, because a second half-width name cannot fit after them; no break control is needed. The saved paths (log.<c>, chat.<c>), get/set, guildless greying and the switches' widths are unchanged.
  • The page has no group left, so countAll no longer walks into groups; Tests/window_spec.lua now asserts there is none, and counts the kind's Log switch at its new key.
  • The other sections were already one control per line with no boxes, so nothing else changed.
  • Specs, red first (run 20261018-101704.189-3822146787), then green (whole suite 20261018-102220.509-3683039031, 1701 passed): Tests/settings_panel_spec.lua ST-11 keeps every old check as its opposite (the row is not a group, has no inline, no args) and adds the row's field set, its order (name, Log, Chat, before the next kind) and each switch writing its own setting; a new ST-15 example draws the page and checks each name, Log and Chat share one line and the next name starts a new one. GUI/SettingsPanel.lua 200/200 lines.
  • Not seen in game. A container narrower than about 340px would wrap the switches under the name.

Added: import buttons on the settings page (docs/design/settings.md ST-16 to ST-19)

  • The operator, 2026-10-18: "we need these import commands to be buttons in the settings tab, so they can be 'repushed' in addition to the / commands".
  • The settings page (the Settings tab and Esc > Options > AddOns) has a new last section, Import, with "Guild Roster Manager" and "TOG Profession Master" buttons. Each opens the same preview as /ugr import and /ugr import togpm, through the one function both use (ImportDialog:Show, ImportDialog:ShowTOGPM, new in GUI/ImportDialog.lua). The slash commands stay. Pressing a button while its preview is up shows the preview again, with a fresh plan.
  • A button is greyed, with the reason in its tooltip, while the admin gate refuses the player (guildless, ranks loading, or not an officer or approved player) and while that addon has nothing for this guild. A refused player can still open the read-only preview from the slash commands.
  • Tests/settings_import_spec.lua (new); Tests/settings_panel_spec.lua's ST-10 and its guildless checks now include the section. Red: run 20261018-180603.908-1226513739, 11 failed on the missing ImportDialog.Show. Coverage: GUI/ImportDialog.lua 100/100, GUI/SettingsPanel.lua 225/225, Slash.lua 87/87. Whole suite: run 20261018-181956.801-2692634767, 1768 passed and 1 failed (IM-04 in Tests/grm_import_spec.lua, a GRM-import spec another agent was changing at the time). Not seen in game.

Fixed: the GRM import read real GRM dates as "Unreadable entry" (docs/design/grm-import.md)

  • The operator's import preview showed 838 skipped as "Unreadable entry", and the operator asked "what does it mean, unreadable entry?". Traced against their real GRM saved file (by the background agent, with a read-only script): almost all were valid data that my date check rejected, and 3 remain skipped after the fix. Five causes:
    • 756 were leaves GRM found in the game's guild event log. GRM stores the day it noticed in the day slots, and the real leave day in the date string, 1 to 6 days earlier. calendar now takes the string's day whenever it is on or before the slots' day, so the leave imports on the day the member left. A string later than the slots, or not a date, is still unreadable.
    • 77 were days past the month's end (31 September, 30 February). GRM's "N days ago" sum borrows the length of the wrong month. The new grmDay moves such a day back by the difference in month lengths, giving the day it stands for.
    • 1 was the guild's rank-name string GRM keeps beside the members. A plain value now counts as a broken member record only when its key looks like a character name.
    • 4 were verified dates with GRM edit time 0. They are rightly not shared, because they have no time to share them by, but the member's own history still imports. 3 remain after the fix.
    • None were log entries: that run's log was already imported.
  • The skip reason now reads "Unreadable date or record", and its locale key was renamed to match.
  • Known cost: the same GRM borrow error can also write a day late without passing the month's end (29 September for 28 September). Nothing in the entry shows it, so those import as GRM saved them.
  • Specs: IM-05a and IM-05b are new. IM-04 now has a ranks value. IM-05's impossible-day example is day 32 instead of 31 February, because GRM really writes 31 February for 28 February (reason in the spec). The skip-reason wording check follows the new label. Red: run 20261018-181258.395-2078321125. Green: run 20261018-181409.201-1499820102, 60 passed. Modules/GRMImport.lua 326/326 (run 20261018-182203.432-3233677980). Label red: run 20261018-183622.082-4286508538. Label green: run 20261018-183725.664-2274735040. Not seen in game.

Added: import alt groups from TOG Profession Master (docs/design/alts.md TP-01 to TP-11)

  • The operator, 2026-10-18: "so we have a ton of alt data in TOG Profession Master. can we not do an integration with that to import alt data?". TOGPM is never a dependency: it is read when the officer asks, through TOGPM.addon:GetGuildDb(), else the TOGPM_GuildDB saved variable.
  • Modules/TOGPMImport.lua (ns.TOGPMImport: Find, Plan, Run) is new in all five TOCs. Find reads global.altClaims, joins entries that share a character into one account, and times each by hashes["accountchars:<key>"].updatedAt when that is a usable realm-clock time, else the realm clock.
  • Plan keeps only current and former members of this guild and skips an account left with fewer than two. A group UGR already holds keeps its main and gains the rest; a character in a different group is skipped, never moved. A new group's main is the highest rank, then level, then name ("Set as main" changes it). A deleted group's tombstone counts as held when judging "older", and an extended group is saved one second past the part it replaces when that part is stamped now or later (as Sync:SavePart).
  • Run is refused by the admin gate; otherwise it plans each account again against what UGR holds at that moment and saves through Alts:Apply. No log entry, as the GRM import's alt groups.
  • /ugr import togpm opens a preview in the GRM import's dialog (ImportDialog:OpenTOGPM / :FinishTOGPM) and nothing is saved until Import. /ugr import now registers in Slash.lua (moved from Modules/GRMImport.lua), and says to type import togpm when GRM has no data and TOGPM has.
  • Specs, red first (run 20261018-102948.698-3138354893, 25 failed on the missing module), then green (whole suite 20261018-104740.290-2474520897, 1727 passed): Tests/togpm_import_spec.lua, 26 examples. Modules/TOGPMImport.lua 161/161, GUI/ImportDialog.lua 92/92, Slash.lua 89/89, Modules/GRMImport.lua 314/314.
  • Checked against the operator's real Classic Era saved file: hashes["accountchars:<key>"].updatedAt and altClaims[<key>] lists of "Name-Realm" are there, after TOGPM_GuildDB's global key (the file has no indentation, so the nesting was read by order). Not verified: realm spelling across a connected-realm cluster, and the dialog in game.
  • Fixed before release (Peer Review H1 and G3, thread 32b7372e): a new group's main is a current member before a former one, then rank, level and name. "Set as main" is never offered for a former member, and TP-03 had asserted a former member as main on level alone; that assertion is changed, with its reason in the spec. Every skip in the preview now counts accounts: an account with characters in other groups counts once as "in another group", not once per character and again as "already linked". New TP-07 example. Red: run 20261018-181647.488-867298654, 2 failed. Green: Tests/togpm_import_spec.lua 27/27 and Modules/TOGPMImport.lua 165/165 (run 20261018-182831.118-2404038761). In that same run, 68 examples elsewhere failed because LibGuildRoster could not load from ../GuildRoster while another workspace was mid-edit. A clean whole-suite run is still owed.
  • Follow-ups (Peer Review I1, I2, A1 to A4, threads 32b7372e and 06b705a7):
    • The preview's rule text still described the old main rule. It now leads with "a current member before a former one" (togpm_main_rule), and the spec checks the wording, not only that the text is shown.
    • The preview's skip lines read "Accounts skipped: N (reason)". A saved account can also be skipped once for a character left in another group, so the skips do not add up with the groups.
    • New TOGPMImport:HasData(): true when the file holds an account naming two of this guild's current or former members (as first built it looked inside one entry only; see "no longer greys out over an outside alt" below for the join it does now). The settings page's button greys on it rather than on Find, which ran at every draw and hover. It is also the guild check: a TOGPM file holding only another guild's accounts now greys the button instead of opening a preview of nothing but "too few".
    • Plan fills any field a caller's context leaves out from Context(), so a context built without current still plans. No production caller passes one.
    • The GRM skip reason's locale key is renamed to match its text, "Unreadable date or record".
    • Red: run 20261018-190256.449-2384813999, 8 failed. Green: togpm, settings import, GRM import and locale specs, run 20261018-190426.085-3707331296, 112 passed. Coverage: Modules/TOGPMImport.lua 187/187, GUI/ImportDialog.lua 100/100, GUI/SettingsPanel.lua 231/231 (run 20261018-191358.436-3544223805). That run's whole suite had 1829 passed and 1 failed: ST-10 in Tests/settings_panel_spec.lua, which does not yet expect the Events section the Events agent is adding.

Added: the log audit tool, in place of the Log tab's Clear button

Refs: Discord thread 5185513d, docs/design/event-log.md LA-01 to LA-10.

  • The Log tab (and the log window) no longer has a Clear button. In its place, Log audit opens GUI/LogAudit.lua (ns.LogAudit): a resizable LibAceGUIWidgets ClearFrame listing the guild's past clears, one row per kind per clear (When, Kind, By), newest first, from the synced clear: records (EventLog:ClearHistory(), new), with Clear by type in its strip. Asked for on Discord: fewer accidental clears, and room for more audit features.
  • Log audit is the admin gate's: greyed with the gate's reason below it and while guildless, and LogAudit:Open() refuses the same way. Clearing itself is unchanged: the same dialog, confirmation, gate and sync (EL-22 to EL-25).
  • The window repaints on a clear, a new line, a log merge (a clear from another officer) and a guild change; a language switch rebuilds it; closing it releases it and closes the clear dialog. Its size is kept in profile.logAudit.
  • Specs, red first (run 20261018-171224.261-2635551481), then green (whole suite 20261018-173436.871-3601119186, 1749 passed): Tests/log_audit_spec.lua (LA-01 to LA-10); log_tab_spec and log_window_spec now pin that neither view has a Clear button, and keep the old button's text, tooltip and greyed states as the audit window's Clear by type. GUI/LogAudit.lua 104/104, GUI/Tabs/Log.lua 142/142, Modules/EventLog.lua 504/504.
  • Known costs: clears made before log sync (0.4.0 and earlier) left no record, so they are not listed; a clear of several kinds at once shows as several rows at one time. Not seen in game.

Added: the game's guild event log is read into log lines

Refs: docs/design/sync.md "Reading the game's log", SY-56, SY-59, SY-60, SY-68 to SY-72.

  • EventLog:AddGameLog(rows, now) turns each row of LibGuildRoster's GetGuildEventLog() that UGR does not already hold into a line: join -> join, quit -> left, remove -> kicked (by the officer), promote/demote with the new rank and officer. An invite makes no line and names the officer of its join. Events missed while offline, such as a join and a leave between two logins, are now kept.
  • Times come from ns.Dates.EpochFromAgo on the realm clock, the same reading the roster comparison uses, so a row and the roster's own line share one LineId. A held line of the same family and member within EventLog.GAME_SLACK (2 hours for an entry of today or days old, 4 days for one months old) is the same event; a held line with no officer takes the game's and is saved again. Other clients that hold the line already take the officer from that copy when its author passes the admin gate (ApplyLine's fill, under "game-log reads and the open views" below); before that fix only a client that never held the line got it (Peer Review thread d7588b98 A2). The days slack was 26 hours until the operator's in-game read of GetGuildEventInfo showed that an entry days old still carries its hours; SY-72 now pins 2 hours, with a days-old row three and a half hours from a held line stored as its own (red run 20261018-202728.885-1696314992, green 20261018-202737.398-2558815825).
  • Roster:ReadEventLog runs after each login comparison and on every OnGuildEventLogUpdated after it. New lines go through the one store, so a cleared line never comes back; they sync like every line, are marked game, and are not said in chat. A roster change for a line the game's log brought first is not stored a second time.
  • Retired: SY-57 and SY-58 (the tombstone dedupe), replaced by the per-day line id and the slack match.
  • Specs, red first (run 20261018-174417.936-2309620719, 8 failed on the missing function), then green (whole suite 20261018-175848.259-1290012888, 1757 passed): Tests/game_log_spec.lua. Modules/EventLog.lua 573/573, Modules/Roster.lua 473/473. Tests/perf/log_perf.lua gains a read of 100 rows of members the log has never seen into a 50,000-entry log, so every run stores and shares: 41 ms against a 50 ms budget with the window closed (run 20261018-180310.156-1196438901). With the Log tab open, the read's UGR_LOG_ADDED no longer repaints the whole log: the tab places the lines (see "the Log tab places new lines" below).
  • KNOWN COSTS (sync.md): an entry near midnight can land on different days on two clients and become two lines; an event under an hour before a clear can read as after it and return once; two real events of one family and member inside one slack read as one; a leave the chat line missed, read from the game's log first across midnight, can be stored twice. Not verified: whether the game rounds an entry's age down or to the nearest hour, and anything else in game.

Changed: the Log tab places new lines instead of repainting the whole log

Refs: EL-28 to EL-30, Peer Review thread 32b7372e F1.

  • A full repaint of a 50,000-line log is about 230 ms, and a game-log read on login sent one to every officer with the Log tab open. EventLog:ViewAdd(rows, entries, opts) now puts a UGR_LOG_ADDED payload's rows into the view already built, where a fresh View would put them (under "New since you last looked" or "Earlier", newest first, a heading added when its group gets its first row), in one merge pass per heading. It returns nil, and the caller repaints in full, while guildless or when the entries are not the newest stored lines in order.
  • The Log tab and the log window use it, and keep the search, the column sort and the headings. They still repaint in full when the guild or the viewed time changed since the view was built, and on Refresh.
  • Specs: Tests/log_append_spec.lua. Tests/perf/log_perf.lua times one UGR_LOG_ADDED of 80 lines dated hours to weeks back against the keystroke budget and fails if the tab rebuilt the view instead.

Fixed: a leave the game's log says was a kick reads as a kick (Peer Review thread 32b7372e G1, SY-73)

  • A game "remove" row matches a held leave line of any leave type and fills only its officer, because a line's id includes its type and a retyped line would leave the old synced copy beside it. So a left or leftUnknown line said "has left" or "reason unknown" while the game knew who removed them. EventLog:Render now draws a leave line that carries an officer as a kick, in the kick colour (LEAVE_BY); its Log tab switch stays the leave's. Spec SY-73 in Tests/game_log_spec.lua, red run 20261019-092858.363-469302649, green 20261019-092928.057-4256919099.

Fixed: the TOG Profession Master import button no longer greys out over an outside alt

Refs: Peer Review thread 32b7372e N1, N2.

  • TOGPMImport:HasData counted guild members inside one TOGPM entry only, while Find joins entries that share any character. Entry A {A, X} and entry X {X, B}, with A and B in the guild and X outside it, made the button say there was nothing to import while the preview would have offered A and B. HasData now joins entries that share a character, as Find does, and stops at the first account with two members.
  • Its member test is now self:Context().member, the preview's own, so the greyed check and the preview cannot disagree. Spec TP-01 "HasData joins entries that share a character, as Find does" in Tests/togpm_import_spec.lua, red run 20261019-093104.480-930090711.
  • Spec change, with its reason (TDD rule 4): the TP-01 example "HasData does not join the claims into accounts" is renamed "HasData does not call Find". N1 asked for HasData to join entries, so the old title asserted the opposite of the decided behaviour; what the example checks, that the greyed state never runs Find, is unchanged.
  • Whole suite and coverage after G1, N1 and N2: 1832 passed, 0 failed, 2 pending; Modules/TOGPMImport.lua 194/194, Modules/EventLog.lua 634/634 (run 20261019-095209.631-3129411525).
  • Then (Peer Review A3, A5): Find and HasData share one local join(claims, weigh, onOwner), so the two cannot disagree on what an account is; HasData's doc comment now says it reports whether TOGPM holds data for this guild, not that anything will import (Plan can still skip every account). Four spec texts in Tests/settings_import_spec.lua and Tests/togpm_import_spec.lua said "entry" or "joins every claim" where the code now works on accounts; titles and comments only, assertions unchanged. 46 passed (run 20261019-122016.148-1847221413); Modules/TOGPMImport.lua 179/179, whole suite 1837 passed, 0 failed, 2 pending (run 20261019-123021.306-2334765621).

Fixed: game-log reads and the open views (Peer Review threads 47d312b3 A1, A2, A4 and 6a128232 S1)

  • A read that fills a held line's officer now sends one UGR_LOG_CHANGED(lines), so an open Log tab, log window or export window shows the kick text at once instead of after Refresh (SY-74). The save itself stays quiet.
  • A received copy of a line UGR holds already fills each field of v the held line lacks and overwrites none (EventLog:ApplyLine), so another client's filled officer arrives and an older or hostile copy cannot rename a held one. Two repaint routes, one per source: the player's own game read (AddGameLog) and the player's own roster change that fills another client's line under another id (AddChanges, SY-75, seen red first in run 20261019-221917.636-1217582403) send UGR_LOG_CHANGED, both through one local changed(filled) that also saves each filled part again (Peer Review F3, thread 1323f2fc); a received fill sends nothing itself (a catch-up can bring thousands) and relies on the merge: Sync:Merge repaints the shown tab (MainWindow:RefreshActiveTab) and sends one UGR_SYNC_MERGED, which the log window and now the export window hear for every set a log line rides (EventLog.IS_LINE_SET). The log audit window lists clears only, so a filled officer does not change it and it needs neither.
  • EventLog.LEAVE_TYPES is the one list of leave types, made from TYPES by category; the kick colour, the game read's leave family, Stats' turnover and the member history's leave family are made from it or from TYPES.
  • A join or leave the login comparison found no game row for is marked loose and keeps the login's time. A later read now matches it to a row of its family and member at or before it at any distance, not only within the 2-hour slack, so an event 2 to 26 hours before the login is no longer stored twice (SY-68b). It takes only the newest such row: a member who left, rejoined and left again while nobody was on keeps both leaves, the older at its own time (SY-68c, red run 20261019-141149.348-515949060, green 20261019-141909.565-2448385596).
  • One departure is one line on every client (Peer Review X1, thread 6a128232). The loose mark and its after (the previous comparison) now ride in the line's part (contentOf, outside v, so the id is unchanged), so a client that receives a login line matches it as its author does (SY-68d). A received line, or the player's own roster change, that is a held line under another id (sameEvent in Modules/EventLog.lua: one of the two loose, same family and member, no line of that member between that breaks them, the nearest) fills that line's missing fields and is not stored a second time (SY-68e, SY-68f, SY-68h; SY-68d and SY-68e seen red first, run 20261019-181726.466-2903893620).
  • A login line matches nothing before its previous comparison less the 2-hour slack (looseHolds; Peer Review Y1), in whichever order the line and the row or other line arrive (SY-68g, red run 20261019-194344.138-1254457041). Specs whose event lies hours before the second login now date the first comparison two days earlier (earlier in Tests/game_log_spec.lua), with a dated reason.
  • A login line no longer takes an older row of its family when a newer join or leave row of that member lies between, or, for a leave, a rank change (a member who quit, came back and then left with no row, such as a deleted character): the old quit is its own line (SY-68i, red run 20261019-201029.971-3924357829). The first cut broke on any row of the member, so a login join followed by a promotion wrote a second join (SY-68j, red run 20261019-202031.640-2313016184). One rule, breaks(family, other) in Modules/EventLog.lua, now serves both AddGameLog and sameEvent, whose "between" test had the same over-broad rule for a join (green run 20261019-202325.300-607778809). AddGameLog's header comment and Tests/game_log_spec.lua's HELD header said a newer row "of any family" blocks the match; both now state the breaks rule (Peer Review NF1, thread 6a128232).
  • A comparison that was postponed (the mass-leave guard) writes no leave or rejoin, yet it still moved tracking.lastCompareAt, so the next login's lines for those members got an after they were never seen at, and a game row from before the postponed login became a second line. Roster:CompareAtLogin now leaves after off a leftUnknown or rejoin when the previous comparison was postponed (Peer Review Z1, thread 6a128232). SY-68l pins departures (red run 20261019-222749.984-1638872203, green 20261019-223247.539-2825838353); SY-68m pins the rejoin half (green in run 20261020-095936.377-3329324809; it was not seen red before its code).
  • A held line that a trusted copy names is found under that name afterwards: a name is part of both indexes a line is kept under (its id in held, its member in named), so fill drops both when it adds a name, and the next read rebuilds them (SY-68n by name, Peer Review N1; SY-68o by id, Peer Review NF2, red with only named dropped in run 20261020-093611.710-1868330396, green 20261020-094043.619-4049684118; thread 99c42f7e). Without the held drop, a non-loose copy of the same event under the named id was stored as a second line. fill now takes the guild's table it fills for, instead of reading ns.Guild:Data() again.
  • fill no longer drops the two indexes on a name fill; it adds the line under its named id and its member in place, and keeps its old nameless id (Peer Review NF3 and F1, thread 1fa065d4). Dropping lost the old id, so a newer copy of the nameless group missed the line, found no match by name, and was stored as a second, nameless line; and each name fill rebuilt the whole by-id index. SY-68o now ends with that newer copy, expecting one join line: red with the drop (run 20261020-110507.803-1585940810, game_log_spec.lua:581, a second join nil line), green after (run 20261020-111027.885-289384816).
  • That old id is now saved on the line (e.ids) and index() reads it, so it survives a prune and a /reload. Kept only in the session index, it died at any prune, not just a /reload as the first KNOWN COST said (Peer Review caae15b6, thread 1fa065d4). fill keeps the old id whenever a fill changes the id, which covers every KEYS field, not only the name (caae15b6 b), and a name fill drops named so it is rebuilt in log order (caae15b6 c). SY-68o now goes on through a prune and a fresh login, and the new SY-68p fills a promote's missing rank: both were red without the change (run 20261020-130000.130-1935203480: a second nameless join after the prune; a second promote under the new id) and green after it (run 20261020-130037.655-2865010528). Those red runs stopped at SY-68o's 'after a prune' and SY-68p's 'a copy under the new id', so the later 'after a /reload' and 'after a prune' assertions were only ever seen green. ids is an optional field, absent on older lines, so no migration is needed. KNOWN COST (caae15b6 a, no spec): a non-loose copy under the filled id that arrives in an earlier merge than the copy that fills the line is stored as a second line. Within one merge the appliers run in sorted group order, so the shorter, unfilled id comes first.
  • sameEvent reads only the arriving line's member's lines, through a by-name index named kept beside the by-id one; store is the one way a line enters data.log and keeps both, and prune (which compacts data.log in place) drops both. A catch-up now costs each line its member's lines, not the whole log (Peer Review P5, thread dc4a4ae9). AddGameLog reads the same index instead of building its own each read (N4, thread 99c42f7e).
  • SY-68k: a received loose join folds into a held join across a held promote of that member (a rank change splits only two leaves), one join line, filled, one promote line (red with breaks forced true, run 20261019-213928.013-1881765527; Peer Review F2, thread dc4a4ae9).
  • Tests/perf/log_perf.lua gains a catch-up case, CATCHUP (3,000) received lines into a 50,000-line log. KNOWN COST (sync.md, "Already held"): 184 ms against a 250 ms budget (run 20261019-231850.832-4240783222) on a busy machine, so not a trustworthy figure; the fold test for loose lines and the index's first whole-log build are not timed (Peer Review N2, N3).

Added: the Events tab, join anniversaries onto the guild calendar

Refs: todo #10, MEMBER-10, docs/design/member-details.md "Anniversaries and the guild calendar", EV-01 to EV-09.

  • ns.Events (Modules/Events.lua) and ns.Tabs.Events (GUI/Tabs/Events.lua) list current members' upcoming join anniversaries, from their newest verified join, from today to events.days ahead (default 14, 1 to 90), optionally mains only (events.mainsOnly). The list is built on every ask and never saved.
  • Add to calendar calls Classic Calendar's API v3 CreateGameEvent (GUILD_EVENT, 12:00 realm time, title and description cut to 31 and 255 bytes without splitting a character). Classic Calendar opens its guild-event window filled in and the player presses Create, so a true return is not taken as added: an entry counts as added once a guild event with its title is read back on its day (GetDayEvents, API v2), and is then shared as a config-set event:<id> record so it drops off every officer's list.
  • Ignore and Ignore all are per guild, this account only, never synced; an ignored entry whose day has passed is pruned.
  • Add is greyed with its reason: on MoP and Retail (the game's own calendar is not built yet), without Classic Calendar 3, outside the admin gate, or with no row chosen.
  • The list rebuilds on the game's CALENDAR_UPDATE_EVENT_LIST, an event, not a timer.
  • /ugr events; an Events section in settings; ClassicCalendar in every TOC's OptionalDeps.
  • Runs: Tests/events_spec.lua 33 passed (run 20261019-121434.508-620139036); Modules/Events.lua 112/112, GUI/Tabs/Events.lua 57/57 (run 20261019-121214.242-840888834). Whether the specs were seen red before the code was written is not known: the agent that wrote them stopped before reporting.
  • KNOWN COSTS: if the player edits the title in Classic Calendar's window, the read-back never matches and the entry stays listed until ignored. Not seen in game: the window opening, Create saving, and whether the server refuses an anniversary of today once 12:00 realm time has passed.
  • Fixed before release (Peer Review N1, thread c3126d79): with the Events tab showing, each entry found on the calendar was saved through Sync:SavePart, whose merge repainted the tab, whose repaint built the list again, so one refresh built it once more per entry found (3 builds for 2 found). Events:Queue now collects what it finds and saves it after the loop in one quiet Sync:SaveParts(batch, true), then pushes only the groups the store actually holds, so a refused save goes to nobody. KNOWN COST: SaveParts stamps at the plain realm clock, without SavePart's floor over a held part or tombstone; harmless while nothing deletes an event: group. The quiet save also skips UGR_SYNC_MERGED; no listener of that message reads the config set (they act on the log, bans and onotes sets only), so nothing goes stale (Peer Review F1, thread 667d101f). The spec helper eventSaves now also records batch saves (a helper change, no assertion changed). Two EV-09 examples: the build-count one was red (run 20261019-125008.827-400912199, 3 builds) then green (run 20261019-125028.165-3182967167, 35 passed). The refused-gate example passed before the fix and was never seen red: it guards point (b), it is not a red-first spec (Peer Review F2).
  • The CurseForge page's Events bullet no longer says "in one click": Add opens Classic Calendar's window filled in and the player presses Create (Peer Review E2).

[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 records logClearedAt, so Add and 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_VERSION 8) 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_PAD 2 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 (AddChanges does not ask Cleared). Peer Review N1 (thread 8d43f4c2): such a change arriving while the question is up is not removed, because Clear looks only at the entries Counts counted (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.lua reads the category list out of docs/design/settings.md rather than keeping a third copy, and asserts SavedVariables.CATEGORIES matches it in order.

Added: SY-54, whole-client login catch-up over the hash protocol

  • The last pending sync spec, which "waited on the protocol", is written: three examples in Tests/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 no by (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 own BroadcastItemHashes send, 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 with wow.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.SHARED is { members = true, log = true }, which also turns the GreenWall bridge on in Sync:Start.
  • Each guild's copy of a shared set is its own DeltaSync item, "<set>@<GuildId>" (Sync:WireKey, Sync:GuildId: ComputeHashV2 of LibGuildRoster's "Faction-GuildName" key in lower case). Our own guild's wire key reads our set; a sister guild's copy is kept at sync.sisters[<guild key, lower case>][<set>], and Sync:Merge calls no applier and sends no UGR_SYNC_APPLIED for 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:IsSetPeer is replaced by Sync:Takes and Sync: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. answerLeg3 merges a leg-3 give only from someone it takes from, askLeg3 gives only to someone it serves, and a live push is taken only for a plain key.
  • Sync:MyHashes lists our guild's wire keys and our copy of each configured sister's after the plain sets; hasContent accepts exactly those keys.
  • Peer Review S1 (thread 51800e63) fixed in UGR: Sync:OnOfferReceived now 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 edit data.items is 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, WireKey missing and the bridge off). Tests/sync_spec.lua's bridge example now asserts the bridge is on, and Tests/sync_hash_scan_spec.lua allows Sync:GuildId as a hash site, as it does Sync: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.lua 687/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:PruneSisters now drops every copy whose guild GetSisterGuildKeys no 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.asking entries now keep the provider's name as DeltaSync gave it for that. It runs on LibGuildRoster's OnSisterConfigChanged and at the start of Sync: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 real SetSisterGuildNames): red first (run 20261017-134100.170, 4 failed: the copy stayed, PruneSisters missing, nothing released), green in run 20261017-134129.123. Whole suite green, run 20261017-134508.434 (1598 passed, 0 failed, 3 pending); Sync.lua 706/706 lines.
  • Peer Review follow-ups (thread a93c588f). N1: PruneSisters also 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_CHANGED releases every open pull's DeltaSync slot before it resets Sync.asking, through one release helper shared with PruneSisters. 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 without string.lower on the listed keys turns the case example red (run 20261017-160127.998).
  • Peer Review S3 (thread 777f80a0): PruneSisters called GetSisterGuildsTs unguarded, so a LibGuildRoster too old to have it raised there. It now feature-detects the method, as Compat.CanWriteNotes does, 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:390 calling a nil method), green in run 20261017-172727.611.

Changed: SY-66 runs on the harness's GreenWall model (WoWAPITesting pin 42bebe3)

  • Tests/wowapi moved from 6bfbb37 to 42bebe3, which adds env/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 installs env.greenwall with Test Guild and Sister Guild, delivering with wow.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's ok says 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-long refusal 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.lua 706/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 six too-long refusals, 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 _BridgeHashList during 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 OnDataRequest and not in the offer filter. Sync:Serves now holds IsSendPeer and OnDataRequest calls Serves alone, 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 by Tests/greenwall_relay.lua (a GreenWallAPI v2 stand-in, until the harness models GreenWall). A sister member's join, leave and log line reach our sync.sisters copy 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 askLeg3 checked Sync:Serves, which then began with IsPeer and ignored rank. That is a send SY-14 forbids. Since G1, Serves holds 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: onLeg finishes 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.lua 689/689 lines.

Fixed: no data set is taken from a sister guild; the GreenWall bridge is wired and off

Refs: SY-64, docs/design/sync.md "Sister guilds".

  • Sync:IsPeer already passed a sister guild's members (IsInAnyRoster), and the receive path took any set from any peer. So a sister guild's config (the approved list) could merge into ours and grant admin here, and since no members part names the guild it was observed in, a sister guild's leaver would become our former member through Members adopt and be relayed to our guild (Peer Review S2, thread 51800e63). New Sync.SHARED (empty) and Sync:IsSetPeer(setKey, name): a sister member is a peer for no set. OnDataReceived and OnDataRequest check it; Sync:Ask checks it and releases a refused offer with p2p: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:Start passes greenwall = C.FOLDER (GreenWall's addon id must be the TOC name, not the namespace) only while Sync.SHARED names 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) and Tests/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.GuildControlGetRankFlags and guild.setRankFlags (UGR's request, inbox 44877338). Tests/member_history_sync_spec.lua's stand-in is gone; MD-44 sets rank flags through guild.setRankFlags and still removes the call for the older-client case. The only other entry crossed (around_each_file comment) 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 } (and rankIndex, rankName); info carries { name, guid, class, race, level, rankIndex, rankName, publicNote, seen }.
  • Sync:SavePart gains nopush (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 through Members:ShareObserved (only when no part is held under that id). Members:ShareInfo runs after every comparison for each current member, on a leave (where seen counts) and in the live join, rank and level handlers. Entries go in through Members:Record, which replaces another client's copy of the same event instead of adding a second.
  • Modules/Members.lua applier: adds each received observation the record lacks as source = "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). info fills 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. applyDate no longer fills a peer's observation with an officer's date.
  • Tombstones: ClearJoinDate / ClearPromotionDate also 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 in OnInitialize, before the guild key resolves), saves in one Sync:SaveParts batch a part for every dated entry of every record (sync-added and officer-corrected entries excluded) at its own setAt, and one info per record at its newest setAt.
  • Officer notes travel only to ranks that may view them (Peer Review P1, thread 81e1e4c4): a fifth set, onotes, group onote:<name lower-cased>, part note = { 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.lua holdsSet); one who may answers and pushes it only to Sync:IsNotePeer: a send peer at or above the player's rank whose rank flags do not bar viewing (ns.Compat.RankCanViewOfficerNote, flag 11 of C_GuildInfo.GuildControlGetRankFlags). Sync.APPLIERS.onotes fills a former member's officerNote. 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 the onotes set), then green (whole suite 20261017-115938.101, 1571 passed, 0 failed). Changed because the login comparison now writes the members set: bans_spec BN-01 and grm_import_spec IM-12 compare before/after; sync_protocol_spec, sync_sets_spec, sync_tab_spec empty the members set after login; sync_sets_spec's set list (SY-43) and applier list gain onotes. The harness has no GuildControlGetRankFlags; the spec stages a stand-in.
  • Harness pin moved dd620e5 to 549bbc9.
  • 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 onSyncAccepted does.

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.lua gains MACRO_BYTES (255, a chosen bound; the real macrotext limit is not measured), Scan(action, now) (reads MacroRules:Rules once 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 the UGR_MACRO_QUEUE_CHANGED message. Peer Review Q1 (thread 18d2d5bc): Check now sends it when an item's status changes and StartBatch always does, so the tab repaints a landed action and an open run dialog re-lists.
  • APICompat.lua: Compat.GuildActMacro builds /gremove, /gpromote or /gdemote from the client's SLASH_GUILD_UNINVITE1 / PROMOTE1 / DEMOTE1 globals (names confirmed from the classic_era source by Peer Review; the enUS fallback spellings are not verified). GuildRemoveMacro now 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 combat preClick settles 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 items StartBatch returned. In combat it does nothing. postClick empties the text again. Peer Review F1, M1-M3 and N1 on thread 18d2d5bc. No saved macro is created or edited.
  • GUI/Tabs/Macro.lua and GUI/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 into TAB_ORDER between 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, because docs/design/alts.md used 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

Refs: Peer Review X1, X2, thread 18d2d5bc.

  • X1: W:CreateSecureActionButton registers its button for the down and the up (LibAceGUIWidgets-1.0.lua:3794), so one physical click can run PreClick and PostClick twice, and the secure handler acts on one half only (classic_era SecureTemplates.lua:805-808). The down started the listed batch, the down's PostClick emptied the text and re-listed the NEXT batch, and the up's PreClick started 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 reaches PreClick in a client is not verified; the fix is right either way. GUI/MacroRun.lua: the down's preClick starts the batch, the down's postClick does nothing, the up's preClick keeps the batch (MacroRun.downPressed), and only the up's postClick empties 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's Settle; the new MacroRun:Rest() empties the button on Open and 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 postClick hid 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.lua KickDialog).
  • X2: MacroQueue:Settle sends UGR_MACRO_QUEUE_CHANGED when 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 in Tests/macro_queue_spec.lua, and the kick example in Tests/roster_menu_spec.lua now clicks the down (dialog stays, Remove armed) before the up. GUI/MacroRun.lua, Modules/MacroQueue.lua and GUI/Tabs/Roster.lua are at 100% line coverage (run 20261017-095427.649). docs/design/macro-tool.md gains "One click, one batch".
  • Follow-ups from Peer Review on thread 8edab7cf: the combat-start comment in GUI/MacroRun.lua now cites warcraft.wiki.gg API_InCombatLockdown (lockdown begins after PLAYER_REGEN_DISABLED fires, so Rest()'s SetAction applies at once; not measured in a client) (F1). The abandoned-down example in Tests/macro_tab_spec.lua asserts behaviour instead of the internal downPressed field: after combat starts, a lone up presses (one Settle) (F3; green run 20261017-100819.548). An up-only option for W:CreateSecureActionButton was 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", one PreClick/PostClick pair 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 ActionButtonUseKeyDown on (classic_era SecureTemplates.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.officerRank may now be saved as "all" (Admin.ALL_RANKS), read by Admin: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:Set refuses, the dropdown shows the lock's rank greyed). Every reader goes through OfficerRank(), 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 in Tests/admin_gate_spec.lua, two ST-05 examples in Tests/settings_spec.lua, and the ST-12 values example in Tests/settings_panel_spec.lua now 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() gives UGR{o=all} instead of today's lowest rank number, which would have put a rank added after the paste outside the lock. Sync.ParseGuildInfo reads o=all (lower case, the o letter only) as Admin.ALL_RANKS; Sync:GuildLock() turns it into the guild's lowest rank as it is read; the new Sync:GuildLocksAll() says the lock is all, so Admin:AllRanks() is true under it and the dropdown shows "Allow all ranks" greyed. Sync:CheckGuildInfo() counts o=2 becoming o=all as a change even when rank 2 is already the lowest. v0.3.0 reads o=all as no o and falls back to its own setting (fails to the local setting, never open). The admin_gate_spec example that pinned the old UGR{o=2} as a known cost now expects UGR{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

Refs: 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.SenderNameFilter and the fallback ChatFilter now pass the message to ChatTag(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's canaccessvalue). 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.md gains 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:Enable returned before the fallback on any library without panes. Removed: GuildTab:AddButton, Label, the buttons table, the docked case of PanelPage, MainWindow's dock helper, dockedTo and CloseDocked, the page argument of MainWindow: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 the buttons table is gone. The MD-33 and MD-35 "library without panes" setups now unregister the pane path's OnGuildTabPageShown handler, because the new Enable returns early and no longer overwrites it; in game Enable runs 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 Rekey spec, also numbered MD-33 (0.3.0's entry, in CHANGELOG_ARCHIVE.md, calls it that), is now MD-36 in member-details.md, Tests/members_spec.lua and the Members:Restore comment. 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/alt parts (admin class, Sync.lua ClassOf) 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); commit saves views, never raw parts; edit plans carry old; HeldAt, Apply (through Sync:SaveParts), FeedAll; the applier OnSynced registered as APPLIERS["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 in parkedAltGroups, since it would fail the read-time check. Constants.lua: SCHEMA_VERSION = 7 and ALT_GROUP_MAX = 75, shared by Alts.MAX and the migration. SavedVariables.lua: the altGroups default is gone.
  • Found while turning green: the applier walked ipairs({before, now}), which stops at a nil before, 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 in sync_two_peers_spec.lua (AL-32's v0.3.0 client is modelled as this build without the applier), AL-33 in migrations_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 gains config/alt; nicknames_spec and nickname_display_spec receive groups as pushed parts, since the admin gate refuses Apply at 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's members order, 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 (now in CHANGELOG_ARCHIVE.md) 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's return nil after the owner-key loop. GuildRoster 1.3.0's CanonName trimmed before stripping a stray colon, so "E :" was filed under owner key "E " and the real library reached that line; the alts_spec example briefly drove the real library with that name (run 20261017-083511.780). GuildRoster then fixed CanonName in 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 replaces GetAltGroup on 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.md no 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 the seen day in a member's info part; zone and online status are planned and not built, and every passage of sync.md now 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, and docs/design/alts.md marks 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 a pending stating 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's set field points at ExportWindow.SETS, which also holds deaths.
  • Whole suite and coverage on the v0.3.0 tree, before the alts_spec change: 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, with Modules/Alts.lua 448/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 (now in CHANGELOG_ARCHIVE.md) 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's range and all clear modes and EventLog:Clearable (Peer Review F1, thread ebe08e19). Nothing outside Tests called them; the Log tab clears only the rows it shows, so Clear now takes "filtered" with rows and raises on any other mode. The A4 range-error spec went with the mode. Specs that cleared everything with Clear("all") (admin_gate, export_window, log_tab) now pass every entry as rows. New spec: the module exposes no Clearable, and "range", "all" and "most" raise; red first (run 20261017-085212.223). docs/design/event-log.md updated.
  • 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 marked free: closing the window or the Members-tab pane leaves it up, as U5 / MD-34 decided (thread d0f49e1b), and ns.UI.ClosePrompt(all) closes a free prompt only when all is 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 in grm_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.md says 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 from ns.Migrations.list were already in the code. RT-44 is now proven: with the guard put back to r.race == nil it 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.lua and APICompat.lua at 100% (run 20261017-090705.451).

0.3.0, 0.2.0 and 0.1.0 are in CHANGELOG_ARCHIVE.md.

This mod has no additional files