TOGTools-v0.8.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 addedGreenWallAPI.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. NewrelayThroughApiinCompat.luacalls it frombridgeThroughGreenWallwhengw.configexists 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 inaddon.Chat.Sendstays. The result table is read:okwithtruncatedprints "was cut short";no-peersis debug-only, because no co-guild member who could have seen it is online;not-configured,refused,too-long,secretand 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.luanow namesaddonNamefrom...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; silentno-peers; each refusal reason told; truncation told; a raising SendChat; not calling it beforegw.config; version-1 fallback; party/raid/say not bridged. Full suite: 1037 passed, 0 failed. GreenWall 1.14.0 shipped (GreenWall commit3893010); its shippedAPI.luaandRelay.luawere re-read before release and match the stand-in:version = 2, theSendChat(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 inSendChatMessage(segment, "CHANNEL", nil, number)(GreenWallChannel.lua:366-370). A numbered CHANNEL send is hardware-event restricted indoors and outdoors (warcraft.wiki.ggAPI_SendChatMessage), so an event-driven Gratz or Smack line was lost silently. A typed line works only because GreenWall'sParseTexthook 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:
bridgeThroughGreenWallno longer sends. It queues{msg, chanType, key, t}and shows a hidden relay frame built atCompat.luaload; the frame'sOnKeyDown(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 callsSetPropagateKeyboardInput(true)beforeEnableKeyboard(true)so every key still reaches the game; propagation is combat-restricted, so a load in combat (a /reload mid-fight) arms the frame onPLAYER_REGEN_ENABLEDand 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 throughaddon.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 throughsendIfConnected, which asks the channelis_connected()(GreenWallChannel.lua:125-138, a freshGetChannelNameeach 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 thepcallare each printed throughreportRelayProblemwith their own reason. The error notice says the line may not have reached the sister guilds rather than that it was not passed:tl_flushdrains 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 segmentopcode#guild_id##messagegoes throughstrsub(..., 1, GW_MAX_MESSAGE_LENGTH)(Channel.lua:322; 255 inConstants.lua:79), so the tail is lost with no error. A typed line is cut the same way;greenWallCutsLinenow 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 namedreportRelayProblemrather than for the not-relayed case alone. Location:Compat.lua.Not verified in a client: that an insecure
OnKeyDowncounts 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 carriesis_connected()plus GreenWall's queue-without-raising behaviour when disconnected: one example pins that a send with no key press lands inwow.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 nois_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.newWorldexposes the relay frame (frames[beforeCompat + 1]) andw.press().A guild line sent while GreenWall was loaded but not yet set up was dropped from the relay without a word.
bridgeThroughGreenWallreturnedfalsesilently whenevergreenWallChannel(key)orrelayFramewas nil. GreenWall'sgwglobal exists from itsGlobals.lua:36, butgw.configis only built in itsADDON_LOADEDhandler (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.gwnil); otherwise the line goes throughreportRelayProblemand nothing is held. One reason per condition: no relay frame (noCreateFrameat 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 whengw.configis missing; a config lacking the channel orGW_MTYPE_CHATis 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 threadc54b54b4: 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 })hidesCreateFramewhileCompat.lualoads).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_HELDsilently (peer review, thread5ca12a4d).Compat.luanow publishes both asaddon.Chat.RELAY_MAX_AGEandaddon.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", readsdocs/Curseforge_Description.htmland 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 ofCompat.lua(harnesscoverage.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, thread6d00c25e). 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, threade3c168f4). 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'sAceEvent-3.0.luacallsCreateFrameat 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_Mainlineand_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 productwow_classic_beta) is the RETAIL client --WOW_PROJECT_ID == WOW_PROJECT_MAINLINE, retailC_*namespaces, Menu API, secret values, the live tree -- with vanilla content at## Interface: 16001. The client reads<Addon>_Camelot.tocbefore_Mainline, soTOGTools_Camelot.tocisTOGTools_Mainline.tocwith 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.luaderives every flavour flag fromselect(4, GetBuildInfo()), and 16001 sits inside the Vanilla band -- so on ForeverisVanillawould be true andisRetailfalse, 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 isisForever = 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.isRetail120Plusis true there because Forever runs the 12.x codebase; not verified in a Forever client (none on this box, andF:\Blizzard API Docshas 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_MAINLINEare declared in.luarc.jsonand.luacheckrc, verified present on every Classic tree (Blizzard_FrameXMLBase/<Flavour>/Constants.lua).!!TOGTships its own_CamelotTOC 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 (thread0a9abbb39cb5) 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 fromWOW_PROJECT_ID), andloadAtnow sets the project id explicitly on every load rather than inheriting it.Tests/toc_spec.lua-- the seventh manifest inTOCS, 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 withgw.config.channel.guild:send(GW_MTYPE_CHAT, msg).CHAT_MSG_GUILDis received but only logged (GreenWall.lua:326-330, "Messages will be forwarded by the ChatEdit_ParseText hook"). An addon'sSendChatMessagenever goes through the edit box, so nothing we said was ever relayed.GreenWallAPI.SendMessagewas not the fix either -- it sendsGW_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_iddefaults to'',tl_flushrefuses while not connected). The call ispcall'd and the failure written toaddon: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).linkSiginModules/GuildBankLog/GuildBankLog.luamatched|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 emitsitem:%dwhen every optional field is empty). Both patterns missed and the fallback returned the link itself. So one movement carried twoitemSigs 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:858anditem:858::::::863all sign858. 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 == itemLinkexactly -- that equality is the tell, and nothing else produces it (an id or a caller-supplied sig never equals the link). New one-shotresignItemSigs(bucket, guildKey), marker_sigsRekeyed, next to its two siblings: re-signs such rows under the fixedlinkSig, regenerates theirid(the sig is part ofrowId), then collapses any pair that now shares a dedupe key in original array order, backfillingnamefrom the dropped copy. It runs from BOTH write paths -- aftermigrateOccOrdinalsiningestTab, and at the top ofInjectEntry-- because on Classic Era nothing ever ingests and every row arrived through injection. Theidchange is invisible to TOGBankClassic:InjectEntrynever returns it and nothing reads it back yet.Specs:
Tests/guildbanklog_spec.lua-- twolinkSigexamples (colon-less link and bare item string, each alongside its coloned twin, all signing858) and a new "colon-less sig repair" block of 5: a stored whole-link row is re-signed and re-id'd throughInjectEntry; the same movement re-delivered under a coloned link now answersduplicate; 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.luaandGUI\SmackTab.luafromTOGTools_Mainline.toc("Retail 12.0+ blocks automated public chat") and pinned that inTests/toc_spec.lua'sINTENTIONAL_OMISSIONS. v0.8.5 then readSmack.lua:134'sisRetail120Plusgate, 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, andTests/smack_spec.luadrives every flavour. Fix: Smack is listed inTOGTools_Mainline.tocandTOGTools_Camelot.toc(between Gratz and ItemDB, matching the Vanilla order the parity spec checks), andINTENTIONAL_OMISSIONSis 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..pkgmetaignored"!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 withwow-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:DescribeSisterStatusnamed 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.mdand 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 vcsay "Retail or Forever"; Smack's Midnight paragraph names Forever alongside it. The slash-command table was checked againstSlashCommands.luaand matches (the undocumented aliases --/togt db,/togt versioncheck,/togt logs bank|gbank|diag-- stay undocumented on purpose;dbanddiagare 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/grand/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, thesisterAutocompleteOn/otherPriority/_installedSourcelocals and bothInstallAutocomplete()calls in the event handler. The stale-mail watcher is untouched. Removed fromTOGTools.lua: thedb.global.mailbox.sisterAutocompletedefault; a copy already in a player's SavedVariables is cleared once atOnInitialize, because a key with no default is one AceDB keeps forever. Kept:GetSisterStatus(now#lib:GetSisterRecipients(), feature-detected) andDescribeSisterStatus, 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) andMailbox: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.luareads 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/InstallAutocompleteexamples left with the code -- they run in GuildRoster'sTests/mailauto_spec.luaagainst the library's copy.Tests/mailbox_spec.luanow pins the other direction: PLAYER_LOGIN and MAIL_SHOW leaveSendMailNameEditBox.autoCompleteSourceexactly as they found it and the three removed methods are nil (so a second wrapper cannot quietly come back);GetSisterStatuscounts 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 adbcopy, and answers nil / refuses the write with no switch to reach; the defaults table carries nosisterAutocompleteandOnInitializeclears 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.mdanddocs/DEPENDENCY_CONTRACTS.mdare 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 (GuildBankLogoccas 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 viaInjectEntry, 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 threads42e8d2c03719(TOGBankClassic) and5e886a380126(GuildRoster). Both files remain in git history.CLAUDE.mdnow 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 listsdocs/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" -- andsendToChannelsonly skipped automated public chat on Retail 12.0+. The actual rule, per theSendChatMessagewiki 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. NowSmack.PublicChannelBlocked(key, isHardware)answers per flavour -- Retail 12.0+ blocks Say/Yell/Emote everywhere; everything else blocks automated Say/Yell only whenIsInInstance()is false; Emote is not in the rule and stays sendable; a hotkey is never blocked -- andsendToChannelsskips 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 stubsIsInInstance(outdoors by default, deliberately), thefilter()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 aRETAIL120), 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.newWorldnow takes a TOC interface number, feeds it through aGetBuildInfostub into the realVersion.lua, and theFLAVOURSlist 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.forEachFlavourruns 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):SendChatMessageisHasRestrictions = truein all four and says nothing about WHICH restriction;IsInInstanceexists in all four with the same returns;UnitHealthisSecretReturns = truein 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
SendChatMessageglobal, which on every Classic tree is only a deprecation fallback.Blizzard_DeprecatedChatInfo/Deprecated_ChatInfo.lua:8defines it behindGetCVarBool("loadDeprecationFallbacks")and forwards toC_ChatInfo.SendChatMessage; a client with that CVar off has no global at all. Found while reading the docs for the fix above.sendChatnow resolvesC_ChatInfo.SendChatMessagefirst 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): itsenv/protected.luamodels "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 separatesaid/dropped/blockedrecorders. Delivered the same night as harnesse1a7055and adopted here (pinbc75950->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. AndqueuedOrSend/flushCombatQueuecalled the bareSendChatMessageglobal, the deprecation fallback. The rule now lives once, inCompat.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) andaddon.Chat.Send(C_ChatInfo.SendChatMessagefirst, 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'ssendToChannelsskips a blocked public channel and reports it,tryFiretells 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.luaruns under the harness's chat gate like Smack's, three channel-dispatch examples that had been asserting an outdoor/saymoved 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
notifyPublicBlockedused 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 toPublicChannelBlockedReasonthat drops either is caught offline rather than by a player.Peer review of the above, three of its findings fixed. (1)
Smack.sendChatreturned nothing, so on a client with neitherC_ChatInfo.SendChatMessagenor the bare global the fire was still counted and its cooldown burned -- the same shape as the outdoor/saydefect, one guard down. It now returns whether a send function existed andsentderives from it. Not seen on any flavour; fixed because the two guards for one dependency should agree. (2) The Guild Bank log'sbaseKeytreated an empty-stringto/fromas 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.InstallAutocompletewrapped the mail box's autocomplete source once and latched; a mail addon assigning that field in its ownOnLoadafter us removed the wrapper for the session with nothing to reinstall it. EveryMAIL_SHOWnow 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 theto/from-in-identity change could double a row TOGBankClassic pushes first without a party and later with one -- dissolved on their trace of TOGBankClassic'sLog: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/reloadcleared 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 andUISpecialFramesare 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 outsideHide()) funnels throughClose(), sodebugstackthere 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/reloadcleared, 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:GetEntrieshands 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), andsourcewas not in the list.InjectEntrystored it,fmtTabtested 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 carriessource,to,from,noteandrequestId, the comment on it says it is the whole read contract, andTests/guildbanklog_spec.luaasserts 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).
handleCombatLogreadamountfrom two payload shapes only --SWING_DAMAGEatinfo[12]and the spell-standard events atinfo[15]-- soENVIRONMENTAL_DAMAGE(lava, fire, slime, falling, drowning) leftamountnil and skipped the branch in silence, on the flavour where the biggest single hits a player takes are usually environmental. Its payload isenvironmentalType, amount, ..., so the amount isinfo[13], and it now has its own arm; folding it into either existing branch would read the type string orresisted.DAMAGE_SHIELDandDAMAGE_SPLITshare the spell-standard prefix and are added to that arm. Offsets verified inBlizzard_CombatLogProcessor.luaon 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.luagained aw.cleu()driver that delivers the real eleven-field-prefix payload shape, and seven specs, one per offset, each carrying a0where 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). BothexpandMessageandexpandLinechained onegsubper placeholder with the game-supplied value as the REPLACEMENT string, where%is an escape (%1is 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 calladdon.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 isALL_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.luanow reads the sister rostersLibGuildRoster-1.0holds (its MINOR 6 cross-guild API:GetKnownRostersminusGetHomeGuildKey,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 EraMailFrame.xml:584) storesC_AutoComplete.GetAutoCompleteResultsinSendMailNameEditBox.autoCompleteSourceandAutoComplete_Updatecalls it assource(text, max, cursor, allowFullMatch, include, exclude); the module replaces the field with a wrapper that calls the original and merges, installed once onPLAYER_LOGIN(retried onMAIL_SHOWfor a client that builds the frame on demand).maxis 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 wholeName-Realmonce the typed text carries a dash; a name Blizzard already returned is not added twice. Appended entries carryEnum.AutoCompletePriority.Other(fallbackLE_AUTOCOMPLETE_PRIORITY_OTHER), and if a client has neither the wrapper contributes nothing --AutoComplete_UpdateResultsindexesAUTOCOMPLETE_COLOR_KEYSby 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#1account 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.mdsection 3, inbox threada95015801eb8) 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 (SendWhoneeds 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) andDescribeSisterStatus, 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 inTests/mailbox_spec.lua: the matching rule pure, the roster read and the status against a stub registered in the real LibStub (evicted after each, sincewow.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 the981197530#1account: list set on Togcook (SetSisterGuildNames), roster relayed from Picky's TOGPM re-feed (held=1in the library's health check), andShlonkin 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 (guildRegistry8e5c20), 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 throughGuildBankLog:InjectEntry(its LOGAPI-002, same day) withsource = "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'sfmtTabrenders 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 nosourceand are unchanged. TOGBankClassic passesoccitself (its version's publish time, identical on every client) so a viewer alt re-injecting the banker's row returnsduplicaterather than landing asocc+1-- the correction that came out of readingInjectEntry:1186-1193against 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) orfrom(the sender a deposit arrived from), and the Player column renders them asToglowweap > Graymoonon a withdrawal andRestaurants > Toglowgearon 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 butto, and withoccbeing that version's publish time the second came backduplicate.baseKeyappends the party ONLY when set, so a native row's key -- and therefore its stored cross-viewerid-- 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 = trueis the switch, set only now that every stage handleskind = "request". A request row is a lifecycle EVENT, not a stock movement:typeis one ofrequested/mailed/handed/cancelled/reopened,namethe actor,tothe other party,countthe units this event covers (absent on cancelled and reopened),notethe cancel reason,requestId, an absolutets, and a STRINGoccof"<ts>:<requestId>". Its identity is therefore the event --request|type|requestId|ts|count-- never the item tuple, so arequestedand amailedof the same item, or two partial fills of one request, stay apart while a replay of the same event dedupes. The stringoccfound a latent crash:InjectEntry's next-ordinal scan comparede.occ > maxOccon every stored row of the incoming tuple, and Lua 5.1 raises onstring > number; it now counts numeric ordinals only, and a spec re-injects the same event with itsoccstripped -- 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 isRestaurants > Toglowgear; Item is the link with the cancel reason in grey after it; Qty is blank rather than0when the event carries no units; Tab is the[TOGB]tag. Retention prunes them bytslike every other row (specced). Amailedrow and thewithdrawrow 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 pushingmailed. Thirteen specs inTests/guildbanklog_spec.lua; the dependency contract indocs/DEPENDENCY_CONTRACTS.mdsection 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
matchedby.namewhile the comment justified the order being total "becausedb.profilesis keyed by name" -- the reason cited the key and the comparator read the field, and the comparator was then byte-identical in Gratz, Smack,GetProfilesandGetFilters.addon.Table.SortedByKey(map, pred)inCompat.luasorts by the key, which is unique by construction, and all four sites use it. Two specs give every entry the same.nameand 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).
_dedupPollingwas tested only by thePLAYER_GUILD_UPDATEwatcher; 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.ApplyDedupSettingnow 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.ApplyMinResizeandaddon.GUI.DetachPoolwere ported from FastGuildInvite / TOGProfessionMaster and never called here; the header said "used by every tab". The reviewer's caveat -- a sibling might reach them throughTOGTools.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, andRowList:Detachorphans 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:
fireFilterreturns before_cooldownLastandcountwhensendToChannelsreports 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.mdwere 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 atff379c2since 2026-08-19 despite a "pinb307a9b" 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)seterrorhandleris real (harness2406895), soTests/diagnostics_spec.luano longer stubs the error-handler pair afterload()-- the stub backed the same slot and, by pinningprevto nil, leftDiagnostics.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.GetServerTimeLocalis real (harness7e929c2), 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 -- from7e929c2it was driving the OFFSET branch and stayed green becausedd/ddand:00hold under a whole-hour shift too. It now removes the function withwow.withoutApiand asserts the exact un-shifted string, and a new block setswow.serverUtcOffsetper example and asserts the hour moves by exactly +3h, -5h across a date boundary, and in the year form. LocalGetServerTimestubs stay: they drive the clock, and the harness'sGetServerTimeLocalreads 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 and1cfad99, which makes a test frame'sFire("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%dnow refuses a value over 2^31 like the client, the anchor solver keeps an explicit height under a fill-width child,InboxItemCanDeletemeans deletable-not-returnable) and the suite was green at the new pin on the first run, 988/988. The one built for this addon isprotected.enforceChat():Tests/smack_spec.luaopts in per world, drops itsIsInInstancestub forwow.instanceType, sets the flavour throughwow.setBuildso the harness's 12.0+ rule andVersion.lua's flags read one number, and runs a hotkey press insideprotected.asHardwareEvent-- the hollow test frame'sFiredoes not mark one, and a keypress is one.w.sentis nowwow.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/sayand 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.lua84.56% (90 lines),Smack.lua92.24% (35),Mailbox.lua94.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/sayone did. Now specced, with the payload shapes fromBlizzard_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'sOnClick,OnEnable, the mid-fight/reloadpickup, and the cheap exit when no combat-log trigger is armed. GuildBankLog gained the event paths that were never driven (PLAYER_LOGIN,GUILDBANKFRAME_CLOSEDresetting the debounce,GUILDBANKLOG_UPDATEwith and without a tab), the sub-tab refresh gate, the deferred-ingest fallbacks (including thatClearAllcancels a pending ingest, the user-reported "Clear isn't clearing"), a buffer slot whose row has not arrived, the tab filter inGetEntries(a Move matches either side), and every early return inDump/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 inSmack.luaandGratz.luawas unreachable by construction rather than untested:return Fill(template, { ... })with a multi-line argument puts theRETURNon 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 (harnesse915b0a, thread368021de743a: anOP_RETURNdirectly after anOP_TAILCALLno 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.mdarchived 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 toCHANGELOG_ARCHIVE.mdahead 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.0stopped firingOnMemberLevelChangedat MINOR 14, and registering against a dead callback still succeeds. That trigger was Gratz's only source forlevelup-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 atLibGuildRoster-1.0.lua:121-136and again where the diff used to be,:1713-1741. Nothing errors when you register a handler for an event nobody fires, soModules/Gratz/Gratz.luaread as correct, the MINOR >= 10 gate passed on a modern library, and three specs inTests/gratz_spec.luawere 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_SYSTEMmessage for a guildmate levelling, andGUILD_NEWS_FORMAT6is 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 enabledlevelup-guildprofile exists, requesting a roster every 60s (first request 10s after login), scanning at most once every 5s becauseGUILD_ROSTER_UPDATEis fired in a loop by the client, and never touchingSetGuildRosterShowOffline-- writing that flag from inside the handler fires anotherGUILD_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 phantom1 -> 60ding; 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 asdocs/DEPENDENCY_CONTRACTS.mdsection 2.Tests/gratz_spec.luafired its events at the wrong frame for the first world in the file, and nothing failed. The spec harness took Gratz's event frame asframes[1]. AceGUI and Compat load through the same hookedCreateFrameand only build anything on the FIRST world --env.acecaches the library -- so world one had two frames andframes[1]was not Gratz's. Everyw.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.0dependency isIsReady/IsInGuildfor 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.luais back to 100% line coverage, up from 92.27%. The gap was the achievement enrichment path --GetAchievementCategory/GetCategoryInfo/GetAchievementLinkare 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 reasonGetAchievementInfoalready 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-GuildRosterdeprecation fallback, and the party tracker catching a level gained whileUNIT_LEVELwas missed. One spec that was written to assert enrichment from_lastOwnAchievementIDnow 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.mdand the CurseForge description said Smack was Classic-only. It has not been for two flavours. The gate inModules/Smack/Smack.lua:134isisRetail120Plus, notisRetail-- 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 atSmack.lua:365points 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.luaships 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## OptionalDepswhose 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.0was missing from both Requirements lists, despite being a hard## Dependenciesentry on all five Classic TOCs. AndLibGuildRosterwas 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 againstSlashCommands.luarather 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.mdnow 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.
All Relations
- All Relations
- Embedded Library
- Optional Dependency
- Required Dependency
- Tool
- Incompatible
- Include
