promotional banner

TOG Tools

ToGTools is a convenient place to aggregate one off tools that don't make sense to have it's own addon.
Back to Files

TOGTools-v0.8.9

File nameTOGTools-TOGTools-v0.8.9.zip
Uploader
PmptastyPmptasty
Uploaded
Sep 24, 2026
Downloads
33
Size
2.3 MB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
8960278
Type
R
Release
Supported game versions
  • 12.0.7
  • 11.2.7
  • 5.5.4
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.60.1
  • 1.15.9

What's new

Changelog

[v0.8.9] (2026-09-23) - Sister-guild lines cross at once through GreenWall 1.14.0's relay; older GreenWalls wait for a key press

New Features

  • GreenWall 1.14.0+ relays Gratz and Smack lines with no key press. At TOGTools' request (inbox contract 898b1e4c, the operator's directive that it be an API for ANY addon), GreenWall 1.14.0 added GreenWallAPI.SendChat(addon, text, chatType) (API.lua, GreenWallAPI.version == 2). It carries the line as addon messages: a WHISPER to one GreenWall user per co-guild, who re-posts it as a GUILD/OFFICER addon message inside their guild. Classic has no CHANNEL addon messages, and SendAddonMessage has no hardware restriction. New relayThroughApi in Compat.lua calls it from bridgeThroughGreenWall when gw.config exists and the API is version 2+, and returns before the key-press queue. It does not post to the sender's own guild, so the real guild send in addon.Chat.Send stays. The result table is read: ok with truncated prints "was cut short"; no-peers is debug-only, because no co-guild member who could have seen it is online; not-configured, refused, too-long, secret and any unknown reason are printed with the quoted line, like every other lost relay. pcall'd, so a raise prints "GreenWall reported an error" and never takes down the handler that already spoke in guild. Older GreenWalls (no API, or version 1) fall through to the unchanged key-press queue below. Compat.lua now names addonName from ... for the API's addon-id assert. Specs: Tests/gratz_spec.lua "on GreenWall 1.14.0+ (GreenWallAPI version 2)", 10 examples against a stand-in of the documented contract: fire-time relay with no key press and no CHANNEL send; OFFICER; no age drop; silent no-peers; each refusal reason told; truncation told; a raising SendChat; not calling it before gw.config; version-1 fallback; party/raid/say not bridged. Full suite: 1037 passed, 0 failed. GreenWall 1.14.0 shipped (GreenWall commit 3893010); its shipped API.lua and Relay.lua were re-read before release and match the stand-in: version = 2, the SendChat(addon, text, chatType) signature and the five reason strings. Not verified in a client: no whisper-then-guild round trip has been seen in game.

Bug Fixes

  • Gratz and Smack lines never reached the sister guilds: the v0.8.8 GreenWall relay was dropped by the client every time. I built the v0.8.8 bridge on GreenWall's channel send (gw.config.channel.<guild|officer>:send), which ends in SendChatMessage(segment, "CHANNEL", nil, number) (GreenWall Channel.lua:366-370). A numbered CHANNEL send is hardware-event restricted indoors and outdoors (warcraft.wiki.gg API_SendChatMessage), so an event-driven Gratz or Smack line was lost silently. A typed line works only because GreenWall's ParseText hook runs inside the Enter key press. Addon messages on a custom channel are no way round it: the wiki says they are disabled on Classic for exactly this reason.

    Fix: bridgeThroughGreenWall no longer sends. It queues {msg, chanType, key, t} and shows a hidden relay frame built at Compat.lua load; the frame's OnKeyDown (flushHeld) sends every held line through GreenWall inside the player's next key press. GreenWall's send is synchronous while connected (Channel.lua:277-382), so the CHANNEL send happens inside that press. The frame calls SetPropagateKeyboardInput(true) before EnableKeyboard(true) so every key still reaches the game; propagation is combat-restricted, so a load in combat (a /reload mid-fight) arms the frame on PLAYER_REGEN_ENABLED and lines wait until then. Lines older than 120 s are dropped rather than sent late; at most 5 are held, oldest dropped. Either drop is printed to the player through addon.lib:Print (reportRelayProblem), quoting the line and the reason, because a relay that never arrives looks identical to one that did from the sender's own chat, so a debug-only drop was invisible to the one person who could notice it (peer review, 2026-09-23). GreenWall is resolved again at send time, and each held line goes through sendIfConnected, which asks the channel is_connected() (GreenWall Channel.lua:125-138, a fresh GetChannelName each call) before sending. That check is load-bearing: a GreenWall that is loaded but off its bridge channel does not refuse a send, it queues the segment and returns 0 without raising (Channel.lua:325-326, 352-381), and the line would then go out on a later flush outside any key press, where the client drops it. A disconnected channel, missing channel objects, and a GreenWall error caught by the pcall are each printed through reportRelayProblem with their own reason. The error notice says the line may not have reached the sister guilds rather than that it was not passed: tl_flush drains its queue FIFO (Channel.lua:354-376), so a raise on a segment GreenWall queued earlier leaves ours queued, and GreenWall's next send can still deliver it (peer review, 2026-09-23). GreenWall also cuts a long line rather than splitting it: the segment opcode#guild_id##message goes through strsub(..., 1, GW_MAX_MESSAGE_LENGTH) (Channel.lua:322; 255 in Constants.lua:79), so the tail is lost with no error. A typed line is cut the same way; greenWallCutsLine now tells the player when a relayed line was cut short, since the home guild saw it whole. That notice goes through the same printer, so it is named reportRelayProblem rather than for the not-relayed case alone. Location: Compat.lua.

    Not verified in a client: that an insecure OnKeyDown counts as the hardware event for a CHANNEL send on Era, and that the frame still receives keys in combat. Needs an in-game test.

    Specs: Tests/gratz_spec.lua "the GreenWall bridge", rewritten. The v0.8.8 specs passed because their GreenWall stand-in recorded the call and never asked the client whether it would deliver it. The stand-in now makes GreenWall's real CHANNEL send through the harness chat gate, and carries is_connected() plus GreenWall's queue-without-raising behaviour when disconnected: one example pins that a send with no key press lands in wow.dropped, and the rest pin hold-at-fire, relay-on-key-press, relay-once, officer routing, the non-bridged types, GreenWall absent, no send function, a GreenWall error, 120 s expiry, the 5-line cap, GreenWall gone at press time, GreenWall disconnected at press time (nothing relayed and nothing left in its queue), a GreenWall with no is_connected (still relayed: the feature-detect fallback), a line one character over GreenWall's limit (relayed, and the player told it was cut; one at the limit is not reported), and the in-combat load arming on regen; the expiry, cap, error, gone, disconnected and an Officer-line example also assert the player-facing notice, and the error example asserts it does not claim the line was not passed. newWorld exposes the relay frame (frames[beforeCompat + 1]) and w.press().

  • A guild line sent while GreenWall was loaded but not yet set up was dropped from the relay without a word. bridgeThroughGreenWall returned false silently whenever greenWallChannel(key) or relayFrame was nil. GreenWall's gw global exists from its Globals.lua:36, but gw.config is only built in its ADDON_LOADED handler (GreenWall.lua:294), so a Gratz or Smack line in that window was never held and never reported -- the one silent path left after every other relay failure was made visible. Now silence is kept only for GreenWall not installed (_G.gw nil); otherwise the line goes through reportRelayProblem and nothing is held. One reason per condition: no relay frame (no CreateFrame at load) is "the key-press relay could not be set up", which is TOGTools' failure and not GreenWall's; "GreenWall was not ready yet" only when gw.config is missing; a config lacking the channel or GW_MTYPE_CHAT is a GreenWall whose shape changed, reported as "GreenWall is not available", since waiting would not fix it and "not ready" would repeat on every line. Per the peer-review ruling on thread c54b54b4: fix it, do not measure the window. Location: Compat.lua. Specs: Tests/gratz_spec.lua "tells the player when GreenWall is loaded but not set up yet, and holds nothing", "does not call a set-up GreenWall with a changed shape 'not ready'" and "tells the player when there is no relay frame, without blaming GreenWall" (newWorld({ noRelayFrame = true }) hides CreateFrame while Compat.lua loads).

  • The relay limits were restated in two test names and nothing tied the CurseForge page to them. Two specs were named "older than two minutes" and "at most five lines" -- a name is compared to nothing, so it would drift from RELAY_MAX_AGE / RELAY_MAX_HELD silently (peer review, thread 5ca12a4d). Compat.lua now publishes both as addon.Chat.RELAY_MAX_AGE and addon.Chat.RELAY_MAX_HELD; the specs are named after the constants and derive their time advance, line count and expected "within N minutes" text from them. A new example, "the CurseForge page states the relay limits the code enforces", reads docs/Curseforge_Description.html and asserts its "after two minutes" / "at most five" wording matches the constants, so changing either side fails the suite. This entry restates the values on purpose: a dated record must not move with the code. Also added "still relays a line held exactly RELAY_MAX_AGE", pinning that the age check is strict (>). Coverage of Compat.lua (harness coverage.lua): every relay line is covered; the 47 uncovered lines (67-83, 601-681) are AceGUI widget helpers, which need the client. README.md (which ships) also said "two minutes" and nothing pinned it (peer review, thread 6d00c25e). It keeps the figures, because a player reading it from the AddOns folder may not have the page open, and the page example (renamed "the CurseForge page and README state the relay limits the code enforces") now asserts the same wording in both files (peer review, thread e3c168f4). README still points at the page's "Sister guilds hear it too" bullet for the notices, and the example asserts that bullet exists. The example also accepts "one minute" for a 60 s limit. The no-relay-frame notice is defensive only -- Ace3's AceEvent-3.0.lua calls CreateFrame at file scope whenever Ace3 runs at all, so a client without it errors in Ace3 first -- which is now commented at the guard (citing the Ace3 line by its text, not a line number) and is why the page does not list it.

