promotional bannermobile 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.10.1

File nameTOGTools-TOGTools-v0.10.1.zip
Uploader
PmptastyPmptasty
Uploaded
Oct 4, 2026
Downloads
34
Size
2.4 MB
Flavors
RetailMoP ClassicClassic TBCClassicForever
File ID
9059891
Type
R
Release
Supported game versions
  • 12.1.0
  • 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.10.1] (2026-10-04) - WoW Forever recognised again

Bug Fixes

  • WoW Forever was treated as Classic Era, so opening a profession window threw ProfCapture.lua:412: attempt to call a nil value (GetTradeSkillLine). Reported by the operator from a Forever client, opening TOGProfessionMaster's Crafting tab. Root cause: addon.DeriveGameVersion only recognised Forever when WOW_PROJECT_ID == WOW_PROJECT_MAINLINE, but Forever reports project id 18 (FastGuildInvite observed "Project ID: 18, Build: 16001" in-client on build 1.60.1.70124, FGI_Compatibility.lua:22-38; the forever tree's ProjectConstants.lua names only 1 and 2). So Forever at Interface 16001 was derived as Vanilla, isRetail was false, and every Forever/Retail branch in the addon was dead there -- ProfCapture took the Classic GetTradeSkillLine path, which Forever does not have. Other consumers that were wrong on Forever for the same reason, among them: Quest DB refused to run at all (QuestDB.lua:71 gates on isForever, "only runs on the WoW Forever client"); ProfCapture:ReadTrainer read GetTrainerServiceInfo in Classic return order (ProfCapture.lua:182-198); I'd Hit That skipped Forever's ranged-hit modifier (IdHitThat.lua:275); Smack offered its six combat-log triggers, which it hides on isRetail120Plus (Smack.lua:142-147); and /togt vc tried VersionCheck instead of saying it is not available. The load-time call now counts project id 18 as a retail-codebase client too. New spec: "is derived from Forever's own project id, 18" (Tests/version_spec.lua). Not verified in game. Location: Version.lua.

Process

  • CHANGELOG.md archived under the release-body ceiling. With the entry above it measured 121,457 characters, over the 120,000 working ceiling (GitHub refuses a release body over 125,000). The oldest section, v0.8.6, moved whole to the top of CHANGELOG_ARCHIVE.md, ahead of v0.8.5, with the editing tools. The trailing pointer now says "v0.8.6 and earlier".

[v0.10.0] (2026-09-30) - Event Trace developer tab