Documentation

  • The v0.8.8 entry below says Smack "stays omitted" from TOGTools_Camelot.toc. That was never true: the same entry's Smack fix lists Smack in both _Mainline and _Camelot, and the shipped Camelot TOC loads it. Smack runs on WoW Forever. Left in place because released entries are not edited; corrected here.

[v0.8.8] (2026-09-20) - Gratz and Smack guild lines now cross GreenWall to the sister guilds; WoW Forever support

New Features

  • WoW Forever ("Camelot") support: a seventh TOC, and a flavour derivation that knows the retail client can report a Classic-band number. From FastGuildInvite's onboarding notes, inbox thread 7480082e. Forever (Battle.net product wow_classic_beta) is the RETAIL client -- WOW_PROJECT_ID == WOW_PROJECT_MAINLINE, retail C_* namespaces, Menu API, secret values, the live tree -- with vanilla content at ## Interface: 16001. The client reads <Addon>_Camelot.toc before _Mainline, so TOGTools_Camelot.toc is TOGTools_Mainline.toc with the Interface line and Title changed (Smack stays omitted: same client, same 12.0+ block).

    The TOC alone would have been WRONG for this addon, and this is the part FGI's note could not know: Version.lua derives every flavour flag from select(4, GetBuildInfo()), and 16001 sits inside the Vanilla band -- so on Forever isVanilla would be true and isRetail false, routing every API branch down the Classic path on a client that has none of those APIs. addon.DeriveGameVersion(tocVersion, isMainlineClient) now takes the project-id test as a second argument (passed in, so the function stays pure): a mainline client under 100000 is isForever = true, isRetail = true, isRetail120Plus = true, every Classic flag false. The same number from a Classic client stays Vanilla, and a real retail number never becomes Forever. isRetail120Plus is true there because Forever runs the 12.x codebase; not verified in a Forever client (none on this box, and F:\Blizzard API Docs has no Forever tree) -- the cost of being wrong that way is a skipped automated /say, where the other way is a secret-value error. WOW_PROJECT_ID / WOW_PROJECT_MAINLINE are declared in .luarc.json and .luacheckrc, verified present on every Classic tree (Blizzard_FrameXMLBase/<Flavour>/Constants.lua).

    !!TOGT ships its own _Camelot TOC in the same pass (its v0.2.3). TOGTools hard-depends on it, and without one the client fell back to !!TOGT.toc (no 16001), so TOGTools could not load on Forever at all. The file was first sent to !!TOGT's inbox (thread 0a9abbb39cb5) because writ refused a write into that repository from this session; the user then spanned the two workspaces as one project (writ workspace span, 2026-09-20 -- their words to Writ: "TOGTools and !!TOGT is basically one addon but it HAS to be two for how wow works") and the TOC, changelog and player notes were written there from this seat. Both must release together.

    Specs: Tests/version_spec.lua -- new "WoW Forever" block of 6 (mainline + 16001 is Retail and not Vanilla; takes the 12.0+ gates; the same number from a Classic client stays Vanilla; a real retail number never becomes Forever; never set on any Classic client; derived at load from WOW_PROJECT_ID), and loadAt now sets the project id explicitly on every load rather than inheriting it. Tests/toc_spec.lua -- the seventh manifest in TOCS, Forever's Smack omission, the VersionCheck rule extended to it, and a {16001, 16002} band pinning the one number the client advertises. Full suite: 1013 passed, 0 failed.