New Features

  • Event Trace: a live stream of every event the WoW client fires. New developer tab (shown with Developer Tools on) over the stream !!TOGT v0.2.5 captures from the first line of addon code (!!TOGT/EventStream.lua). Lines arrive as they happen, like a chat window: sequence number, local time with milliseconds, seconds from login (negative before it), event name, arguments. The view is a ScrollingMessageFrame (the widget chat windows use; AceGUI has none) keeping the newest 5,000 lines on screen, with the mouse wheel scrolling 3 lines, or 20 with Shift. Filter narrows the view and the copy to lines containing the text. Copy opens every captured line, not only the on-screen ones, in a read-only box for Ctrl+A / Ctrl+C: an AceGUI MultiLineEditBox whose OnTextChanged (which AceGUI fires only for the user's own typing) puts the text back, so nothing can be deleted by accident. Counts lists every event fired since load and how often, excluded ones included. Pause, a confirmed Clear, a Feed on tick box, and an editable exclude list round it out. Feed on is OFF by default and saved, so it stays as left through reloads (operator: "we don't want a memory leak, this should only work when i want it to"); unticking it stops the stream at once and frees every line. The Copy box shows the whole stream 1,000 lines to a page, with Previous / Next, because one edit box cannot hold it. That was measured in game on 2026-09-30. The whole stream in one box showed BLANK at 9,511 lines. It was blank again at 5,099 lines after the box's byte cap was lifted (SetMaxBytes(0), as the AceGUI report in WoWInterface thread 55511 did) and !!TOGT began escaping control bytes. 1,000-line pages showed and copied. Later the same day a single box showed at 1,691 lines and was blank at 1,992. AddOn Studio's EditBox:SetText page documents one cause: "SetText fails when the text contains characters that aren't renderable (e.g. control chars, invalid UTF8). In that case the EditBox is cleared". So !!TOGT v0.2.5 now also escapes every byte that is not valid UTF-8, and never cuts an argument mid-character. Whether that alone lets one box hold a long stream is not tested in game, so Copy stays paged. Needs !!TOGT v0.2.5; with an older !!TOGT the tab says there is no event stream. Memory only: nothing is saved, so a /reload starts a fresh stream. The operator asked for it on 2026-09-30 ("an in game log capture ... capture them into a 'memory' log and display them in TOGTools to help analysis"; "it's just a livestream we capture"; "this is to capture the wow api, so we can see what's going on in it"). It replaces a GUILD-chat login probe drafted earlier the same day for FastGuildInvite's question (inbox thread 15b4d7a3), which never shipped.
  • Settings in db.global.eventTrace (enabled, capture, exclude), which !!TOGT reads from TOGTools_DB at PLAYER_LOGIN. EventTrace:SetExclude applies a new list to the live stream at once, and empty text restores !!TOGT's default. SetCapture(true) restarts a stopped stream and SetCapture(false) calls !!TOGT's Stop(). The module is in moduleregistry_spec's GATE_EXEMPT, with the reason: the gate is !!TOGT's. 15 specs in Tests/eventtrace_spec.lua load the REAL !!TOGT files from the sibling folder, not a stand-in; Modules/EventTrace/EventTrace.lua is at 100% line coverage. Locations: Modules/EventTrace/EventTrace.lua, GUI/EventTraceTab.lua, TOGTools.lua, every .toc, Tests/eventtrace_spec.lua, Tests/moduleregistry_spec.lua.

Bug Fixes

  • Quest DB threw a load error on the Classic client: Attempt to register unknown event "QUEST_DATA_LOAD_RESULT" (24x). Reported by QuestDB (inbox thread b0734afe) from the user's _classic_ client. The event exists only in the live and Forever trees (QuestLogDocumentation.lua); it is not in the classic or classic_era docs. Modules/QuestDB/QuestDB.lua loads on every flavour and registered it unguarded at file load, and RegisterEvent throws on an event the client does not have. The registration is now pcall'd; the walk only runs on Forever (UnavailableReason), so nothing is lost elsewhere. New spec: "loads on a client that does not have the reply event" (Tests/questdb_spec.lua), whose frame stub throws on any RegisterEvent as the client does. Not verified in game. Location: Modules/QuestDB/QuestDB.lua.

[v0.9.6] (2026-09-29) - Diagnostics explains Lua errors

New Features

  • Explain a Lua error. Players bring errors to whoever they know, whether or not the addon is theirs: the prompt for this was a Retail LibOpenRaid.lua:1222: attempt to compare a secret number value (execution tainted by 'Details') brought to the TOG author. New pure-logic addon.ErrorInterpreter (Interpret returns a result table, Explain returns the display text) reads a single path:line: reason line or a whole BugSack / Blizzard error-window paste. It works out what the error means (16 rules: secret value, blocked / forbidden action, attempt to call / index / compare / concatenate / arithmetic, bad argument, stack overflow, script ran too long, SavedVariables too large, missing library, XML from another game version, and more), which addon folder owns the file (it also handles a cut-off ...face/AddOns/ path, a path pasted without its Interface/AddOns/ prefix, and [string "@..."]), and whether the file is inside a library bundled with another addon (Foo/Libs/LibBar/). It then says who to report it to. For Blizzard code or a standalone library addon (Lib*, Ace*, Ace3), the report goes to the game's tainted by 'X' marker first, otherwise the first real addon in the stack (skipping BugGrabber/BugSack). The stack scan stops at Locals:. In the Diagnostics tab: a new Explain entry in the row right-click menu, and a plain left-click on a row does the same (added after the user could not find how to get a row into the window). There is also an Explain an error button that opens a paste window updated as you type. The window shows the addon's installed title, version and author through GetAddOnMetadata, and says whether it is a Pimptasty addon (author Pimptasty or a TOG* folder). The label is "Pimptasty", not "TOG", because not all of the user's addons are TOG ones. For a Pimptasty addon, the Discord invite (EI.DISCORD, the same link the README uses) appears in a read-only copy field right under the reading: click it and press Ctrl+C, since addons cannot write the clipboard. It was briefly a separate button; the user asked "why can't we just have the copy box appear in the text". The field is the new addon.UI.CopyField, which selects all on click and puts the text back if typed over. Its select-all hook on AceGUI's pooled edit box acts only while TOGTools holds the widget. The field is shown only for Pimptasty addons, so the explanation pane is rebuilt on each render (AceGUI's List layout leaves room for a hidden child). The Explain window snaps to the main window's right edge with LibAceGUIWidgets' DockWindow (user request). It is docked by default; dragging it away undocks it, dropping it near the edge re-docks it, and the choice is kept per character in char.frames.explainDock. An AceGUI Frame moves from its title bar's mouse handlers and never fires the OnDragStop that DockWindow listens to, so the title bar's OnMouseUp calls h:Moved(). The main window's frame goes back to AceGUI's shared pool on close, and the Explain window must let go of it first, or it would dock to whichever addon's window reused that frame. A new MainWindow.beforeRelease hook calls the library's W:UndockWindow (MINOR 38, requested on thread 2472500eacf8; that MINOR also lets go of a released AceGUI frame by itself). On an older library, it pins the Explain window and re-points its dock at a private hidden frame. Output is ASCII only. Location: Modules/Diagnostics/ErrorInterpreter.lua (new, added to all seven TOCs), GUI/DiagnosticsTab.lua, GUI/MainWindow.lua. Spec: Tests/errorinterpreter_spec.lua, which uses the Details error verbatim; line coverage is 100%. After Peer Review's reply to this session's self-audit (thread 4e63f914), there were three fixes, each with a spec. The stack scan skips the error's own message line, so a reason quoting a file ("... in Foo/Bar.lua") no longer names Foo as the caller; that spec was written first and failed first. The Explain window interprets once per keystroke and passes the result to Explain and WantsAddonLoad, which now accept either text or a result. It used to interpret the same text three times. And the "Pimptasty addon" test is EI.IsOurs, a substring match on the TOC Author line, because co-authored addons carry the name inside a longer line.
  • Diagnostics keeps each error's original message (msg, new entries only, capped at 500 characters, not part of the dedup key), because the compact source drops the folders where an embedded library shows. Rows saved before this, and blocked-action rows, are rebuilt into the game's own wording for Explain. Location: Modules/Diagnostics/Diagnostics.lua. Spec: Tests/diagnostics_spec.lua.

Improvements

  • One copy box, from the widget library. The user asked for the "copy" popup to live in LibAceGUIWidgets so every addon can use it. That library method, W:ShowCopyBox(text, opts) (one button, text selected, read-only), was requested on the library's inbox (thread 737274f20c2f) and built there at MINOR 38 (library v0.2.8). TOGTools now copies through a single helper, addon.UI.ShowCopy(text, prompt, parent), which calls ShowCopyBox when the installed library has it and otherwise builds the same box from the library's existing ShowDialog (one button, pre-filled, selected). So nothing in TOGTools has to change when the library ships it. The hand-rolled StaticPopupDialogs["TOGTOOLS_DIAG_COPY"] is gone; the Diagnostics row Copy uses the helper. parent is passed on purpose: the library dialog takes its parent's strata, so parenting to UIParent would open the box behind the FULLSCREEN_DIALOG windows. Location: GUI/UI.lua, GUI/DiagnosticsTab.lua. Spec: Tests/uishowcopy_spec.lua (both paths).
  • Diagnostics is its own main tab, for every player, not a developer-only Logs sub-tab. User report, 2026-09-29, with a screenshot of Settings > Modules: the Diagnostics tab did not appear and there was no toggle for it. I had made it visible only when Settings > General > Developer Tools was on, so no player could reach it, and neither could the user with Developer Tools off. It briefly became a Log category, and then the user asked: "it needs to be it's own main tab, and not a subtab". It now registers in addon.modules["diagnostics"] (no longer addon.logCategories), with tabKey/configKey/label/WINDOW_SIZE in GUI/DiagnosticsTab.lua, the usual automatic Settings > Modules toggle backed by diagnostics.enabled (default on), and showWhenDev so it stays visible under Developer Tools with the toggle off. The gate (Diagnostics.IsWanted, which is also recordingEnabled) is IsModuleEnabled("diagnostics") OR Developer Tools. Switching it on runs Diagnostics:Wake() (broker, fallback capture, draining the companion's early buffer), from the module toggle's OnModuleToggled and from the Developer Tools setting, replacing the startup code the Settings file used to carry. The toast, the Titan broker and /togt diag open the main tab. /togt diag no longer insists on Developer Tools: it refused players even after the tab was surfaced, which I missed when I changed the gate. /togt logs diagnostics still gets there, and /togt diag is now in /togt help. Known cost: with it on by default, every player records errors (capped at 500 distinct entries) and sees the new-error pop-up. Location: Modules/Diagnostics/Diagnostics.lua, GUI/DiagnosticsTab.lua, GUI/Settings.lua, GUI/LogsTab.lua, SlashCommands.lua, TOGTools.lua (default). Specs: Tests/diagnostics_spec.lua (the recording gate, the main-tab registration, Open, Wake) and Tests/logregistry_spec.lua (Diagnostics exemptions removed).
  • Explain sends a missing-dependency error to the Addon Load tab. User request: "if the error is the missing dep error, it needs to have some way to click to open up the addon load tab". New AddonLoad:GetMissingDeps(name) answers one addon's missing required dependencies, the same way the tab works them out. Explain asks it for the culprit and every addon in the stack, names what each is missing under a new "Missing addons" heading, and shows an Open Addon Load button (EI.WantsAddonLoad). It does the same for an error that is itself about a dependency (a new deps rule) or a missing library. Location: Modules/AddonLoad/AddonLoad.lua, Modules/Diagnostics/ErrorInterpreter.lua, GUI/DiagnosticsTab.lua. Specs: Tests/addonload_spec.lua, Tests/errorinterpreter_spec.lua.
  • The Discord copy field is the library's. The user asked for the library's copy box with its copy icon. LibAceGUIWidgets built an inline widget, LAGW-CopyField (MINOR 38, inbox thread 513d5779bb63), and addon.UI.CopyField creates it when AceGUI:GetWidgetVersion("LAGW-CopyField") >= 2. Version 1 errored on every Create ("FontString:SetText(): Font not set", its label had no font); the user hit it in game, it was reported (thread 63629c29099f) and fixed as Version 2. On an older library, UI.CopyField draws the same box itself: the library's COPY_ICON with its numbers (LEFT +1,-1, 14px, inset 20, the 1.0/0.6 tint), and on release it hides the icon and puts AceGUI's insets (0, 0, 3, 3) back on the pooled edit box. Location: GUI/UI.lua.

Documentation

  • README and the CurseForge page checked claim by claim against the code (user request: "ensure we have addon functionality parity with the documents"). Corrected where the documents said something the code does not do:

    • Both promised a history-retention setting. retentionDays is only a default (TOGTools.lua), and GUI/Settings.lua has no control for it, so both now say "older than 90 days".
    • The Gratz achievement triggers were described as Retail-only. Modules/Gratz/Gratz.lua registers the achievement events on every flavour, so both now say "a version of WoW that has achievements".
    • The README's Gratz and Smack Say/Yell rule, and the page's Smack Emote note, left out that Midnight and Forever block automatic Say/Yell/Emote everywhere (Compat.lua, via Version.lua isRetail120Plus).
    • Bloodlust was listed as working on Classic Era. Modules/Smack/Smack.lua says it never lands there.
    • The gathering summary's sources were described as "the best". They are the three most gathered by item count (GatheringLog.lua).
    • The custom date filter was described as a single day. GUI/DateRangePicker.lua picks a day or a range, labelled "Custom...".
    • Diagnostics "records nothing" when switched off now adds "unless Developer Tools is on".
    • Quest DB was described as a Forever-only tab. It shows on every flavour behind Developer Tools and only works on Forever.
    • Walk recipe sources was described as covering "every known recipe". It covers the TOG profession database and needs ProfessionDB, which both Requirements sections now list as optional.
    • Switching Login Digest off from its tab hides the tab. Both documents now say so.
    • The Addon Load status list now says "include", since unlisted client reasons show raw.

    Added what the code has and neither document mentioned: Settings > Clear Data, the verbose debug switch, the minimap right-click to settings and its per-character hide, the gear icon, Login Digest's Print test digest, Smack's Manual trigger, and the missing slash aliases (versioncheck, db, logs bank / gbank / whisper, plus the README's aliases now copied into the page).

  • Stale in-game help text. The Mailbox help said the threshold "lives in the addon settings panel", but it is the slider on the tab (GUI/MailboxTab.lua). The Logs help still called Trade "coming in a follow-on patch" (GUI/LogsTab.lua). Both Gathering descriptions in Settings left out gas and pickpocketing (GUI/Settings.lua). Two code comments were also out of date.


[v0.9.5] (2026-09-28) - I'd Hit That on WoW Forever; Name Prefix matches a first or last name

Bug Fixes

  • I'd Hit That said "not applicable" on WoW Forever. Player report, 2026-09-28 (screenshot of the tab: "Hit chance was removed as a stat in Warlords of Draenor"). I gated the whole module on gv.isRetail, which is true on Forever because it is the Retail client, and never checked whether Forever has hit. It does: its own character sheet computes melee, ranged and spell hit as GetCombatRatingBonus(CR_HIT_*) + Get*HitModifier() (wow-ui-source-forever Blizzard_UIPanels_Game/Camelot/PaperDollFrameStats.lua:489-496). The engine now asks NO_HIT_STAT = gv.isRetail and not gv.isForever everywhere it used to ask isRetail, and the tab shows its settings on Forever. Forever has no weapon-skill API in its generated docs, so the existing no-skill path is used (Blizzard's miss table, glancing "not known"); ranged hit is read the way Forever's sheet reads it (GetRangedHitModifier + CR_HIT_RANGED, no scope tooltip scan), Forever only. Every one of those calls is SecretWhenUnitStatsRestricted (PlayerScriptDocumentation.lua), so each read goes through stat(): a secret value marks the rebuild unreadable, the previous numbers are put back and the player half stays dirty so the next refresh retries. Retail proper is unchanged (still always-hits, no stat reads). Not verified in a Forever client, including the HUD's placement on Forever's target frame. Location: Modules/IdHitThat/IdHitThat.lua, GUI/IdHitThatTab.lua. Spec: Tests/idhitthat_spec.lua ("WoW Forever", three cases).
  • Name Prefix's "Hide prefix if nickname matches character name" never matched on Forever. Player report, 2026-09-28: character "Vishi Swaz", nickname "Vishi". Forever allows a space in a character name (a first and a last name), and the test was an exact, case-sensitive nick == UnitName("player"). New nickMatchesName matches the whole name or any one word of it (first or last name, operator 2026-09-28), ignoring case; a partial word does not match. The player also suggested per-character checkboxes; not built, as the name match covers the report. Location: Modules/NamePrefix/NamePrefix.lua, GUI/NamePrefixTab.lua (checkbox description). Spec: Tests/nameprefix_spec.lua.

[v0.9.4] (2026-09-28) - Quest DB developer tool; Item DB opens on WoW Forever and walks its whole item range; Profession Capture on Forever

New Features

  • Quest DB developer tool (WoW Forever only). QuestDB inbox thread 76745964: LibQuestDB has no offline source for Forever's own quests (QuestieDB's Forever TOC carries the Vanilla set, and Forever's QuestV2 DB2 has no titles), so this asks the live client. It walks the ids LibQuestDB:GetCoverage answers "uncovered" for, not every non-"shipped" id, because "unknown" spans the whole range. Each quest is requested with C_QuestLog.RequestLoadQuestByID and read on QUEST_DATA_LOAD_RESULT (both confirmed in wow-ui-source-forever QuestLogDocumentation.lua:1219, :1336). The walk is paced by replies, not a ticker: at most 10 requests outstanding, and each reply sends the next. A _filling flag guards re-entry, because the event is declared SynchronousEvent. The cost: a request the server never answers holds its slot. When all ten are held, the tab says "Waiting", and Pause then Fill gaps re-sends them. Saved as db.global.questDB.quests[id] = "title\31level\31groupSize\31elite\31questType\31obj\30obj...\31tip\30tip...", the format QuestDB's generator reads. A field the client does not return is stored empty. Separators are stripped from client text. Off Forever, or without LibQuestDB loaded, the tab says why and nothing runs. The walk's ceiling is the end of LibQuestDB's last coverage run (QuestDB:TopID). I first shipped a fixed 65000 with a comment claiming the ids sat below it, without checking. Forever's coverage runs to 99234 and most of its ids are above 73000. The first in-game walk therefore stopped at cursor 65001, marked itself complete, and saved 64 quests. A store in that state resumes from its cursor on the next Build / Resume; the operator's resumed walk finished at cursor 99235 with 833 quests (QuestDB, thread 76745964). That first walk did show that objectives and tooltip text come back for a quest the player does not hold. Location: Modules/QuestDB/QuestDB.lua, GUI/QuestDBTab.lua, all seven TOCs (QuestDB added to TOGTools_Camelot.toc OptionalDeps), the questDB defaults in TOGTools.lua, and C_QuestLog in .luacheckrc / .luarc.json. Spec: Tests/questdb_spec.lua; ungated exemption recorded in Tests/moduleregistry_spec.lua.

Bug Fixes

  • Item DB tab threw "SetPoint would result in anchor family connection" on open (WoW Forever). ItemDB inbox thread a2110cf1; the operator saw it 3x opening the window from the minimap button. The failing call is AceGUI's Flow layout anchoring the tab's first group to the TabGroup content (AceGUI-3.0.lua:717); the client refuses that when the content already depends on the group. AceGUI pools widget frames for every addon and Release clears only the released frame's own points, so a group another frame still anchors onto can come back from AceGUI:Create. I did not find the stray anchor in TOGTools' own code (every SetPoint onto a widget frame in GUI/ was read), so which addon leaves it is not verified. New UI.AnchorPath(from, target) walks a region's points breadth-first and returns the chain, and UI.CreateUntangled(AceGUI, type, container) refuses a pooled widget the container already depends on: it is hidden, kept out of the pool for the session, and a fresh one is taken; the first time, the chain is printed once in chat so the frame holding the stray anchor is named. The Item DB tab's four groups use it. Not verified in game. Location: GUI/UI.lua, GUI/ItemDBTab.lua. Spec: Tests/uianchor_spec.lua.

  • Profession Capture's "Walk recipe sources" looked like it did nothing. On Forever it walked all 2,511 ProfessionDB recipes, got 0 answers, and said nothing (operator's screenshot, 2026-09-27). Blizzard only calls C_TradeSkillUI.GetRecipeSourceText for a recipe shown in the open profession window, and only for an unlearned one (wow-ui-source-forever Blizzard_ProfessionsRecipeSchematicForm.lua:633-634). A second walk with the trainer and the Blacksmithing window open also answered 0. The operator's screenshot shows why: the window listed only learned recipes, because its "Show unlearned" filter (Blizzard_Professions.lua:1222) was off. retailKnown asks for a source only for an unlearned recipe, so it had nothing to ask about. Changes:

    • Capture never writes the profession window's filters. I first forced "Show learned" and "Show unlearned" on, the way the trainer's are, and that locked the WoW Forever profession window (K) so nothing in it could be clicked (operator, 2026-09-28; it cleared with TOGTools disabled). Cause: Forever's Professions.GenerateCraftingDataProvider forces "Show unlearned" back off every time it rebuilds the list (wow-ui-source-forever Blizzard_ProfessionsTemplates/Camelot/Blizzard_Professions.lua:17-20), each change re-lists and fires TRADE_SKILL_LIST_UPDATE, and my handler turned it on again, so the two fought without end. OpenTradeSkillFilters, RestoreTradeSkillFilters, TradeSkillFiltersOn and the TRADE_SKILL_CLOSE registration are gone; the list the window shows is read as it is.
    • CaptureKnown merges a Retail read into the stored profession, so a narrower list (learned only) keeps the recipes an earlier read listed instead of overwriting them.
    • WalkRecipeSources also counts every source the window capture read. These are keyed by the client's own recipe id, which covers ids ProfessionDB lacks.
    • A walk keeps every source an earlier walk found, and reports answeredNow (what this pass added).
    • The button prints the result in chat.
    • The walk now also records sourceType from C_TradeSkillUI.GetRecipeInfo, labelled with BATTLE_PET_SOURCE_<n>, the same numbers and labels Blizzard's own Sources filter uses (Blizzard_Professions.lua:1255-1272). The operator's SavedVariables show GetRecipeInfo answered for all 2,496 unlearned recipes with no window open, while GetRecipeSourceText answered none. The walk was already calling it and threw that field away. retailKnown keeps sourceType too. A recipe counts as answered when it has either a source type or source text, and is never counted twice.
    • Forever gives no source at all -- measured. The operator's field dump for unlearned recipe 7928 (2026-09-28) lists every GetRecipeInfo key: no sourceType or anything like it, and GetRecipeSourceText returned nil. The forever tree's TradeSkillUIDocumentation.lua has no other source call, only the window's source-type filter, which would mean writing the window's filters (see above). The walk now keeps skillLineAbilityID per recipe (4414 for 7928), the client's SkillLineAbility row, so an offline build can join it to how the recipe is learned, and counts those as walk.linked. A link is not counted as an answer. When nothing answers, the chat line now says the client gives no recipe sources and how many links were saved, instead of printing the sample dump.

    Not verified in game, apart from the Forever field dump above. Location: Modules/ProfCapture/ProfCapture.lua (CaptureKnown, OnEvent, WalkRecipeSources), GUI/ProfCaptureTab.lua. Spec: Tests/profcapture_spec.lua.

  • Item DB walk stopped at ID 240,000, below WoW Forever's items. ItemDB inbox thread 500e4ce2: Forever (build 1.60.1.69913) has 4,862 item IDs above 240,000, up to 286,427. DEFAULT_TOP_ID is now 320,000, and raiseTopID lifts a stored topID below it on every Start and FillGaps, so an existing walk (stored 240,000, cursor 240,001, even marked complete) resumes from its cursor into the new range instead of keeping the old ceiling. FillGaps raises before placing the cursor past the ceiling, so it still skips the walk. The comment claiming the ceiling was "editable via the tab" was wrong (the tab only displays it) and is replaced. Location: Modules/ItemDB/ItemDB.lua. Spec: Tests/itemdb_spec.lua ("walk ceiling").

  • wow-version-replication.ps1 could delete a live file from every replica install. Peer review 259f99e3 (TOGProfessionMaster): a save-by-replace raises Deleted and Created/Changed for one save, in either order, and when Deleted ran last Sync-File removed the file from every other WoW install while the source still had it. A delete is now honoured only when the source file is really gone; otherwise it is copied as a change. Dev-only; the script does not ship. Location: wow-version-replication.ps1 (Sync-File).


[v0.9.3] (2026-09-27) - Gratz: sister-guild level-ups, and no more roster polling

New Features

  • Guild level-up profiles also fire for sister-guild members. Operator request, 2026-09-27. LibGuildRoster (already a required dependency in all seven TOCs) reports sister members' level-ups from its periodic /who sightings, sister roster copies and grouped UNIT_LEVEL as OnSisterMemberLevelChanged(charKey, guildKey, oldLevel, newLevel, source). Gratz routes it through fireGuildLevelUp, so the level band, every-Nth-level interval, cooldown, skip-self and channel rules apply unchanged; [guild] is the sister guild's name (the key's Faction- prefix stripped). The line goes to our guild chat and crosses to the sister guilds through the existing GreenWall relay. Location: Modules/Gratz/Gratz.lua ("Guild level-ups", onMemberLevelChanged, guildNameFromKey).
  • "Sister guilds" checkbox on Level up (guild) profiles. Stored as profile.sisterGuilds; nil means on, so profiles saved before this release get it, and new profiles default to on. Unticked keeps a profile to the home guild. Location: GUI/GratzTab.lua, Gratz:NewProfileDefaults.

Improvements

  • The 60 s guild roster poll is gone. Gratz asked the server for the whole roster every 60 s (C_GuildInfo.GuildRoster) and compared every row while a guild level-up profile was enabled; the operator called that too expensive for one addon. Home-guild dings now come from LibGuildRoster's OnHomeMemberLevelChanged (same signature), which is free: every library user's own PLAYER_LEVEL_UP announced on the guild addon channel ('comm'), plus UNIT_LEVEL on a grouped guildmate ('unit'). GuildRoster searched every flavour tree and the web and found no server message announcing another player's level. Removed: scanGuildLevels, guildLevelUpQualifies, requestGuildRoster, guildTrackerArmed, the GUILD_ROSTER_UPDATE registration and the login ticker. Baseline, online-at-both-readings and cross-source dedupe are now the library's rules (its Tests/sisterlevel_spec.lua); the Gratz specs that pinned them for the old scan went with it. Contracts 2f198123 and 9c178c96.
  • A second guard behind the library's: a decrease, no change or a malformed call says nothing, and a home-guild key arriving on the sister callback is refused, so the two can never gratz one person twice.
  • wow-version-replication.ps1 now replicates into WoW Forever. _classic_beta_ (Battle.net product wow_classic_beta, which reads TOGTools_Camelot.toc) added to the version list, and a destination is kept when the version folder exists rather than its Interface\AddOns folder: a fresh client has none yet, so the old test skipped Forever silently. The AddOns folder is created with the addon folder. Verified with -DryRun (Forever listed as a target) and by the live watcher filling _classic_beta_\Interface\AddOns\TOGTools. Dev-only; the script does not ship.
  • Addon-list title in the brand colours, on all seven TOCs. Fleet request ff41f4c4 (DeltaSync, at the operator's direction). The version tag used |c999999FF, which is alpha-first (a pale blue, not grey), and neither colour code was closed. Now |cffFF8000TOG Tools|r |cff999999<Version>|r. A new Tests/toc_spec.lua case pins the exact title per flavour. Not yet seen in the client's AddOns list. Locations: every .toc, Tests/toc_spec.lua.

Bug Fixes

  • Guild Log read Blizzard's event buffer twice per query on Classic. requestQuery arms a 1.5 s fallback ingest for Retail, where GUILD_EVENT_LOG_UPDATE no longer fires; on Classic the event arrives too, and both ran, so every query re-hashed all 100 buffer entries twice (the identical pairs every ~30 s in the operator's 2026-09-27 debug trace). The event handler now cancels the pending fallback before ingesting. Location: Modules/GuildLog/GuildLog.lua. Specs: Tests/guildlog_spec.lua counts ingest passes on both paths; the Classic spec was run red with the cancel line removed (1 failed) and green with it restored.
  • /togt glcheck wrote to the saved Guild Log. GuildLog:Diagnose's instrumented ingest walk tinserted every new buffer row into the bucket, with no id, unsorted and unpruned, and the next real ingest would not re-sort them (it sorts only when it appends). The walk now counts what the real ingest would append ("would append / deduped") and writes nothing. Location: Modules/GuildLog/GuildLog.lua. Found by the self-audit's coverage pass.
  • One level-up could post twice in the same channel. Since LibGuildRoster MINOR 27 a grouped guildmate's ding reaches the party trigger (Gratz's own UNIT_LEVEL) and the guild trigger (the library's 'unit' source) at once, so a party profile and a guild profile both set to Guild chat said it twice. sendToChannels now keeps one line per (player, level, channel) for the session, first profile wins; profiles aimed at different channels still each post. A repeated identical ding is therefore no longer re-gratzed after its cooldown. Peer review, inbox thread 0f319d11. Location: Modules/Gratz/Gratz.lua (_dingSent, tryFire's dingKey). The spec for profile order and two cooldown specs moved to the achievement trigger, which the rule does not touch.
  • Guild Log line coverage 53% -> 100% (437/437): specs for /togt gldump, /togt glcheck, the default-rank lookup, the login query, Requery, the C_Timer.After fallback and the sub-tab refresh. Gratz.lua 404/404. Full suite 1126 passed, 0 failed (desk, 2026-09-27, after the TOC title spec).

Known limits

  • Needs LibGuildRoster MINOR 27 (GuildRoster 1.1.0), released before this. On an older library neither callback fires and guild level-up gratz go quiet; registering an unfired name is harmless under CallbackHandler. Seen in game 2026-09-27 (Classic Era): a home guildmate's ding to 15 was gratzed in guild chat. Which library source fired it was not captured (the order against GRM's own announcement proves nothing: two pollers on different schedules); with debug on, every callback now prints [gratz] home|sister level-up: ... (source=...) before any filter. Sister-guild dings, the grouped path, the "Sister guilds" checkbox and the no-library-addon re-check are not yet seen in game.
  • The 60 s re-check moved INTO LibGuildRoster, shared by every addon (operator, 2026-09-27, contract 7e447fb6). The library re-reads the home roster about once a minute while any addon holds RequestLevelRefresh, keeps member.level current for every member, and fires OnHomeMemberLevelChanged with source 'roster' under the same rules. Gratz holds it exactly while the module is on and a guild level-up profile is enabled: Gratz:SyncLevelRefresh, called from OnEnable, OnModuleToggled, SaveProfile and DeleteProfile (the tab's form and "On" checkbox both go through SaveProfile). Feature-detected, so an older library is a no-op. So a guildmate who runs no library addon is still seen, as the old poller saw them, at one request per client however many addons ask.
  • Sister dings can be minutes late (the library's /who cadence), or missed if the member logs off first. Seen in game 2026-09-27: a sister member's ding to 40 was never reported, because no reading of them preceded it (a first reading is a baseline). LibGuildRoster MINOR 27 now forwards a player's own ding to the sister guilds through GreenWallAPI.SendMessage (version 2) and fires OnSisterMemberLevelChanged source 'comm' on receipt (contract 58cbd95c), so a sister member running the standalone GuildRoster addon plus GreenWall 1.14.0+ is gratzed at once. Not verified in a client.
  • Sister gratz volume is untested. Every TOGTools user with a guild level-up profile gratzes a sister-guild ding, and existing profiles have "Sister guilds" on. Peer review (thread 0f319d11) asks for one in-game check with two TOGTools users online; the player notes say it can be switched off.

[v0.9.2] (2026-09-25) - Addon Load names the missing dependency and shows each addon's .toc

New Features

  • "Dep Missing" now names the dependency. A row the client refuses with a DEP_* reason reads Dep Missing: LibX, LibY, listing every required dependency that is not installed or will not load. Until now the tab only said that something was missing. AddonLoad:GetData reads each addon's ## Dependencies through C_AddOns.GetAddOnDependencies (the call Blizzard's own AddOn list tooltip uses, Blizzard_AddOnList/AddonList.lua:849 in the Era tree) and resolves each one: C_AddOns.DoesAddOnExist false -> "Not installed"; IsAddOnLoaded -> "Loaded"; otherwise the client's reason from GetAddOnInfo(dep) (Disabled, Wrong Version, ...). A dependency that is installed, loadable and not yet loaded (load-on-demand) counts as fine. Location: Modules/AddonLoad/AddonLoad.lua (depState, collectDeps).
  • Hover a row to see its .toc. An addon cannot open a .toc file, so the tooltip reads it back through the client: title, folder name, ## Notes, status, ## Version and ## Author (C_AddOns.GetAddOnMetadata), the interface number (C_AddOns.GetAddOnInterfaceVersion), and the required and optional dependency lists (GetAddOnOptionalDependencies), each dependency coloured green or red with its state. Wired through RowList's onRowEnter/onRowLeave, which the translator passes to LibAceGUIWidgets unchanged. Location: GUI/AddonLoadTab.lua (showRowTooltip).

Improvements

  • Every new API is feature-detected on C_AddOns. A flavour without the dependency calls shows the bare "Dep Missing" label and an empty list, never an error. GetAddOnInfo on a dependency name is pcall'd: whether it raises or returns for an uninstalled name is not verified on every flavour, so a raise, or a MISSING reason, reads as "Not installed".
  • Sort priority keys on the bare label, so Dep Missing: LibX still sorts with the failures.
  • Tests/addonload_spec.lua covers the module at 149/149 lines. Eleven dependency specs, plus three for paths no spec had run before: the module's own ADDON_LOADED/PLAYER_LOGIN timing capture used when !TOGT is absent, and RefreshMemory. Not verified in a client.

[v0.9.1] (2026-09-25) - LibGraph-2.0 and LibDBIcon-1.0 become required dependencies instead of embedded copies

Improvements

  • LibGraph-2.0 is now the standalone "LibGraph-2.0 Revived" addon, not a vendored file. All seven TOCs add LibGraph-2.0 to ## Dependencies and stop loading libs\LibGraph-2.0.lua, which is deleted. .pkgmeta adds libgraph-2-0-revived to required-dependencies, so CurseForge installs it alongside TOGTools; that slug is the one FastGuildInvite already ships with. The TOC line decides whether TOGTools loads and the .pkgmeta line decides what gets installed, so both are needed.
  • Why the embedded copy was wrong, not just duplicated. LibGraph finds its line and bar textures by parsing debugstack for the directory its .lua sits in, and expects the .tga files beside it (LibGraph-2.0/LibGraph-2.0/*.tga in the standalone addon). TOGTools shipped the .lua flat in libs\ with no textures at all. The standalone copy also carries the maintained fixes and a minor (92069) that out-ranks older embedded copies. GUI/LogGraph.lua reads the library through LibStub("LibGraph-2.0") at file scope and needs no code change.
  • LibDBIcon-1.0 (and the LibDataBroker-1.1 it carries) is a required dependency on six of seven flavours. The Era, TBC, Wrath, Cata, Mists and Mainline TOCs add LibDBIcon-1.0 to ## Dependencies, drop the ## OptionalDeps: LibDataBroker-1.1, LibDBIcon-1.0 line and stop loading libs\LibDataBroker-1.1.lua / libs\LibDBIcon-1.0.lua; .pkgmeta adds libdbicon-1-0 to required-dependencies. The standalone addon (Funkeh, v12.0.3) loads LibStub, CallbackHandler-1.0 and LibDataBroker-1.1 from its own embeds.xml, so one dependency covers both libraries GUI/MinimapButton.lua asks LibStub for. Why: an embedded copy only reaches the players of the addon that shipped it, so every LibDBIcon update meant a release of every addon with a minimap button; as a CurseForge dependency it updates without one. Kept against: the embedded copy's zero install cost. TOGTools_Camelot.toc still embeds both files, because the standalone's TOC lists no 16001 and an out-of-date required dependency would keep TOGTools from loading on WoW Forever (inferred from how the client treats out-of-date addons, not observed on a Forever client). The embedded copy was already MINOR 56, the same as upstream v12.0.3, so it was not refreshed.
  • .luarc.json (TOGTools and !!TOGT) sets diagnostics.libraryFiles and diagnostics.ignoredFiles to Disable, so vendored code in libs\ stops filling the Problems panel whenever one of its files is open in a tab. TOGTools' workspace.library also points at the sibling ../LibGraph-2.0/LibGraph-2.0 folder so LogGraph.lua keeps its type information.
  • GUI/MinimapButton.lua: the unused _self argument of the LDB OnClick is now _.
  • Tests/toc_spec.lua: INTENTIONAL_OMISSIONS lists libs\LibDataBroker-1.1.lua and libs\LibDBIcon-1.0.lua for every flavour except Forever, so the cross-flavour parity check pins the Forever-only embed instead of failing on it.
  • Docs: README.md and docs/Curseforge_Description.html list LibGraph-2.0 and LibDBIcon-1.0 under Requirements (LibDBIcon auto-installed everywhere but Forever, which keeps a built-in copy). They no longer call LibDataBroker/LibDBIcon optional. Smack's big-hit trigger now notes environmental damage, a detail kept from the v0.8.6 notes that roll off Recent Updates.

[v0.9.0] (2026-09-24) - Profession Capture developer tool; every table now drawn by LibAceGUIWidgets

New Features

  • Profession Capture (developer tool). Built for ProfessionDB (LibProfessionDB-1.0), inbox contract e2868011. The client's DBC says which recipes exist; only a live client standing at the NPC can say which ones a player can obtain, and where. A walker turns on Capture in the new Profession Capture tab (Developer Tools only), visits trainers and recipe vendors and opens their own profession windows. The SavedVariables go back to ProfessionDB. New Modules/ProfCapture/ProfCapture.lua and GUI/ProfCaptureTab.lua, listed in all seven TOCs. Passive: it reads on window events (TRAINER_SHOW / TRAINER_UPDATE, MERCHANT_SHOW / MERCHANT_UPDATE, TRADE_SKILL_SHOW / TRADE_SKILL_UPDATE / TRADE_SKILL_LIST_UPDATE, CRAFT_SHOW / CRAFT_UPDATE, LOOT_OPENED) and sends no request of its own. Gated three ways: devTools, the module toggle (IsModuleEnabled("profcapture")), and profCapture.capture, which defaults off. enabled stays on by default because MainWindow hides a disabled module's tab, and that would hide the switch.

    • Two API shapes, by gameVersion.isRetail. Read from the Blizzard trees under F:\Blizzard API Docs. On the Retail client, including WoW Forever, GetTrainerServiceInfo returns name, type, texture, reqLevel, subText, category (wow-ui-source-forever Blizzard_TrainerUI/Mainline/Blizzard_TrainerUI.lua:425), and the spell id comes from C_TooltipInfo.GetTrainerService(i).id. On every Classic client it returns name, subText, type, isExpanded with header rows. That order is the same in the classic_era, classic_anniversary and classic (Mists) trees. The spell id comes from a hidden GameTooltip's SetTrainerService + GetSpell. Vendors use C_MerchantFrame.GetItemInfo or GetMerchantItemInfo. Recipe lists use C_TradeSkillUI (GetFilteredRecipeIDs, GetRecipeInfo, GetRecipeSchematic) or GetTradeSkillInfo / GetCraftInfo.
    • The trainer filter. On show, every service filter (available, unavailable, used) is turned on and the player's own is saved; TRAINER_CLOSED puts it back. A read is kept only when all three are on, so a filtered list never overwrites a complete one. If Blizzard's trainer frame re-applies its own filter after TRAINER_SHOW, the next TRAINER_UPDATE turns ours back on and the re-list that follows is read. There is no timer.
    • Not verified in a client. Nothing here has run in game. Unverified points: that SetTrainerServiceTypeFilter still applies after TRAINER_CLOSED; what an enchant's GetCraftItemLink carries on Classic Era (Era's craft frame calls only GetCraftItemLink, never GetCraftRecipeLink -- Blizzard_CraftUI/Vanilla/Blizzard_CraftUI.xml:571-574 -- so the spell id is read from it as a fallback, and a recipe with neither lands in unkeyed; Era's trade-skill frame does call GetTradeSkillRecipeLink, Blizzard_TradeSkillUI.xml:11); and that GetFilteredRecipeIDs returns every row while the player's profession-window filters are set. It honours those filters, so a filtered list is a partial list.
  • Recipe-source walk: the NPC-free route, Retail client only (WoW Forever included). A Walk recipe sources button in the tab. ProfCapture:WalkRecipeSources() takes every recipe spell id LibProfessionDB ships (GetProfessions / GetRecipes). For each one it asks C_TradeSkillUI.GetRecipeSourceText(id): the text the Professions window shows under an unlearned recipe (Blizzard_ProfessionsRecipeSchematicForm.lua:632-638 in the forever and live trees). The pass is synchronous: local calls, no timer and no travelling. Result: profCapture.walk = { recipes = { [spellID] = { prof, learned?, source? } }, total, answered, build, locale, ts }. The same text is also recorded during a recipe-list capture, as source on an unlearned recipe and as nextRecipeID + nextRankSource on a learned one. ProfessionDB is an OptionalDep of the Camelot and Mainline TOCs so it loads first. No Classic client has this call. The writ blizzard-api index finds it only in the forever and live trees, and the classic_era TradeSkillUIDocumentation.lua has no recipe-info or source function. So Classic has no walk. Not verified in a client: whether the call answers for a profession the walking character lacks, or with no profession window open. The walk's answered count settles that on the first click. Specs: 5 more examples, covering an unsupported client, ProfessionDB missing, no store, a full walk with counts and purge, and the source text in a recipe-list capture. The Profession Capture tab shows the walk's "answered" line only on a client that has the call, since "0 of 0" on Classic read as a failed walk.

  • SavedVariables shape, schema 1 (TOGTools_DB.global.profCapture), for ProfessionDB's parser. Every record carries build ("<version>.<build>" from GetBuildInfo, e.g. 1.60.1.69977), locale and ts (server time). Fields shown ? may be absent.

    profCapture = {
        enabled = true, capture = false,
        trainers = { [npcID] = {
            npcID, name?, zone?, subzone?, mapID?, faction?, trainerType?, rank?, maxRank?,
            collapsedHeaders?,   -- Classic only: how many headers were collapsed (rows hidden)
            seen,                -- how many complete reads this NPC has had
            build, locale, ts,
            services = { {       -- array, in the trainer's own order
                spellID?, name, subText?, category?, type,   -- "available"|"unavailable"|"used"
                reqLevel?, skill?, skillRank?, cost?, abilities? = { "<ability name>", ... },
            }, ... },
        } },
        vendors = { [npcID] = {
            npcID, name?, zone?, subzone?, mapID?, faction?, seen, build, locale, ts,
            recipes = { [itemID] = {   -- Recipe-class (9) items only
                name?, price?, stack?, numAvailable?,   -- nil = unlimited stock
                extendedCost?,
            } },
        } },
        known = { ["Name-Realm"] = { [professionID or profession name] = {
            name, skill, maxSkill, professionID?,          -- professionID on Retail
            childProfessionID?, childSkill?, childMaxSkill?,
            collapsedHeaders?, build, locale, ts,
            recipes = { [spellID] = {
                name, learned, difficulty?,   -- "optimal"|"medium"|"easy"|"trivial"
                itemID?, minMade?, maxMade?, numSkillUps?, categoryID?,
                source?,                             -- Retail: unlearned recipe's source text
                nextRecipeID?, nextRankSource?,      -- Retail: learned recipe's next rank
                reagents = { { itemID, count }, ... },
            } },
            unkeyed? = { <recipe as above>, ... },   -- Classic rows with no readable spell id
        } } },
        drops = { ["<itemID>:<sourceKind>:<sourceID>"] = {
            itemID, sourceKind?, sourceID?,   -- from the loot source GUID, e.g. "Creature", 570
            count, first, zone?, subzone?, mapID?, build, locale, ts,   -- ts = last seen
        } },
        walk? = {   -- Retail only; the latest Walk recipe sources pass, replaced whole
            total, answered, build, locale, ts,
            recipes = { [spellID] = { prof, learned?, source? } },
        },
    }
    

    Two limits the parser should know. A trainer or vendor record is replaced whole on each complete read, so it holds the latest visit rather than a merge. Service type belongs to the character who opened the trainer. On Classic a service whose tooltip yields no spell id (Classic Era's GetSpell on a trainer-service tooltip is not verified) is still recorded, without spellID: a raising SetTrainerService or GetSpell is caught. Specs: Tests/profcapture_spec.lua, 34 examples covering both API shapes, the filter handling, the gate, a failing spell-id tooltip, vendors, both recipe frames, drops, the walk, and the event wiring. Full suite: 1075 passed, 0 failed; ProfCapture.lua 376/376 lines covered.

Improvements

  • Every list now draws through LibAceGUIWidgets. The eleven tabs' tables (the six logs, Diagnostics, Item DB, Addon Load, Gratz, Smack) are drawn by LibAceGUIWidgets-1.0's RowList, MINOR 34. The library added what they relied on at TOGTools' request (inbox thread c571dee6):

    • the header sort/filter menu, with cascading filters, filterGroup and a cap;
    • SetSearch, autoFit and hyperlinks in cells;
    • Detach for AceGUI's recycled parents;
    • col.align, col.gapBefore and col.sortDescDefault.

    GUI/RowList.lua went from ~1100 lines to a translator that maps the options the tabs were written against onto the library's, so no tab changed:

    • format(entry) becomes format(value, entry);
    • sortKey becomes sortValue;
    • a RIGHT/CENTER justify becomes align;
    • headerTipDesc becomes the library's headerTip;
    • tiebreakKey becomes a tiebreak(a, b) that checks name, then the key, then falls through to SetData order.

    addon.UI.OpenColumnMenu in GUI/UI.lua is removed, because nothing else called it. LibAceGUIWidgets is a hard dependency in all seven TOCs (Tests/toc_spec.lua pins it). Release order: this version must not ship before LibAceGUIWidgets MINOR 34 is published. The operator has settled that it is released first. As a backstop, RowList.LibraryIsCurrent() checks LibStub.minors['LibAceGUIWidgets-1.0'] >= 34. On an older library the first list built prints one chat line saying LibAceGUIWidgets is out of date, instead of silently drawing broken tables.

    Hyperlinks are opt-in per list, not on for every list. A link click also runs the row's own OnMouseDown, so a list with onRowClick could open its row menu and the chat-name menu together (Peer Review, audit thread cf6cc085). The six lists whose cells render item or player links pass hyperlinks = true: Gathering, Guild Bank, Mail, Trade, Whisper and Item DB. Gratz, Smack, Diagnostics, Addon Load and Guild Log do not. The Gathering Log's sortType = "date" option was removed; nothing reads it now.

    Visible differences:

    • headers are the library's gold rather than TOGTools' orange, because the accent is session-global and TOGTools does not set it;
    • the menu reads "Sort ascending/descending" rather than "Oldest > Newest";
    • sorting is numeric-aware and case-insensitive;
    • a search also scrolls the list to the top.

    Specs: Tests/rowlist_spec.lua is rewritten. It pins every translation rule, and it runs the old tiebreak guarantees (findings rev-B and rev-J) end to end through the real library. A formatter's nil still draws as a blank cell rather than "nil" (the library tostrings the result, and the translator maps nil to ""). Full suite: 1075 passed, 0 failed, against LibAceGUIWidgets' working tree as it stood after Peer Review's fixes (thread af2ebd61); GUI/RowList.lua 56/56 lines covered. Not verified in a client: nothing has been drawn in game on LibAceGUIWidgets MINOR 34 yet.

  • The seven TOCs read as four blocks. Libraries, core, shared GUI and modules now sit under # comment headings. The modules block no longer has a blank line between each engine and its tab. Load order is unchanged (Tests/toc_spec.lua pins it across all seven).

  • Test harness pin moved to WoWAPITesting 3d492c7 (from 7932484, ten days of Adoption-log entries). Tests/itemdb_spec.lua drops its own strsplit stand-in, because the harness's strsplit now keeps empty fields and honours the piece limit as the client does. The spec-local IsInGroup / UnitInParty / UnitHealth / CombatLogGetCurrentEventInfo assignments in the Gratz, Smack and IdHitThat specs stay: they drive per-test scenarios, not a missing API. Full suite on the new pin: 1075 passed, 0 failed.

  • Retail TOC declares 110207, 120100 (was 110207, 120008). LibAceGUIWidgets is now a hard dependency, so every Interface number TOGTools declares must be one the library declares too, or a client on that build skips the library as out of date and TOGTools with it. The library lists 120100 and never listed 120008; LibAceGUIWidgets found 120008 in no TOC but TOGTools' own and no 12.0.8 patch anywhere, with 12.1 (120100) the current Retail patch (inbox thread c571dee6). Tests/toc_spec.lua still pins one 11.x floor and exactly one 12.x build. Not verified: this box does not record the Retail client's build, so 120100 is the library's evidence, not a reading of the installed client.

Documentation

  • README and CurseForge description brought to parity. Both now describe the same features in the same detail. The README gains what only the CurseForge page had: the Name Prefix GRM duplicate-name toggle, the per-log details (Take-All merge, C.O.D., trade enchant slot, guild and bank history kept past the game's own window, date presets), Gratz placeholders and cooldown, Smack's channels and cooldown, I'd Hit That's layout options, and the Addon Load status badges. The CurseForge page gains what only the README had: a Settings section and the Developer Tools commands. Both gain a Developer Tools section (Diagnostics, Item DB, Profession Capture) and list LibAceGUIWidgets v0.2.4 (MINOR 34) under Requirements.

[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.

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