Bug Fixes

  • A Gratz (or Smack) sent to Guild or Officer stayed in the home guild; the sister guilds on the GreenWall confederation never saw it. Reported by users on 2026-09-20. Root cause, read in GreenWall's source rather than assumed: GreenWall's entire outbound relay is a hook on the chat edit box's ParseText (GreenWall.lua:223-270, GreenWall_ParseText), which pushes the typed line onto the bridge channel with gw.config.channel.guild:send(GW_MTYPE_CHAT, msg). CHAT_MSG_GUILD is received but only logged (GreenWall.lua:326-330, "Messages will be forwarded by the ChatEdit_ParseText hook"). An addon's SendChatMessage never goes through the edit box, so nothing we said was ever relayed. GreenWallAPI.SendMessage was not the fix either -- it sends GW_MTYPE_EXTERNAL, addon data dispatched to registered handlers, not chat.

    Fix, in Compat.lua: addon.Chat.Send -- the one choke point Gratz and Smack both use -- now mirrors the typed-line call after the real send, for exactly the two types GreenWall bridges (GUILD -> gw.config.channel.guild, OFFICER -> gw.config.channel.officer, GW_MTYPE_CHAT). GreenWall's receive side already drops the sender's own guild (Channel.lua:408), so the home guild hears the real send once and each co-guild hears the relay once. Feature-detected on GreenWall's own globals through _G (they are another addon's, not WoW API, so they stay off the linter globals list): with GreenWall absent it is a table lookup and nothing else; with GreenWall loaded but unconfigured it is a logged no-op (guild_id defaults to '', tl_flush refuses while not connected). The call is pcall'd and the failure written to addon:Debug, because the real guild send has already gone out by then and a GreenWall-internal change must not take down the event handler that just spoke. Location: Compat.lua (bridgeThroughGreenWall, addon.Chat.Send).

    Specs: Tests/gratz_spec.lua, new "the GreenWall bridge" block, 7 examples: GUILD goes to the guild channel as chat (not EXTERNAL) after the real send; OFFICER to the officer channel; party / raid / say are never bridged; GreenWall absent sends normally; no send function means no relay; a GreenWall-internal raise leaves the send and the fire count standing and is logged; a combat-queued line is bridged at the flush, not at queue time. Full suite: 999 passed, 0 failed.

    Not verified in a client yet. The call shape is the one GreenWall's own hook makes, checked against the fork's source on this box; the first gratz across a live confederation is the test.

  • A plain item (no suffix, enchant or gems) in the Guild Bank log was signed by its WHOLE LINK, not its id, so the same movement could be stored twice. TOGBankClassic's contract, inbox thread 55e84c617342 (2026-09-17). linkSig in Modules/GuildBankLog/GuildBankLog.lua matched |Hitem:(%d+): / item:(%d+): -- both REQUIRE a colon after the id -- and a plain item's link has none (|cffffffff|Hitem:858|h[Minor Healing Potion]|h|r; LibItemDB emits item:%d when every optional field is empty). Both patterns missed and the fallback returned the link itself. So one movement carried two itemSigs depending on whose item cache answered the link lookup -- the id when the viewer's cache gave a coloned link, the link string when LibItemDB's did -- and two dedupe keys for one row. Fix: |Hitem:(%d+) / ^item:(%d+), so |Hitem:858|h, |Hitem:858:..., item:858 and item:858::::::863 all sign 858. A native Blizzard bank link always carries colons, so native rows' sigs and ids do not move.

    Stored rows re-key once. A row the old fallback signed has itemSig == itemLink exactly -- that equality is the tell, and nothing else produces it (an id or a caller-supplied sig never equals the link). New one-shot resignItemSigs(bucket, guildKey), marker _sigsRekeyed, next to its two siblings: re-signs such rows under the fixed linkSig, regenerates their id (the sig is part of rowId), then collapses any pair that now shares a dedupe key in original array order, backfilling name from the dropped copy. It runs from BOTH write paths -- after migrateOccOrdinals in ingestTab, and at the top of InjectEntry -- because on Classic Era nothing ever ingests and every row arrived through injection. The id change is invisible to TOGBankClassic: InjectEntry never returns it and nothing reads it back yet.

    Specs: Tests/guildbanklog_spec.lua -- two linkSig examples (colon-less link and bare item string, each alongside its coloned twin, all signing 858) and a new "colon-less sig repair" block of 5: a stored whole-link row is re-signed and re-id'd through InjectEntry; the same movement re-delivered under a coloned link now answers duplicate; a movement already stored under both sigs collapses to the named copy; an id-signed row and a caller-supplied sig are untouched; the marker is honoured. Full suite: 1006 passed, 0 failed.

  • Smack did not load on Retail, while the README and CurseForge page had said since v0.8.5 that it works there. Both are mine. v0.8.2 omitted Modules\Smack\Smack.lua and GUI\SmackTab.lua from TOGTools_Mainline.toc ("Retail 12.0+ blocks automated public chat") and pinned that in Tests/toc_spec.lua's INTENTIONAL_OMISSIONS. v0.8.5 then read Smack.lua:134's isRetail120Plus gate, concluded the tool works on Retail 11.x and Midnight, and corrected both player documents to say so -- without ever opening the manifest. So for a month Retail players were told about a tab that was not in their build. The code was the right half: only Say/Yell/Emote are blocked on 12.0+, Party/Raid/Guild and the hotkey work, the secret-value guards are in place, and Tests/smack_spec.lua drives every flavour. Fix: Smack is listed in TOGTools_Mainline.toc and TOGTools_Camelot.toc (between Gratz and ItemDB, matching the Vanilla order the parity spec checks), and INTENTIONAL_OMISSIONS is empty -- with the story left in its comment so the next omission is added together with a check that the player docs agree. Found by the docs-accuracy pass this release, not by a report. Full suite: 1013 passed, 0 failed.

  • .pkgmeta ignored "!TOGT" -- one bang, the companion's pre-rename folder name -- so the defensive entry named a folder that no longer exists. Raised in !!TOGT's peer-review board on 2026-08-14 ("theirs would not fire in the one situation it exists for"), read while retiring that board for !!TOGT's v0.2.3. Now "!!TOGT". Verified with wow-version-replication.ps1 -DryRun: the seven globs load and the WOULD list is exactly the release set. Location: .pkgmeta.

Improvements

  • The Mailbox tab's sister-guild tooltip now says BOTH officers must list each other. GuildRoster 0.8.0 made the sister sync mutual (inbox thread 586d309279d2, from the user's 2026-09-15 directive: "we don't want one guild stealing info from another one without permission"): a guild that lists yours without your officer listing them back is refused. Mailbox:DescribeSisterStatus named one officer in both setup states, which sends a player to do half the job. "No sister guild is configured" now says an officer of each guild lists the other and that a one-sided listing gets nothing; "configured, no roster pulled yet" says their officer must list your guild back, that their side refuses until then (/gr's status line says so), and that the next automatic round brings the roster once both sides list each other. The old "one sighting on the wire" bootstrap sentence is gone with the bootstrap it described. Location: Modules/Mailbox/Mailbox.lua. Specs: Tests/mailbox_spec.lua -- a new example asserts "each" / "mutual" / "list your guild back" across both states, and the bootstrap example now asserts "next automatic round" and the absence of "one sighting". Full suite: 1007 passed, 0 failed.

Documentation

  • README.md and the CurseForge description brought up to the addon's actual state. Forever added to every supported-versions list, with the one-line explanation that it gets the Retail version of each tool; the GreenWall bridge described under Gratz, Smack and (as an optional dependency) Requirements; the sister-guild mailbox text now says the link is mutual; I'd Hit That and /togt vc say "Retail or Forever"; Smack's Midnight paragraph names Forever alongside it. The slash-command table was checked against SlashCommands.lua and matches (the undocumented aliases -- /togt db, /togt versioncheck, /togt logs bank|gbank|diag -- stay undocumented on purpose; db and diag are developer-gated). The v0.8.8 CurseForge entry carries the Smack packaging fix under Fixed, because Retail players were told about the tab by name.

[v0.8.7] (2026-09-15) - The sister-guild mail autocomplete moves into the Guild Roster library

Improvements

  • The Send Mail To: box's sister-guild suggestions are now LibGuildRoster's, not TOGTools'. The user's direction, given in the GuildRoster session on 2026-09-15 and delivered here as inbox thread 427ea01a1362: "i added a feature to TOGTools i want to rip out and put in here. The mailbox autocomplete for sister guilds. it shouldn't live in an addon, it belongs in this library." GuildRoster 0.8.0 / MINOR 19 now owns the hook (InstallMailAutocomplete, at PLAYER_LOGIN and re-checked on every MAIL_SHOW), the candidate read (GetSisterRecipients), the matching rule (MergeRecipients, verbatim) and the ONE account-wide switch (IsMailAutocompleteEnabled / SetMailAutocomplete, in /gr and /guildroster mail on|off). Until this rip-out both wrappers were chained on the same box -- clean output, because both dedupe, but one redundant merge per keystroke and two switches for one hook, which is the "one concept, two spellings" finding by definition.

    Removed from Modules/Mailbox/Mailbox.lua: GetSisterRecipients, MergeRecipients, InstallAutocomplete, the sisterAutocompleteOn / otherPriority / _installedSource locals and both InstallAutocomplete() calls in the event handler. The stale-mail watcher is untouched. Removed from TOGTools.lua: the db.global.mailbox.sisterAutocomplete default; a copy already in a player's SavedVariables is cleared once at OnInitialize, because a key with no default is one AceDB keeps forever. Kept: GetSisterStatus (now #lib:GetSisterRecipients(), feature-detected) and DescribeSisterStatus, since the tab's tooltip still has to explain an empty list; the "too old" sentence now names 0.8.0. New, and thin: Mailbox:IsSisterAutocompleteEnabled() (nil when the library has no switch) and Mailbox:SetSisterAutocomplete(bool), which forward to the library. The tab's checkbox is a second handle on the library's switch, not a second switch -- GUI/MailboxTab.lua reads and writes through those two, and on a library older than MINOR 19 draws a "needs Guild Roster 0.8.0 or later" note in the checkbox's place rather than an OFF box that does nothing. The help line and tooltip say where the switch lives.

    Specs: the 20 MergeRecipients / GetSisterRecipients / InstallAutocomplete examples left with the code -- they run in GuildRoster's Tests/mailauto_spec.lua against the library's copy. Tests/mailbox_spec.lua now pins the other direction: PLAYER_LOGIN and MAIL_SHOW leave SendMailNameEditBox.autoCompleteSource exactly as they found it and the three removed methods are nil (so a second wrapper cannot quietly come back); GetSisterStatus counts the library's recipients and reports 0 on a library that predates the autocomplete even when it holds rosters; the checkbox handle reads and writes the library's switch and never a db copy, and answers nil / refuses the write with no switch to reach; the defaults table carries no sisterAutocomplete and OnInitialize clears a saved one. Suite: 992 passed, 0 failed. Verified in game by the user the same day. Location: Modules/Mailbox/Mailbox.lua, GUI/MailboxTab.lua, TOGTools.lua, Tests/mailbox_spec.lua.

Process

  • docs/AUDIT.md and docs/DEPENDENCY_CONTRACTS.md are deleted. Both were boards that writ's inbox replaced on 2026-09-10; the user's direction on 2026-09-15: "you shouldn't be doing anything in audit.md. once it was migrated to the inbox, you could delete it" and "you can remove dependency_contracts.md as well, as long as you migrated it to the inbox". The audit board's open items had already gone to the inbox and the todo list on 2026-09-11; its last open item, round 5 finding H (GuildBankLog occ as a position in a mutating buffer), was falsified in game today -- the user's three-deposit test showed the third identical stack recorded, so the ordinal is stable and no dedupe-key change is needed. The contracts board's three sections were all closed (TOGBankClassic pushes via InjectEntry, delivered; the level-change callback, absorbed into Gratz; sister-guild ownership and now the mail autocomplete, delivered by GuildRoster) and their records are filed as inbox threads 42e8d2c03719 (TOGBankClassic) and 5e886a380126 (GuildRoster). Both files remain in git history. CLAUDE.md now points at the inbox instead; the code comments and specs that cited the boards by path (GuildBankLog.lua, Mailbox.lua, guildbanklog_spec.lua, logregistry_spec.lua) cite the inbox thread or say the finding is settled; the watcher spec no longer lists docs/AUDIT.md.

[v0.8.6] (2026-09-14) - Sister-guild names in the mail To: box, TOGBankClassic rows and requests in the Guild Bank log, Smack and Gratz stop "sending" a Say/Yell the game drops outdoors, Smack sees environmental damage, and a % in a name can no longer crash a trigger

Bug Fixes

  • A Smack auto-trigger set to Say or Yell said nothing outdoors on Classic, while its Count went up. Reported in game 2026-09-13 with a Target-HP filter on Say. Smack.lua's header stated the public-chat rule BACKWARDS -- "Say/Yell/Emote are silently dropped during an instance boss encounter; open-world combat works normally" -- and sendToChannels only skipped automated public chat on Retail 12.0+. The actual rule, per the SendChatMessage wiki page and Blizzard's forum answers, in force since 8.2.5 and ported to Classic in 1.13.3: an addon's SAY or YELL (and public CHANNEL) needs a hardware event while outdoors and is allowed inside an instance -- dungeon, raid, battleground, arena. Only the outdoor half is measured (by the user, on Era); the inside-an-instance half is the wiki's, and the harness model built from this report says the same thing because it reads the same page -- two copies of one source, not two measurements. An SFK run with the filter is the measurement, and until then a wrong inside-half means Smack allows, inside instances only, a send the client drops. The drop is silent, so the send "succeeded", the filter was counted as fired and its minute-cooldown burned on a line nobody saw. Now Smack.PublicChannelBlocked(key, isHardware) answers per flavour -- Retail 12.0+ blocks Say/Yell/Emote everywhere; everything else blocks automated Say/Yell only when IsInInstance() is false; Emote is not in the rule and stays sendable; a hotkey is never blocked -- and sendToChannels skips a blocked channel so the fire is neither counted nor cooled down, and the once-per-filter chat notice says why in the flavour's own terms. The Smack tab's help, the Say/Yell tooltips, the hotkey tooltip and the Manual-trigger blurb all said "works fully on Classic" or "even /say mid-boss"; they now say the opposite, which is the truth. Specs: the world stubs IsInInstance (outdoors by default, deliberately), the filter() default channel moved from Say to Guild because eight auto-fire specs had been quietly ratifying a send the client never delivered, and a new block pins skip-outdoors / not-counted / cooldown-not-burned / allowed-inside / battleground-is-inside / emote-unaffected / hotkey-anywhere / Retail-everywhere. Location: Modules/Smack/Smack.lua, GUI/SmackTab.lua, Tests/smack_spec.lua.

    The Smack suite is now multi-flavour, driven from the shipped TOCs, because the user's verdict on the above was right: this was the suite's to catch, and it could not have -- it hand-built two flag tables ({ isVanilla = true } and a RETAIL120), which tests a branch and never the code that picks it (Version.lua's own header says so), and it had no notion of indoors versus outdoors at all. newWorld now takes a TOC interface number, feeds it through a GetBuildInfo stub into the real Version.lua, and the FLAVOURS list is READ from the six TOC files at spec load (## Interface: -- Mainline's two numbers become two rows), so a bumped or added TOC changes the matrix with no edit here. forEachFlavour runs an example once per row, named by flavour and number, so a failure says which client. Sixty-three rows pin trigger availability (the health-reading and combat-log triggers hidden on 12.0+ and nowhere else) and the public-chat rule on every flavour: outdoor drop uncounted, inside allowed below 12.0 and not on 12.0+, emote unaffected below 12.0, hotkey anywhere, guild/party/raid never restricted, and the notice worded for the flavour. What the Blizzard docs say is recorded at the top of the spec, per tree, with line numbers (F:\Blizzard API Docs, Era 1.15.9 / Anniversary 2.5.6 / Classic 5.5.4 / Live 12.0.7): SendChatMessage is HasRestrictions = true in all four and says nothing about WHICH restriction; IsInInstance exists in all four with the same returns; UnitHealth is SecretReturns = true in live only. Wrath and Cataclysm have no docs tree and are covered by the band derivation alone; the spec says so.

  • Smack called the bare SendChatMessage global, which on every Classic tree is only a deprecation fallback. Blizzard_DeprecatedChatInfo/Deprecated_ChatInfo.lua:8 defines it behind GetCVarBool("loadDeprecationFallbacks") and forwards to C_ChatInfo.SendChatMessage; a client with that CVar off has no global at all. Found while reading the docs for the fix above. sendChat now resolves C_ChatInfo.SendChatMessage first and the global second, at call time; the spec records on both spellings tagged by name and asserts the namespaced one is reached on every flavour, with one example for the fallback. Location: Modules/Smack/Smack.lua.

    A harness request went to WoWAPITesting (inbox thread 4858266e90e3): its env/protected.lua models "any restricted call outside a hardware event is blocked", which would flag the GUILD sends the client allows, so it could not be turned on for Smack and could not have caught this. Asked for a channel-aware model -- SAY/YELL/CHANNEL dropped outdoors, delivered inside, the rest always delivered, 12.0+ blocking SAY/YELL/EMOTE -- with separate said / dropped / blocked recorders. Delivered the same night as harness e1a7055 and adopted here (pin bc75950 -> e1a7055; see Process below).

  • Gratz had the same Say/Yell defect, and the same deprecated call, one module over. Found on 2026-09-14 by the peer review asking why Gratz's send had sat in "Not covered" twice. Every Gratz fire is event-driven -- an achievement, a level-up, a guild event -- so none carries a hardware event, and a profile with Say or Yell ticked sent outdoors anyway: dropped silently by the client, counted on the tab, cooldown armed. On 12.0+ it was an ADDON_ACTION_BLOCKED. And queuedOrSend / flushCombatQueue called the bare SendChatMessage global, the deprecation fallback. The rule now lives once, in Compat.lua: addon.Chat.PublicChannelBlocked(chatType, isHardware) (outdoors drops SAY/YELL below 12.0, 12.0+ blocks SAY/YELL/EMOTE everywhere, a hardware send is never blocked), addon.Chat.PublicChannelBlockedReason() (one sentence per flavour so the two modules never explain it differently) and addon.Chat.Send (C_ChatInfo.SendChatMessage first, global fallback, returns whether a send function existed). Smack's own copy of the rule -- it had lived there alone for a day -- became a key-to-chat-type wrapper; Gratz's sendToChannels skips a blocked public channel and reports it, tryFire tells the user once per profile (no hotkey to offer, so it says to use Party / Raid / Guild) and counts nothing that reached nobody. The Gratz tab's Say/Yell tooltips said "Always works, no group requirement"; they now say where it works. Specs: Tests/gratz_spec.lua runs under the harness's chat gate like Smack's, three channel-dispatch examples that had been asserting an outdoor /say moved inside an instance where the send is real, and a new block pins skip / not-attempted / not-counted / cooldown-unarmed outdoors, the once-per-profile notice, the non-public channels still going out beside a skipped Say, the 12.0+ block inside an instance, the namespaced-first send with the global fallback, and no-send-function-at-all. Locations: Compat.lua, Modules/Gratz/Gratz.lua, Modules/Smack/Smack.lua, GUI/GratzTab.lua.

    The combat queue asks the rule again at the flush (peer review of the above). A gratz queued mid-fight inside an instance whose fight ends outdoors would have been handed to the client and dropped silently -- and it was already counted at queue time, so un-counting is not available. It is now held instead, with the same once-per-profile notice marked "held until combat ended"; the count stands, the client is never given a line it will lose. The notice moved into one notifyPublicBlocked used by both paths, and a nil profile name can no longer raise inside it (or "?"; same guard in Smack). Two specs: queue inside, flush outside -> nothing sent, nothing dropped, one notice, count untouched; and the unchanged-answer flush still delivers. A source-reading spec also pins that both tabs' Say/Yell tooltips carry the two phrases the rule's wording turns on, so a change to PublicChannelBlockedReason that drops either is caught offline rather than by a player.

  • Peer review of the above, three of its findings fixed. (1) Smack.sendChat returned nothing, so on a client with neither C_ChatInfo.SendChatMessage nor the bare global the fire was still counted and its cooldown burned -- the same shape as the outdoor /say defect, one guard down. It now returns whether a send function existed and sent derives from it. Not seen on any flavour; fixed because the two guards for one dependency should agree. (2) The Guild Bank log's baseKey treated an empty-string to/from as a different identity from an absent one; '' is now absent, so the dedupe rule does not depend on which spelling of "unknown" a pusher uses. TOGBankClassic never sends '' (the party comes from a normalised requester name) -- this is independence, not a repair. (3) Mailbox.InstallAutocomplete wrapped the mail box's autocomplete source once and latched; a mail addon assigning that field in its own OnLoad after us removed the wrapper for the session with nothing to reinstall it. Every MAIL_SHOW now checks the field still holds our closure and re-wraps whatever is there if not, chaining onto the newcomer rather than replacing it. Spec: "re-wraps on the next mailbox visit after another addon replaced the field". The reviewer's fourth point -- that the to/from-in-identity change could double a row TOGBankClassic pushes first without a party and later with one -- dissolved on their trace of TOGBankClassic's Log:AttributeChanges: the party is attributed at mint time and a batch is pushed once, so no single ledger can see both shapes of one row. Locations: Modules/Smack/Smack.lua, Modules/GuildBankLog/GuildBankLog.lua, Modules/Mailbox/Mailbox.lua.

  • MainWindow:Close() traces the closing call stack, with the window's shown state and mouse buttons, behind verbose debug. The user reported the main window closing on a title-bar drag, with no Lua error; a /reload cleared it and the cause was not found in the source -- every close path was read, nothing in the working tree touches the drag handlers, and the ESC proxy and UISpecialFrames are untouched by the companion. Rather than guess at a fix, every way the window can go away (X, ESC proxy, slash and minimap toggles, Rebuild, any outside Hide()) funnels through Close(), so debugstack there names the caller if it recurs; per the reviewer, IsShown() and the left/right button state ride along, because a drag ending over the close region is the usual cause of exactly this report. On 2026-09-14 the user reported the window dragging normally on this tree. Nothing in this release was aimed at it, so the cause is still unidentified: it was session state a /reload cleared, and the trace stays in so a recurrence names its caller. Location: GUI/MainWindow.lua.

  • The [TOGB] tag never rendered, because the Guild Bank log's read copy dropped the field it reads. GuildBankLog:GetEntries hands the sub-tab a field-by-field copy of each stored row (deliberately -- audit finding tog-N, a read must not expose the SavedVariables table), and source was not in the list. InjectEntry stored it, fmtTab tested for it, and nothing between the two ever carried it, with no error anywhere: the row simply rendered as native. The same omission would have swallowed every field LOGAPI-004/005 add below. The copy now carries source, to, from, note and requestId, the comment on it says it is the whole read contract, and Tests/guildbanklog_spec.lua asserts each field survives the copy and stays nil on a native row. The v0.8.6 entry below that said the tag renders was written against the renderer and never checked against the read path. Location: Modules/GuildBankLog/GuildBankLog.lua.

  • Smack's big-hit trigger could not see environmental damage, and the obvious fix would have read the wrong field (audit finding tog-AW). handleCombatLog read amount from two payload shapes only -- SWING_DAMAGE at info[12] and the spell-standard events at info[15] -- so ENVIRONMENTAL_DAMAGE (lava, fire, slime, falling, drowning) left amount nil and skipped the branch in silence, on the flavour where the biggest single hits a player takes are usually environmental. Its payload is environmentalType, amount, ..., so the amount is info[13], and it now has its own arm; folding it into either existing branch would read the type string or resisted. DAMAGE_SHIELD and DAMAGE_SPLIT share the spell-standard prefix and are added to that arm. Offsets verified in Blizzard_CombatLogProcessor.lua on both the Era and Retail trees. The trigger stays combat-scoped, and that is now a recorded decision rather than a side effect of the arming gate: a fall outside a fight still says nothing, and the spec pins it so a future change is a design change. Tests/smack_spec.lua gained a w.cleu() driver that delivers the real eleven-field-prefix payload shape, and seven specs, one per offset, each carrying a 0 where a wrong-offset read would land. Location: Modules/Smack/Smack.lua.

  • A % in a player, guild, target or achievement name raised inside the Gratz and Smack event handlers (audit rounds 34-36). Both expandMessage and expandLine chained one gsub per placeholder with the game-supplied value as the REPLACEMENT string, where % is an escape (%1 is a capture reference; a lone % is "invalid use of '%' in replacement string"). Whether any name in any locale carries one is unprovable, and the defence costs less than the proof. Both now call addon.Text.Fill, a single-pass function-replacement form that never interprets the value -- which also closes the chain's other hole: a value that itself contained [guild] or [hp] was substituted again by the next pass. Unknown placeholders are still left as typed. Location: Compat.lua, Modules/Gratz/Gratz.lua, Modules/Smack/Smack.lua.

New Features

  • The Send Mail To: box autocompletes sister-guild members. Requested by a guild member, 2026-09-13. Blizzard's mail autocomplete list is ALL_CHARS -- own guild, friends, people interacted with, own characters (Blizzard_AutoComplete/AutoComplete.lua:19-22, :106) -- so a sister-guild name had to be typed in full. Modules/Mailbox/Mailbox.lua now reads the sister rosters LibGuildRoster-1.0 holds (its MINOR 6 cross-guild API: GetKnownRosters minus GetHomeGuildKey, GetRoster(key)) and appends prefix matches to Blizzard's results. The names are only as complete as TOGProfessionMaster's roster sync on that client -- TOGTools feeds nothing; it reads what TOGPM left in the one LibStub instance both share, so without TOGPM (or without a sister guild configured there) the feature contributes nothing and says so in the checkbox tooltip. Home-guild members are deliberately not added; Blizzard already lists them. The hook is one field, not a secure function: the box's OnLoad (Classic Era MailFrame.xml:584) stores C_AutoComplete.GetAutoCompleteResults in SendMailNameEditBox.autoCompleteSource and AutoComplete_Update calls it as source(text, max, cursor, allowFullMatch, include, exclude); the module replaces the field with a wrapper that calls the original and merges, installed once on PLAYER_LOGIN (retried on MAIL_SHOW for a client that builds the frame on demand). max is respected as a hard cap -- Blizzard asks for one more than it shows and reads "more than shown" as continued..., so appending past it would mark every list continued. The matching rule is a case-insensitive PREFIX on the character name, or on the whole Name-Realm once the typed text carries a dash; a name Blizzard already returned is not added twice. Appended entries carry Enum.AutoCompletePriority.Other (fallback LE_AUTOCOMPLETE_PRIORITY_OTHER), and if a client has neither the wrapper contributes nothing -- AutoComplete_UpdateResults indexes AUTOCOMPLETE_COLOR_KEYS by that value and an invented one would raise in Blizzard's code on the first keystroke. The setting (db.global.mailbox.sisterAutocomplete, default on, checkbox on the Mailbox tab) is read per call, so switching it off needs no uninstall.

    Where the names come from changed the same day, on the user's direction. The first cut read whatever TOGProfessionMaster's sync had left in the library, and the user's test on the 981197530#1 account found nothing -- that account's TOGPM has no sister list and no persisted roster (checked in its SavedVariables), so the store was empty by construction. The user's words: "i need that and i need it to not step on each other, because i need to build that into togbank too" and "probably best to do it in the library". Three consumers of one store means one owner of the list, the wire and the login re-feed, so the request went to GuildRoster (docs/DEPENDENCY_CONTRACTS.md section 3, inbox thread a95015801eb8) and shipped the same day as GuildRoster 0.7.0 / MINOR 18. One correction to my request is on record there: I asked for "/who-driven" discovery, which cannot exist (SendWho needs a hardware event), and the user ruled it out; the library uses proven wire sightings and a one-sighting bootstrap. TOGTools adopted the new read: Mailbox:GetSisterStatus() (GetSisterGuildNames + IsSisterSyncAvailable, feature-detected) and DescribeSisterStatus, so the checkbox tooltip names what is missing in each empty state -- old library / nothing configured / no DeltaSync / configured-but-never-pulled with the bootstrap rule -- instead of one generic "no names". A bare name count could not tell those apart. Twenty-six specs in Tests/mailbox_spec.lua: the matching rule pure, the roster read and the status against a stub registered in the real LibStub (evicted after each, since wow.reset() restores the registry object but not its contents), the hook -- argument pass-through, install-once, the MAIL_SHOW retry, both off-switches, the no-priority guard, a nil answer from Blizzard -- and every tooltip state. Verified in game 2026-09-13 on the 981197530#1 account: list set on Togcook (SetSisterGuildNames), roster relayed from Picky's TOGPM re-feed (held=1 in the library's health check), and Shlonk in the To: box offered Shlonky-Azuresong in the plain colour under the green guild entries (it renders as Blizzard's default gold, which the user asked to keep: "it's a yellowish color, do change it, it's good"). User's words: "validated it's working". The name first tried, Chingdaddy, is in neither roster: TOGPM's crafter tag for it is The Old Gods (guildRegistry 8e5c20), Blizzard's own guild list does not offer it, and The Other Gods' 27-member pull does not contain it. Location: Modules/Mailbox/Mailbox.lua, GUI/MailboxTab.lua, TOGTools.lua (default).

  • Guild Bank log rows pushed in by TOGBankClassic are tagged [TOGB]. TOGBankClassic now delivers its bank-character movements through GuildBankLog:InjectEntry (its LOGAPI-002, same day) with source = "TOGB" on every row, per the user's direction that the log carry native AND addon-bank rows and that the addon ones be told apart at a glance -- "maybe something like [TOGB]". GUI/GuildBankLogSubTab.lua's fmtTab renders that field as a light-blue [TOGB] in the Tab column, which is meaningless for those rows anyway (no tabs) and is already filterable on the rendered label, so the Tab header filter now offers [TOGB] as a value: show only addon-bank rows, or hide them. Native rows carry no source and are unchanged. TOGBankClassic passes occ itself (its version's publish time, identical on every client) so a viewer alt re-injecting the banker's row returns duplicate rather than landing as occ+1 -- the correction that came out of reading InjectEntry:1186-1193 against their first delivery. The renderer is not specced (AceGUI file); the read path that feeds it is, as of the fix above.

  • Guild Bank log rows from TOGBankClassic name the other party (their LOGAPI-004, 2026-09-12). The operator, on a mailed fill: "the mail item didn't end up there either" -- it had, as the Withdraw row, with nobody named on it. An item row may now carry to (the requester a withdrawal was mailed or handed to) or from (the sender a deposit arrived from), and the Player column renders them as Toglowweap > Graymoon on a withdrawal and Restaurants > Toglowgear on a mailed-in deposit (ASCII >, for the same missing-glyph reason the Tab column uses it). They are part of the row's identity when present: the same stack withdrawn to two requesters in one TOGBankClassic version is two rows on their side, distinct by nothing but to, and with occ being that version's publish time the second came back duplicate. baseKey appends the party ONLY when set, so a native row's key -- and therefore its stored cross-viewer id -- is byte-identical to before; a spec pins the exact id string of a party-less row so a refactor that appends an empty segment to every key goes red. Location: Modules/GuildBankLog/GuildBankLog.lua, GUI/GuildBankLogSubTab.lua.

  • TOGBankClassic's request events are rows in the Guild Bank log (their LOGAPI-005). The operator, reading the tab: "the requests aren't showing up in the togtools logs". They were never pushed, and TOGBankClassic will not push them until this log says it can take them -- so GuildBankLog.ACCEPTS_REQUEST_ROWS = true is the switch, set only now that every stage handles kind = "request". A request row is a lifecycle EVENT, not a stock movement: type is one of requested / mailed / handed / cancelled / reopened, name the actor, to the other party, count the units this event covers (absent on cancelled and reopened), note the cancel reason, requestId, an absolute ts, and a STRING occ of "<ts>:<requestId>". Its identity is therefore the event -- request|type|requestId|ts|count -- never the item tuple, so a requested and a mailed of the same item, or two partial fills of one request, stay apart while a replay of the same event dedupes. The string occ found a latent crash: InjectEntry's next-ordinal scan compared e.occ > maxOcc on every stored row of the incoming tuple, and Lua 5.1 raises on string > number; it now counts numeric ordinals only, and a spec re-injects the same event with its occ stripped -- the only shape that reaches the comparison -- and asserts it is numbered 1. Rendering: the Type column reads Requested / Mailed / Handed / Cancelled / Reopened (red for Cancelled, the movement palette otherwise) and, being built from the rendered labels, the Type header filter offers them without a second control; Player is Restaurants > Toglowgear; Item is the link with the cancel reason in grey after it; Qty is blank rather than 0 when the event carries no units; Tab is the [TOGB] tag. Retention prunes them by ts like every other row (specced). A mailed row and the withdraw row for the same fill are the same physical movement seen from two sides, and BOTH are kept -- the request row is the lifecycle, the withdraw is the stock, and the Type filter hides either. That is a recorded decision, pinned by a spec, and reversible on TOGBankClassic's side by not pushing mailed. Thirteen specs in Tests/guildbanklog_spec.lua; the dependency contract in docs/DEPENDENCY_CONTRACTS.md section 1 carries the response. Location: Modules/GuildBankLog/GuildBankLog.lua, GUI/GuildBankLogSubTab.lua.

Improvements

  • Profiles and filters iterate in KEY order, through one helper, instead of four copies of a field sort (audit finding tog-AX). The tog-AS/tog-AV fix sorted matched by .name while the comment justified the order being total "because db.profiles is keyed by name" -- the reason cited the key and the comparator read the field, and the comparator was then byte-identical in Gratz, Smack, GetProfiles and GetFilters. addon.Table.SortedByKey(map, pred) in Compat.lua sorts by the key, which is unique by construction, and all four sites use it. Two specs give every entry the same .name and assert the order still holds -- a field sort has six ties there and no defined order.
  • A Name Prefix dedupe chain is now superseded by a new toggle, not raced by it (audit finding rev-L). _dedupPolling was tested only by the PLAYER_GUILD_UPDATE watcher; the checkbox's own caller had no guard, so ticking off-then-on inside the one-second poll window scheduled a second chain while the first's timer was outstanding -- that timer then cleared the flag under the live chain and both drew on one twenty-poll budget. ApplyDedupSetting now carries a generation token: every non-retry entry starts a new generation, the retry closure captures its own, and a stale retry returns before touching the counter or the flag. The invariant lives in the function rather than in whichever caller remembered it. One spec drives the double-toggle through the captured timer queue and asserts one budget, then that the watcher's guard still holds afterwards. Location: Modules/NamePrefix/NamePrefix.lua.
  • Three dead helpers deleted from Compat.lua (audit finding R, open since 2026-08-16 as "needs a decision"). addon.Tooltip.AnchorFrame, addon.GUI.ApplyMinResize and addon.GUI.DetachPool were ported from FastGuildInvite / TOGProfessionMaster and never called here; the header said "used by every tab". The reviewer's caveat -- a sibling might reach them through TOGTools.addon.GUI.* -- was checked across the whole AddOns tree: every hit is that addon's own copy. The one raw-frame pool in this addon is RowList's, and RowList:Detach orphans its own rows. The header now lists what the file actually holds, each with a caller, and says a helper without one is the same finding again.
  • Audit finding tog-AU verified closed by reading the caller: fireFilter returns before _cooldownLast and count when sendToChannels reports nothing sent (Smack.lua:375-381), which is the half the reviewer had not followed.

Process

  • Onboarded to writ. The two contract boards and docs/AUDIT.md were imported into the inbox; the two open harness requests (seterrorhandler, GetServerTime/GetServerTimeLocal) and the open TOGBankClassic contract were sent to their owning seats. The importer does not recognise this audit board's lettered finding identifiers (FINDING A, tog-AX, rev-B) and read 3 items from ~80; reported to writ, and the open/closed triage of the lettered board is on the todo list.
  • Harness pin moved ff379c2 -> bc75950, and both open harness requests are delivered and adopted. The pin had sat at ff379c2 since 2026-08-19 despite a "pin b307a9b" response the same day; the jump is ~50 commits, and the whole suite ran green at the new pin on the first try. What the two deliveries changed here: (1) seterrorhandler is real (harness 2406895), so Tests/diagnostics_spec.lua no longer stubs the error-handler pair after load() -- the stub backed the same slot and, by pinning prev to nil, left Diagnostics.lua's chain-to-previous branch unreachable offline; a new example asserts our record lands AND the handler we chained over still runs. (2) C_DateAndTime.GetServerTimeLocal is real (harness 7e929c2), which caught a spec that had gone silently wrong the day it shipped: Tests/guildkey_spec.lua's "degrades when the realm-offset API is absent" block drove the degraded branch by relying on the env NOT having the function -- from 7e929c2 it was driving the OFFSET branch and stayed green because dd/dd and :00 hold under a whole-hour shift too. It now removes the function with wow.withoutApi and asserts the exact un-shifted string, and a new block sets wow.serverUtcOffset per example and asserts the hour moves by exactly +3h, -5h across a date boundary, and in the year form. Local GetServerTime stubs stay: they drive the clock, and the harness's GetServerTimeLocal reads that global, so both halves of the subtraction move together.
  • Harness pin moved bc75950 -> e1a7055 -> e915b0a, and the Smack and Gratz suites now run under the harness's chat gate instead of their own recorders. (The second move picks up the tail-call coverage fix above and 1cfad99, which makes a test frame's Fire("OnClick") a hardware event on its own, so the button-click example needs no wrapper.) Eight harness commits in the first range; three say "run your suite" (string.format %d now refuses a value over 2^31 like the client, the anchor solver keeps an explicit height under a fill-width child, InboxItemCanDelete means deletable-not-returnable) and the suite was green at the new pin on the first run, 988/988. The one built for this addon is protected.enforceChat(): Tests/smack_spec.lua opts in per world, drops its IsInInstance stub for wow.instanceType, sets the flavour through wow.setBuild so the harness's 12.0+ rule and Version.lua's flags read one number, and runs a hotkey press inside protected.asHardwareEvent -- the hollow test frame's Fire does not mark one, and a keypress is one. w.sent is now wow.said, what the client delivered, and the outdoor examples also assert #wow.dropped == 0: not merely undelivered, never attempted. One example per flavour sends a raw outdoor /say and asserts the harness drops it, so those zero-dropped assertions cannot pass with the gate absent. Mutation-checked: disabling Smack's outdoor arm turns 15 examples red on every pre-12 flavour, which is the shape the shipped defect had and the shape the old recorder could not see. The "namespaced call first" pin survives as a count of sends that arrived through the bare global (0 on every flavour, 1 on the one no-namespace example), since both harness spellings now record identically.
  • Coverage measured, and three modules brought to 100% from pre-existing gaps. The self-audit of this release found the suite had been run all night and coverage never; measured with Tests/wowapi/coverage.lua: GuildBankLog.lua 84.56% (90 lines), Smack.lua 92.24% (35), Mailbox.lua 94.17% (12). Every line added this release was covered; the gaps were older, and the worst of them was that Smack's combat-log triggers had no executed example at all -- killing blow, group death, bloodlust -- so a regression there would have shipped green exactly as the /say one did. Now specced, with the payload shapes from Blizzard_CombatLogProcessor.lua: those three, own death outside combat, per-GUID group re-arm, target re-arm on target change, the in-combat binding deferral, the bound button's OnClick, OnEnable, the mid-fight /reload pickup, and the cheap exit when no combat-log trigger is armed. GuildBankLog gained the event paths that were never driven (PLAYER_LOGIN, GUILDBANKFRAME_CLOSED resetting the debounce, GUILDBANKLOG_UPDATE with and without a tab), the sub-tab refresh gate, the deferred-ingest fallbacks (including that ClearAll cancels a pending ingest, the user-reported "Clear isn't clearing"), a buffer slot whose row has not arrived, the tab filter in GetEntries (a Move matches either side), and every early return in Dump / Diagnose. Mailbox gained the login alert through the real timer queue (plural and singular forms, silence when nothing expires or the watcher is off) and the live tab refresh. One line each in Smack.lua and Gratz.lua was unreachable by construction rather than untested: return Fill(template, { ... }) with a multi-line argument puts the RETURN on the closing }) line and Lua 5.1's tail call never executes it. That is a harness gap, not an addon one, and it was fixed there the same night (harness e915b0a, thread 368021de743a: an OP_RETURN directly after an OP_TAILCALL no longer makes its line executable) rather than by keeping the build-the-table-into-a-local rewrite both files briefly carried. All three at 100%; suite 958 -> 995, 0 failed.
  • CHANGELOG.md archived down to 31 KB. It was 116,926 characters, 3,074 under the 120,000 working ceiling (GitHub refuses a release body over 125,000), with a 95 KB v0.8.4 section that the next entry of any size would have pushed over. The whole v0.8.4 section moved to CHANGELOG_ARCHIVE.md ahead of v0.8.3, at a heading boundary, with the editing tools rather than a script (the user's call: a script would mangle it). The trailing pointer now says "v0.8.4 and earlier". Both files lint clean.

Known Issues

  • Guild Bank log: the occurrence ordinal is a position in a mutating buffer (audit round 5, Finding H). A stored transaction's dedupe key can change underneath it when Blizzard's per-tab buffer shifts, so a re-ingest can either miss a third identical stack or store a duplicate. Not a regression: it has shipped in every release since v0.5.1. Deliberately not touched -- a wrong fix to a dedupe key on stored user data is worse than the defect -- and gated on the user's three-deposit test on a TBC/Anniversary guild bank, which says which of the two outcomes actually happens. A player report of missing or doubled identical stacks in the Guild Bank log lands on this item. Location: Modules/GuildBankLog/GuildBankLog.lua (assignOcc).

[v0.8.5] (2026-08-19) - Gratz's guild level-up trigger was dead, and everything said it was fine

Bug Fixes

  • Guild level-up gratzes stopped firing, and the module looked perfectly wired the whole time. Reported from the field: a guildmate dinged, another addon announced it in chat, and the Gratz profile that has fired 325 times said nothing.

    Root cause: LibGuildRoster-1.0 stopped firing OnMemberLevelChanged at MINOR 14, and registering against a dead callback still succeeds. That trigger was Gratz's only source for levelup-guild. MINOR 14's build-once rework replaced the post-login rebuild with a single build, and deleted the roster diff that produced the callback -- the library says so at LibGuildRoster-1.0.lua:121-136 and again where the diff used to be, :1713-1741. Nothing errors when you register a handler for an event nobody fires, so Modules/Gratz/Gratz.lua read as correct, the MINOR >= 10 gate passed on a modern library, and three specs in Tests/gratz_spec.lua were green the entire time the feature was dead -- they drove a stub library's callback directly, which tests the handler and says nothing about whether the client reaches it. The 0.8.4 audit read this module line by line and did not catch it either, because every line in it is correct; what was wrong was an assumption about another repo.

    The fix: Gratz reads the guild roster itself. There is nothing to move the trigger to -- Classic Era has no CHAT_MSG_SYSTEM message for a guildmate levelling, and GUILD_NEWS_FORMAT6 is a Cataclysm Guild News feed the flavour does not have -- and re-reading the roster is exactly the cost build-once exists to remove from a library twenty addons share. So the polling lives in the one feature that needs it: armed only while an enabled levelup-guild profile exists, requesting a roster every 60s (first request 10s after login), scanning at most once every 5s because GUILD_ROSTER_UPDATE is fired in a loop by the client, and never touching SetGuildRosterShowOffline -- writing that flag from inside the handler fires another GUILD_ROSTER_UPDATE, which is the feedback loop that once read as an FPS decay rather than a crash. The presence policy is unchanged: a member must be seen ONLINE at both ends of the change. A first sighting is always a baseline and never a gratz, which covers login and anyone who joined since; a roster row that arrives with no level is not recorded at all, because inventing a placeholder is what manufactures a phantom 1 -> 60 ding; and a guild transfer re-baselines rather than diffing one guild's levels against another's. Location: Modules/Gratz/Gratz.lua, written up in full as docs/DEPENDENCY_CONTRACTS.md section 2.

  • Tests/gratz_spec.lua fired its events at the wrong frame for the first world in the file, and nothing failed. The spec harness took Gratz's event frame as frames[1]. AceGUI and Compat load through the same hooked CreateFrame and only build anything on the FIRST world -- env.ace caches the library -- so world one had two frames and frames[1] was not Gratz's. Every w.frame:Fire(...) in that world went to a frame with no handler on it. It never failed because firing an event at the wrong frame produces silence, and silence is what most of these specs assert. It now takes the frame created while Gratz loaded. Found by the first spec that expected a world-one event to produce something. Location: Tests/gratz_spec.lua.

Improvements

  • The guild level-up specs now drive the roster, not a stub callback. Old/new level and both presence flags mean exactly what they always meant, but the path under test is the one that runs in game. Three specs pinning the MINOR >= 10 gate were deleted rather than adapted: they asserted that a registration was correctly made against an event no client sends, and all three passed throughout the outage. The MINOR floor went with them -- it guarded a feature nobody supplies any more, and its refusal message named the wrong cause. What is left of the LibGuildRoster-1.0 dependency is IsReady / IsInGuild for the achievement scope filter, both present since MINOR 5, so the guild trigger now works with the library absent entirely. Locations: Tests/gratz_spec.lua, Tests/toc_spec.lua, docs/DEPENDENCY_CONTRACTS.md.

  • Modules/Gratz/Gratz.lua is back to 100% line coverage, up from 92.27%. The gap was the achievement enrichment path -- GetAchievementCategory / GetCategoryInfo / GetAchievementLink are each feature-detected separately and are nil on Classic Era, so the branch that actually runs there was the only one any spec drove. They are now opt-in in the spec world for the same reason GetAchievementInfo already was. Also newly covered: the guild-as-entity achievement path's own criteria (it has a separate fire helper, so nothing was shared with the individual path), the bare-GuildRoster deprecation fallback, and the party tracker catching a level gained while UNIT_LEVEL was missed. One spec that was written to assert enrichment from _lastOwnAchievementID now records the truth instead: that cache is consulted only for the player's OWN achievement, and skip-self returns before it is read, so it cannot currently reach anything. Location: Tests/gratz_spec.lua.

Documentation

  • README.md and the CurseForge description said Smack was Classic-only. It has not been for two flavours. The gate in Modules/Smack/Smack.lua:134 is isRetail120Plus, not isRetail -- War Within 11.x has no secret values and a working combat log, so it keeps every trigger, and even on Midnight only six go dormant (target health, group health, killing blow, big hit taken, group death, Bloodlust). Everything self-scoped still fires there: own health and mana, pull, survival, crowd control, death, plus the hotkey. Both documents told Retail players the tool was not for them. Also corrected: the public-chat note described the 11.x rule (blocked during boss encounters); on 12.0+ automated Say/Yell/Emote is blocked generally, which is why the in-game warning at Smack.lua:365 points at Party/Raid/Guild or a hotkey.

  • Smack's hotkey was documented nowhere at all. Any filter can be bound to a key (Modules/Smack/Smack.lua:722-770), which skips the per-filter cooldown on a one-second floor and reaches channels an automatic trigger cannot, because a keypress is a hardware event. That is a headline feature and neither document mentioned it existed. Both now do, including the reason it can speak where auto-triggers cannot.

  • The minimap button was also undocumented. GUI/MinimapButton.lua ships a LibDataBroker launcher plus a LibDBIcon button with drag-to-position and a per-character hide, so Titan Panel and other broker displays pick it up unprompted. Neither document mentioned it, nor that LibDataBroker and LibDBIcon are ## OptionalDeps whose absence costs only the button.

  • Gratz was documented with three triggers and it has four. achievement-guild -- an achievement the guild itself earns, with its own fire helper and [player] mapped to the guild name -- was missing from both, as was the [guild] placeholder from the template list. The guild level-up entry now also states the online-at-both-ends rule and the silent first look at the roster, since those are the two behaviours a user would otherwise read as the feature not working.

  • VersionCheck-1.0 was missing from both Requirements lists, despite being a hard ## Dependencies entry on all five Classic TOCs. And LibGuildRoster was described as what powers the guild level-up trigger, which stopped being true this release -- it is the achievement scope filter now.

  • Slash-command tables were incomplete. Missing aliases added (mb/mailbox, ld/digest, nameprefix, addonload, log, whispers, config), and both documents now say plainly that Smack and I'd Hit That have no shortcut, rather than leaving a reader to conclude the list is wrong. Verified against SlashCommands.lua rather than against the old table.

  • Checked and deliberately left out of user-facing docs: Item DB (GUI/ItemDBTab.lua:20, devOnly) and Diagnostics (a Logs sub-category gated behind Developer Tools). Both are developer tools, so their absence from the feature list is correct -- README.md now says the Developer Tools switch exists and what it reveals, which it did not before.


Older releases (v0.8.4 and earlier) are archived in CHANGELOG_ARCHIVE.md.

This mod has no additional files