# Changelog

## [v0.8.4] (2026-08-18) - The audit release: a Retail crash, and ~60 defects found by reading every module line by line

### New Features

- **The Gathering log now has the stats panel its own tooltip already promised.** `ComputeStats` had
  been written and specced to 100% and never given a caller, so the Gathering sub-tab showed a table
  and no summary at all -- while the Source column's tooltip told users *"The stats panel above breaks
  down yield per source."* A user-visible claim with nothing behind it, and the reason audit finding L
  (two same-name ore tiers merging) was impossible to see in game. The panel sits above the table and
  reads the same filtered set the table does, so every figure honours the active date range and column
  filters: total items, nodes worked, average yield per node, items/hour, top zone, and per-source
  yield for the top three sources. Both divisions are guarded -- a set with no action ids reports zero
  nodes rather than dividing by them, and a set spanning under an hour reports no items/hour.
  Location: `GUI/GatheringLogSubTab.lua`.

- **`GuildBankLog:InjectEntry` is now a published, safe API for another addon to write through.**
  The user settled the TOGBank integration direction on 2026-08-18: **TOGBankClassic pushes** into
  this hook, rather than TOGTools reading their SavedVariables -- which would have coupled us to a
  schema `GUILD_STORE.md` says is mid-overhaul. Classic Era has no real guild bank, so TOGTools'
  native capture produces nothing there and the sub-tab is empty by construction; TOGBankClassic is
  what actually knows what moved. The receiving end is complete and the work is now entirely on
  their side, written up as `docs/DEPENDENCY_CONTRACTS.md` section 1 with the exact entry shape,
  every return code, and what TOGTools does and does not guarantee. Two changes were needed to make
  it safe for a caller we do not own: **the caller's table is now copied, never stored** -- a
  producer may hand us a scratch table it reuses in a loop, and storing the reference let another
  addon silently rewrite rows already in the log -- and it **returns `(stored, reason)`** so a
  producer can tell `"stored"` from `"duplicate"` from `"disabled"`, the last meaning the user has
  switched the log off and it must not be retried. Six specs, and reverting the copy shows the
  stored row taking the caller's later value. Location: `Modules/GuildBankLog/GuildBankLog.lua:1112`,
  `docs/DEPENDENCY_CONTRACTS.md`.

  **The first draft of that contract named the wrong address twice** -- `TOGTools.modules`
  `["guildbanklog"]`, when the log actually registers in `logCategories` under `"guildbank"` and the
  reachable global is `TOGTools.addon`. A guarded call in another addon would simply have stopped
  finding us, silently. Caught by checking the source rather than shipping it, and the published
  address is now pinned by a spec so a rename cannot break the contract quietly.

### Bug Fixes

- **Smack had the same three defects as Gratz, in its own copy of the same code.** Reading
  `Modules/Smack/Smack.lua` for the first time turned up no new kinds of defect -- it turned up
  `tog-AQ`, `tog-AR` and `tog-AS` again, live in the twin, filed as `tog-AT` / `tog-AU` / `tog-AV`.
  The Raid and Instance Chat options tested the wrong kind of group in exactly the same way; a line
  that reached no channel was still counted and still burned the cooldown, which here is configured
  in **minutes** and which also consumed the threshold crossing, so the filter had to re-cross before
  it could ever be heard; and filters ran in hash order while the tab sorted by name. **The channel
  predicate was MOVED rather than fixed twice** -- `addon.Chat.ChannelAvailable` now lives in
  `Compat.lua` and both engines call it, so breaking that one function fails three specs in each
  module's suite, which is the proof the two can no longer drift. This pair had already diverged
  three times; fixing Gratz alone would have queued up the fourth. `fireFilter` returns `false` for
  exactly one reason -- "reached no channel" -- so every other bail still consumes the crossing
  exactly as before and cooldown behaviour is unchanged. `ApplyHotkeys` also sorted: when two filters
  claim the same key, last-writer-wins meant **which one that key fired varied between sessions**
  with nothing the user changed. Location: `Modules/Smack/Smack.lua`, `Compat.lua`.

- **Gratz: the Raid and Instance Chat channel options did not test the right kind of group.** One
  bare `IsInGroup()` covered `PARTY`, `RAID` and `INSTANCE_CHAT`, and it is only the correct predicate
  for the first. Blizzard lists all four in one place -- `ChatFrameEditBoxMixin:ResetChatType`, Classic
  Era `Blizzard_ChatFrameBase/Shared/ChatFrameEditBox.lua:562-573`: `PARTY` is
  `IsInGroup(LE_PARTY_CATEGORY_HOME)`, `RAID` is `IsInRaid(...)`, and `INSTANCE_CHAT` is
  `IsInGroup(LE_PARTY_CATEGORY_INSTANCE)`, a **different category**. The two wrong ones failed in
  opposite directions, which is why neither surfaced: `RAID` was true in any five-man, so a profile
  with raid ticked tried to speak into a raid that did not exist; `INSTANCE_CHAT` was **suppressed**
  in an LFG or instance group with no home party -- the exact case that channel exists for -- and
  attempted from an ordinary party outside an instance. Now a `channelAvailable` helper carrying
  Blizzard's own table. If `LE_PARTY_CATEGORY_INSTANCE` were ever absent, `IsInGroup(nil)` falls back
  to the home category, i.e. exactly the behaviour being replaced, so the degradation is the old bug
  and never an error. Audit finding tog-AQ. Location: `Modules/Gratz/Gratz.lua`.

- **Gratz: a congratulation that reached nobody was counted as sent, and blocked the real one.**
  `tryFire` incremented the profile's fire count and armed the cooldown regardless of whether
  `sendToChannels` actually emitted anything -- and it legitimately emits nothing when every ticked
  channel is unavailable (a guild-only profile on a guildless character, a raid-only profile in a
  five-man). Two costs: the tab's Count column, documented as "Total times this profile has fired
  since creation", was counting attempts; and because the cooldown key is per level, a swallowed
  attempt **blocked the genuine gratz for the same ding** -- join a guild seconds after a guildmate
  hits 40 and it stayed silent for the next five minutes. `sendToChannels` now reports whether
  anything went out and `tryFire` counts only then. A message queued for the end of combat still
  counts, since that is the same promise deferred. Audit finding tog-AR. Location:
  `Modules/Gratz/Gratz.lua`.

- **Gratz: two profiles on the same trigger emitted their messages in an unpredictable order.**
  `eachActiveProfile` walked the profile table with `pairs`, so which of two matching profiles spoke
  first was hash order and could differ between identical events -- while `GetProfiles` and the tab
  both sort by name, so the engine and the UI disagreed about the same list. Now sorted by name
  before dispatch, which is a total order because profiles are keyed by name. Driven red with six
  profiles rather than two: the mutated build returns `Zeta, Kappa, Tau, Mu, Alpha, Beta`, and a
  two-element test would have passed by coincidence half the time. Audit finding tog-AS. Location:
  `Modules/Gratz/Gratz.lua`.

- **Mail Log: a batch of identical auction payouts could be logged with the money counted twice.**
  `_receiveCache` is keyed by `MailIndex`, and taking a mail deletes it and shifts every mail below it
  up one -- so the next physical mail arrives at the index just vacated. The composite identity check
  (`sender`, `subject`, `firstLink`, `daysLeft`) is what stops the newcomer merging into its
  predecessor's row, and **two mails can be equal on all four**: auction payouts settling in the same
  tick share a sender, a subject, a `daysLeft` computed from one arrival time, and a `nil` `firstLink`
  because money-only mail has no attachment. The file's own comment named `firstLink` as the escape
  from exactly this collision, but `nil == nil`. The merge itself was survivable; what it broke is the
  dedupe counter shared with the TradeSkillMaster snapshot path, which increments once per row
  **created** and decrements once per mail **vanished**. A merge creates no row, so the counts drifted
  apart and the snapshot path logged that mail a second time -- two 500-copper payouts recorded as
  1500. `appendReceive` now takes the attachment slot it is delivering and a side table records which
  slots (and whether the money) a cached row has already consumed; within one physical mail each is
  taken exactly once, so a repeat is proof of a different mail and blocks the merge whatever the
  identity fields say. The bookkeeping is kept out of the stored entry deliberately -- that object goes
  into SavedVariables. Audit finding tog-AO. Location: `Modules/MailLog/MailLog.lua`.

  **The take-hook path had no test coverage at all.** Every existing receive spec drove the snapshot
  path, which only arms for TradeSkillMaster users; the path that runs for everyone else was untested.
  Six specs added, and with the guard removed three go red -- the end-to-end one reporting 1500 copper
  logged for 1000 received. The positive control (money plus two attachment slots of one mail still
  collapsing into a single row) stays green, so the guard is not simply refusing every merge.

- **Mail Log: a C.O.D. mail you sent was logged as an empty envelope.** `commitSend` hardcoded
  `cod = false` and stored `GetSendMailMoney()`, but money and C.O.D. are separate amounts and are
  mutually exclusive on send -- Blizzard's send button zeroes both, then feeds the one money input box
  to `SetSendMailMoney` **or** `SetSendMailCOD` depending on the radio button
  (`Blizzard_UIPanels_Game/Classic/MailFrame.lua:844-861`). So the charge was captured in no field at
  all and the Money column rendered a dash: a 50g C.O.D. was indistinguishable from an empty mail. The
  `SendMail` hook now captures `GetSendMailCOD()` alongside the rest of the staged state, where both
  values are still readable. It is **not** folded into `money` -- a C.O.D. charges the recipient and
  pays the sender later in a separate C.O.D. Payment mail this log already captures, so adding it there
  would count the same coins twice. The receive side was reading the amount out of the mail header to
  compute its flag and then discarding the number; both directions now store it. The Money column shows
  the charge tagged `COD`, which is the first time that flag has been rendered at all. Sorting is still
  on enclosed money, so C.O.D. rows sort as zero -- said in the column tooltip rather than left to be
  discovered. Audit finding tog-AP. Location: `Modules/MailLog/MailLog.lua`, `GUI/MailLogSubTab.lua`.

- **Retail: `attempt to perform arithmetic on field 'hit' (a secret number value)`, 30x per target
  swap.** `IdHitThat:Recalculate` rebuilt the entire player half **before** its Retail early-out, so
  on a flavour where the result is discarded outright it still called
  `GetCombatRatingBonus(CR_HIT_MELEE)` and added the answer to `_player.hit`. Retail hands back a
  **secret number** from stat APIs once execution is tainted by an addon, and arithmetic on a secret
  value raises rather than returning a wrong number. `rebuildPlayer` now returns immediately when
  `gv.isRetail`: the whole expensive half was dead work there, since `Recalculate` answers 100 from
  a table without reading a single `_player` field. Location:
  `Modules/IdHitThat/IdHitThat.lua:150`.

- **Turning I'd Hit That off in settings did not turn it off.** The `enabled` flag was read in
  exactly one place, `IdHitThatHUD.lua:605`, which decides whether to *draw*. `Refresh`, reached
  from `PLAYER_TARGET_CHANGED`, `UNIT_AURA`, `UNIT_INVENTORY_CHANGED` and four more, recalculated
  unconditionally, so a disabled module kept running its engine on every target change and every
  aura tick. On Retail that is why the crash above fired for players who had switched the module
  **off**. `Refresh` now checks the flag itself and skips `Recalculate`; it still calls `UpdateHUD`,
  which is what hides a HUD that was on screen when the toggle flipped. Location:
  `Modules/IdHitThat/IdHitThat.lua:407`.

  **Both were verified by mutation, not by a green suite.** The existing Retail spec passed
  throughout the defect: it only ever read the 100s the early-out writes, never whether the rating
  APIs were touched. Five specs were added and the fixes then reverted one at a time. Three go red,
  and the fourth is annotated in place saying it does *not* guard the gate but pins that the gate
  did not lose the hide path. Suite 520 to 525, 0 failed.

- **Our tooltips stretched every other addon's tooltip.** `_G.GameTooltip` is ONE frame shared with
  Blizzard, ATT, TSM and everyone else, and a line appended without the wrap flag ignores the
  engine's preset width and stretches the frame -- dragging their content out with it. The flag
  defaults to false, so omitting it is the failure mode and nothing errors. Fixed at **seven** call
  sites across `GUI/MinimapButton.lua`, `GUI/MainWindow.lua`, `GUI/LogGraph.lua`, `GUI/UI.lua`,
  `GUI/RowList.lua`, `GUI/IdHitThatHUD.lua` and `Modules/Diagnostics/Diagnostics.lua`.

  **The two arities differ and that is where this went wrong**: `AddLine(text, r, g, b, wrapText)`
  puts wrap fifth, while `SetText(text, r, g, b, alpha, wrap)` puts it sixth -- so the four `SetText`
  sites needed an explicit alpha to reach it. Both confirmed in the Classic Era client tree, not from
  memory: `FrameAPITooltipDocumentation.lua:62-73` declares `SetText`'s `wrap` with `Default = false`,
  and Blizzard's own `PaperDollFrame.lua:1571` shows `AddLine`'s five-argument form.

  **`AddDoubleLine` is deliberately left alone: it has no wrap parameter.** Every Blizzard call site
  in that tree tops out at eight text/colour arguments and the generated docs do not declare the
  method at all, so a double line always sizes to left + gap + right. `GUI/LogGraph.lua` carries a
  comment saying so, since the only control there is keeping both halves short.

- **Our help tooltip pinned every tooltip in the game to a 360px floor until logout.**
  `GUI/MainWindow.lua:228` called `GameTooltip:SetMinimumWidth(360)` and nothing ever restored it.
  `GameTooltip_OnHide` clears money frames, status bars and the backdrop -- and **not** the minimum
  width -- so the floor survived on the shared frame and every later tooltip in the session, Blizzard's
  included, inherited it. A setter with no matching reset cannot be noticed from inside the addon,
  which is why this needed the client source to spot rather than testing.

  **Fixed with Blizzard's own pair, not an invented one**: `Blizzard_TalentUI.lua` sets
  `SetMinimumWidth(300, true)` in `SpecButton_OnEnter` and clears it with `SetMinimumWidth(0, false)`
  in `SpecButton_OnLeave`. The reset now lives in **`addon.Tooltip.Release()`** (`GUI/UI.lua`) and is
  wired into `addon.UI.MakeIcon`'s `OnLeave` -- including when a caller supplies its own `onLeave`,
  which is wrapped so the reset still runs. Putting it in the shared helper rather than at the one
  call site is the point: an `onEnter` is free to set a floor, and this is the only version of the
  rule that stays true as icons get added. Every remaining bare `GameTooltip:Hide()` in
  `GUI/LogGraph.lua`, `GUI/RowList.lua`, `GUI/IdHitThatHUD.lua` and `GUI/UI.lua` now goes through it
  too, so the teardown path is uniform.

- **An em dash was shipping into a game tooltip.** `Modules/Diagnostics/Diagnostics.lua` titled its
  broker tooltip `TOG Tools -- Diagnostics`; the client's fonts stop at Latin-1, so that character
  renders as a box. Now a plain hyphen. `GUI/MainWindow.lua`'s two bottom-row icon lines had the
  same character and are fixed with it.

  **Not swept further on purpose, because the evidence conflicts.** `SlashCommands.lua` prints ~18
  `Ace:Print` lines containing em dashes (the whole `/togt help` block, plus several status
  messages), and those have shipped for a long time without a report. Either nobody has looked
  closely at `/togt help`, or the client renders U+2014 fine and the Latin-1 rule is stricter than
  the client is. Mass-editing 18 user-visible lines on an unverified premise is the wrong move, so
  they are left alone and the question is on the todo list to settle by looking at the chat frame.

  **SETTLED, AND THE PREMISE WAS WRONG: `/togt help` was run in a live client and every em dash
  renders correctly.** So the three strings above were **not** defects, and describing them as "an em
  dash shipping into a game tooltip" overstates a lint rule as a user-visible bug. The strings are
  left hyphenated for now only because writing U+2014 back into a `.lua` file is refused by tooling;
  reverting them is a pending decision, not a fix. **Not sweeping the other 18 was the right call.**

- **A disabled module could still hijack your keybind, and a disabled Gratz could still speak.** Two
  defects of one shape, found by a peer-review sweep of every module's enabled flag: **the toggle
  covered the decision to act and not the acting.**

  - **Smack kept its hotkeys bound after you switched it off.** `ApplyHotkeys` installed override
    bindings with no enabled check. Firing was already gated, so a disabled Smack never spoke -- what
    it did was keep **swallowing a keypress** you had rebound to something else, with no way back
    short of a `/reload`. Location: `Modules/Smack/Smack.lua`.
  - **Gratz sent a queued congratulation even if you turned it off mid-fight.** Messages composed in
    combat are queued by design and flushed when combat ends, but the flush was ungated and nothing
    wiped the queue -- so switching the module off during the fight that queued one did not stop it,
    and the queue then survived the toggle indefinitely. It now discards rather than holds: a
    congratulation is worthless once the moment has passed, and holding it would replay a stale one
    on re-enable. Location: `Modules/Gratz/Gratz.lua`.

- **The module toggle now tells modules it moved.** It wrote the flag and notified nobody -- a level
  with no edge. Polling a level is correct for a per-event decision, because there *is* an
  every-event; it can never be correct for **installed state** like a keybind, a chat filter or a
  debounce timestamp, which is set once and persists. `GUI/Settings.lua` now calls
  `mod:OnModuleToggled(val)` when a module exposes one (optional by design), which is what lets Smack
  release its bindings on disable and restore them on enable. The idiom already existed in the addon
  -- `NamePrefix:ApplyDedupSetting` installs or removes and is idempotent; two call sites simply were
  not using it.

  Both fixes were **driven red before being made**: the specs' binding APIs were no-op stubs, so
  every assertion that could have caught the Smack defect was unwritable rather than unwritten.

- **The deterministic row order was being thrown away by the table widget before it reached the
  screen** (peer review, finding rev-B). Two fixes earlier in this entry made `GetEntries` return a
  stable order tie-broken on the row id -- and then `RowList` re-sorted the whole set with
  `table.sort`, tying on the player **name** and giving up when that tied too. `id` was never
  consulted, `table.sort` is not stable, and `name` is the exact field the Guild Bank Log documents
  as unreliable (Blizzard returns it populated on one query and nil on the next). So the reshuffle
  the earlier fixes were meant to stop carried on happening. `RowList` now takes an optional
  `tiebreakKey`, **defaulting to `id`**, so every tab is fixed without changing a single call site;
  rows without that field behave exactly as before. Location: `GUI/RowList.lua`.

- **A shared widget's comment claimed its sort was "stable". It never was** (peer review, finding
  rev-C). `table.sort` is quicksort in Lua 5.1. What the comparator had was a *secondary key*, which
  orders some ties; stability would preserve input order for all of them. The word mattered because
  every tab renders through this widget, and a consumer reading "stable" reasonably pre-sorts its
  data and expects that to survive -- which is precisely what failed above.

- **28 column tooltips told you a column sizes to the rows you can SEE. It sizes to every row captured**
  (peer review, finding rev-E). Autofit measures the full data set once per load and never re-measures,
  so filtering a column or typing in the search box does not narrow anything -- but the tooltips on
  seven tabs promised otherwise, in text you can hover. The behaviour is the sensible one (a column
  that resizes on every keystroke is worse), so the words were corrected rather than the code, across
  `AddonLoadTab`, `GratzTab`, `GuildLogSubTab`, `GuildBankLogSubTab`, `MailLogSubTab`,
  `TradeLogSubTab` and `WhisperLogSubTab`.

  The replacement word is per-tab rather than uniform, because a single word would have been wrong
  twice: the five log tabs say **captured** (their own vocabulary -- the Whisper tab's empty state
  already reads *"captures every whisper sent or received"*), Gratz says **saved** because profiles
  are saved and nothing is captured, and the Addon Load tab says **listed** -- where "loaded" would
  have collided with `Loaded` as a literal status value in the very same tooltip.

- **The Guild log had both of the Guild Bank log's read-path bugs, unchanged** (audit round 8,
  finding T). The two read APIs are near-identical code over the same per-guild-bucket domain, so
  fixing one and not sweeping for the other left the twin broken: opening the Guild tab **wrote ids
  into your saved data** (ungated, so a disabled Guild Log still wrote), and same-hour rows
  **reshuffled between redraws** because the sort had no tie-break and `tupleToTs` here is hour-coarse
  by the file's own admission. For a guild event log that is most rows, since joins and leaves
  cluster. Both fixed the same way as the twin. One difference worth stating: GuildLog has no `occ`,
  so nothing later regenerated the id -- the ungated write was the whole of it, not the
  identity-instability the Guild Bank version also had. Location:
  `Modules/GuildLog/GuildLog.lua:400` and `:412`.

- **Two helpers fought over one callback slot, and whichever lost caused the exact leak the other
  prevents** (audit round 8, finding Q). An AceGUI widget has a single `OnRelease` slot and
  `SetCallback` **assigns** to it -- read in the installed Ace3 (`AceGUI-3.0.lua:292-296`), not
  assumed. `Compat.lua` documents two helpers that both want release-time cleanup:
  `AceGUIFrameScripts` (restores raw frame scripts so they do not survive into the next owner of a
  pooled widget) and `DetachPool` (orphans virtual-scroll rows so they do not bleed into another
  addon's window). Installing both on one widget silently destroyed one of them, with no error.
  Latent -- nothing used both -- but the path was wired: `addon.UI.AttachTooltip` installs
  `AceGUIFrameScripts` silently on any Dropdown or EditBox. New `addon.GUI.OnRelease(widget, fn)`
  chains instead of assigning, and both helpers now route through it. Location: `Compat.lua`.

- **Opening the Guild Bank tab wrote to your saved data** (audit round 7, finding N). `GetEntries`
  carried a lazy migration inside its read loop -- `if not e.id then e.id = rowId(...) end` -- on the
  stored entry rather than the copy it was building. So a *read* mutated SavedVariables, and because
  it does not look like a write path it was never gated: a **disabled** Guild Bank Log still wrote
  whenever the tab was opened. It also stamped the id under ordinal `0`, and since an entry with no
  id is exactly an entry with no `occ`, the next ingest's migration then assigned a real ordinal and
  regenerated the id -- publishing one identifier and settling on another, depending on whether you
  opened the tab before or after the guild bank. The id is now computed into the returned copy only.
  Location: `Modules/GuildBankLog/GuildBankLog.lua:751`.

- **Same-hour guild bank rows reshuffled between redraws** (audit round 7, finding O). `GetEntries`
  sorted on `ts` alone, but `tupleToTs` has **hour** precision, so every transaction in the same hour
  ties -- and with no guild filter the list is assembled from `pairs(db.guilds)` in hash order to
  begin with. `table.sort` is not stable in 5.1, so tied rows came out in an arbitrary order that
  could differ between two draws of an unchanged log. Now tie-broken on `id`. This is the same defect
  as finding J on the read path instead of the migration path; fixing J did not fix it.

- **Two `C_Timer` guards in one file disagreed about whether a missing API was survivable** (audit
  round 7, finding P). The bank-open handler falls back to calling `requestAllTabs()` directly when
  `C_Timer.After` is absent; the deferred-ingest guard logged a line and scheduled **nothing** when
  `C_Timer.NewTimer` was absent -- which would have left capture depending entirely on
  `GUILDBANKLOG_UPDATE`, the event the surrounding comment says does not reliably fire on TBC and
  Anniversary. Believed unreachable on every shipped flavour, so this is consistency rather than a
  live fix: the reason to do it is that the next person copying one of the two guards has even odds
  of copying the one with no fallback.

- **The fallback error handler installed itself for every user, forever, whether or not Developer
  Tools was ever switched on** (audit round 4, finding E). `Diagnostics:_installSelfCapture` chains
  into `seterrorhandler` and registers six events, and it ran whenever the `!!TOGT` companion was not
  supplying capture -- gated only *inside* `_record`. That is the wrong place, because **a chained
  error handler has no inverse**: you take the previous handler and call it, and there is no way to
  unchain yourself later without clobbering whoever chained on top. Install-only-when-enabled is the
  only design under which it can ever be off. It now returns early unless recording is enabled, and
  `GUI/Settings.lua`'s Developer Tools toggle calls it so flipping the switch on captures immediately
  rather than at next login. A new `_companionCapturing` flag stops that path chaining a **second**
  handler on top of the companion's, which would double-count every error. Location:
  `Modules/Diagnostics/Diagnostics.lua:269`.

- **A diagnostics `enabled` flag that nothing read** (audit round 4, finding D's follow-up).
  `db.global.diagnostics.enabled` sat in the defaults, was written by no UI, and was read by nothing
  in the module -- Diagnostics is dev-only and gates on the Developer Tools switch through its own
  `gate`. Removed, with a comment saying why there is deliberately no off-switch there. The change is
  behaviour-neutral: `IsLogCategoryEnabled("diagnostics")` answered true with the flag present and
  answers true without it, since a nil `enabled` on an existing section defaults to on.

- **Five spellings of the enabled flag disagreed about what a missing flag means** (audit round 4,
  finding G -- rated the highest of that addendum). `addon:IsModuleEnabled` defaults a missing
  section to **ON**, by explicit design and with a comment saying so
  (`TOGTools.lua:350-356`), while the inline `if not db.enabled` spelling defaults the same state to
  **OFF**. On a fresh or partly-migrated DB that meant half the addon was on and half off, decided
  by nothing but which spelling a given module happened to use. Every module-level gate now goes
  through the one predicate, which is what `Gratz:124` and `Smack:156` already did. Converted:
  `Modules/Mailbox/Mailbox.lua` (two sites), `Modules/LoginDigest/LoginDigest.lua` (two),
  `Modules/IdHitThat/IdHitThat.lua`, `GUI/IdHitThatHUD.lua`.

  **The finding's list of offenders was incomplete, and reading every `.enabled` in the tree rather
  than trusting it turned up two more.** `Modules/NamePrefix/NamePrefix.lua:72` and `:296` carry the
  same inline gate. And the per-module settings checkboxes in `GUI/NamePrefixTab.lua`,
  `GUI/LoginDigestTab.lua` and `GUI/IdHitThatTab.lua` rendered the **raw** flag, so a missing one drew
  an unticked box for a module that was actually running -- they now use `~= false`, the spelling
  `GUI/Settings.lua:266` already used.

  **One assertion was deliberately inverted, and it is a real behaviour change rather than a
  re-baseline.** `Tests/idhitthat_spec.lua` used to pin *"treats a missing config as disabled"*.
  Deciding the default once means one of the two old behaviours had to change, and ON is the one
  chosen -- it is the shared predicate's documented default, and a module silently doing nothing on a
  partly-migrated DB is the worse failure. The spec now says so in place, with the reasoning, so
  nobody reads it later as a test bent to fit the code.

  **Left alone on purpose:** the per-filter `f.enabled` in Smack and the per-profile `p.enabled` in
  Gratz. Those are a different concept -- a filter or profile you have not enabled should not fire --
  and their tab code normalises the value on write, so the default-off read is coherent there.
  `NamePrefix`'s `dedupFilter` is also untouched: whether the module toggle is meant to be a master
  over that checkbox is an open question for you in `docs/AUDIT.md`, and answering it by quietly
  adding a gate is exactly what that entry asks nobody to do.

- **Gathering stats merged two different items that share a name** (audit round 6, finding L).
  `GatheringLog:ComputeStats` keyed its per-item aggregate on `e.name`, so the two Refulgent Copper
  Ore tiers -- genuinely different items with different `itemID`s -- collapsed into one row under
  whichever link arrived first, taking the per-source `avg` yield derived from those totals with
  them. `Modules/LogGraphs/LogGraphs.lua:130-151` does the same job keyed by `itemID` and its comment
  claims the Gathering log already does too; it did not. Now keyed by `itemID`, falling back to the
  name only when an entry has no id, with the sort tie broken on `itemID` because a name is no longer
  a total order and `table.sort` is not stable in 5.1. A row also now adopts `link` and `quality`
  from a later entry when the first one was logged before the item cache warmed. Location:
  `Modules/GatheringLog/GatheringLog.lua:602`.

  **Worth knowing before trusting the old behaviour was ever seen:** `ComputeStats` has **no
  production caller**. `GUI/GatheringLogSubTab.lua` never calls it, so ~115 lines of aggregation are
  currently reachable only from the spec suite, which is why the merge was invisible in game.

- **The Trade graph counted stacks while its axis said items** (audit round 6, finding M). `tradeSpec`
  declares `unit = "items"` but its series value was `#e.received` -- the number of list *elements*.
  `TradeLog.slotsToList` merges same-link slots into one element carrying a `count`, so a trade of 20
  Copper Ore charted as **1**. Now sums the elements' counts, matching `gatheringSpec`, which already
  used `e.count or 1`. Location: `Modules/LogGraphs/LogGraphs.lua:250`.

- **The Mail and Trade filter dropdowns did not cascade** (audit round 6, finding M). `gatheringSpec`
  documents the contract -- `getOptions(scoped)` receives the set already narrowed by the other
  active filters -- and every one of its builders takes it. Mail's Character and Trade's
  Character/Partner declared `function()` with no parameter, and Lua discards the extra argument
  silently, so narrowing one filter never trimmed the others. Mail's **Direction** stays uncascaded
  deliberately: it is a fixed two-value enumeration, not a list derived from the data, and a control
  that flickers between one and two choices is worse than one that charts an empty series.

- **`InjectEntry` could store exactly one row per transaction shape** (audit round 6, finding K).
  The hook a future TOGBankClassic sync writes through never set `occ`, and `dedupeKey` falls back to
  `tostring(entry.occ or 0)` -- so every injected row hashed under ordinal `0`, and a second genuine
  transaction of the same item/count/tab/hour was dropped. That is precisely the loss `occ` was added
  to prevent, and the comment on `migrateOccOrdinals` already named "future `InjectEntry` batches" as
  a producer of several rows per tuple. A nil `occ` also left the row to be renumbered -- and its
  `id` regenerated -- by the next migration pass, handing the injecting addon an identifier this
  addon then changed behind it, with the outcome depending on whether the row arrived before or after
  the one-shot migration ran. `occ` is now assigned in ingest's own ordinal space (starting at 1) in
  the same pass that builds the dedupe set, and a caller that knows the real ordinal may supply it.
  `InjectEntry` also now gates on `isEnabled()` like every other write path in the module. Location:
  `Modules/GuildBankLog/GuildBankLog.lua:967`.

  **Not fixed, and deliberately:** a transaction that both our ingest and the injecting addon see
  still lands twice. Injection cannot distinguish "a second identical transaction we have not got"
  from "the one we already stored", because the ordinal is a position rather than an identity -- that
  is finding H, which is unsettled. Deduping on the base tuple alone would trade the duplicate for
  silent loss of genuine repeats, which is the worse failure for a ledger.

- **`migrateOccOrdinals` stamped the current guild's key onto whatever bucket it was handed** (audit
  round 5, finding I). It took a bucket but resolved the key itself with `GuildKey()`, five lines
  from its sibling `cleanupDuplicateEntries`, which correctly takes the key as a parameter. Per-guild
  buckets for alts in different guilds are a shipped feature, so any maintenance pass or sync over a
  non-current bucket would have stamped guild A's key onto guild B's rows. It now takes `guildKey`
  as a parameter. The guildless path was a live inconsistency rather than a latent one: `GuildKey()`
  returns nil outside a guild, the old code wrote `occ` and then skipped the `id`, and the one-shot
  `_occMigrated` marker meant the entry kept an id from the pre-`occ` formula permanently. The
  migration is now skipped **entirely** when there is no key, so it simply runs next login. Location:
  `Modules/GuildBankLog/GuildBankLog.lua:248`.

- **`cleanupDuplicateEntries` rebuilt the array in hash order** (audit round 5, finding J). It walked
  `pairs(byKey)` and then sorted by `ts` descending, but `tupleToTs` has hour precision, so equal
  timestamps are common and `table.sort` is not stable in 5.1 -- the order among ties was arbitrary
  and could differ between two runs over the same data. That is not merely a display wobble:
  `migrateOccOrdinals` runs immediately afterwards and numbers legacy rows in **array order**, so the
  `occ` and hence the persisted cross-viewer `id` depended on a hash iteration order. It now rebuilds
  by walking the original array and taking each key's first survivor. This makes the pass
  **deterministic**, not stable -- Lua 5.1's quicksort still reorders ties freely; what is fixed is
  that the same stored data now always yields the same ids. Location:
  `Modules/GuildBankLog/GuildBankLog.lua:221`.

- **`/togt gbdump` produced output that could not be diffed against itself** (audit round 9, peer
  finding S). Two independent sources of nondeterminism in one triage instrument: the entry sort was
  `ts` alone, and `tupleToTs` is hour-precision so ties are the common case rather than an edge case;
  and the per-type name-population summary was printed straight from `pairs(byType)`, so the summary
  block itself reordered between runs. A dump exists to be run, changed, run again and compared, so
  a reshuffle of unchanged data reads as a change in the data. The sort now tie-breaks on `id` and
  the type keys are sorted before printing. The ids are computed into a **side table keyed by the
  entry**, deliberately not stamped onto the stored entry -- doing that would have re-created finding
  N (a read mutating SavedVariables on a path that does not look like a write path). Location:
  `Modules/GuildBankLog/GuildBankLog.lua:923`.

- **`/togt gbcheck` blamed capture when the answer was the off switch** (audit round 9, peer finding
  T). `Diagnose` checked eight preconditions -- five API calls, the bank frame, `IsShown`,
  `IsInGuild`, the guild key and the persisted count -- and not the module's own enabled flag. Every
  capture path is gated on that flag, so with the Guild Bank Log switched off the entry count cannot
  move, and the command's closing advice read that as *"capture is failing"* and sent the user to the
  verbose debug trace, away from the answer. The checklist now reports the enabled state and the
  conclusion branches on it. The eight `QueryGuildBankLog` calls are deliberately left ungated: this
  is a user-invoked command, and with the module off its most useful output is that the API works and
  the buffer has rows you are not capturing. Location:
  `Modules/GuildBankLog/GuildBankLog.lua:1004`.

- **The third `C_Timer` spelling in one file was the one that raises** (audit round 9, peer finding
  U, extending finding P). `:649` fell back to a direct call, `:529` now falls back to `After`, and
  `Diagnose`'s `C_Timer.After(2, ...)` had no guard at all. Unreachable on every shipped flavour, so
  the cost today is zero -- the point is that a reader copying one of three variants had a one-in-
  three chance of lifting the one with no fallback. Location:
  `Modules/GuildBankLog/GuildBankLog.lua:1025`.

- **Opening the Guild Bank Log's Custom date picker built and sorted a full copy of the entire log**
  (audit round 10, peer finding V). The calendar asked *"which days have data"* through `GetEntries`,
  which allocates a sixteen-field copy and computes a row id for every row that passes the filter and
  then sorts the whole result -- and the caller read exactly one field, `ts`, from each and discarded
  the rest. This module's stated purpose is accumulating history beyond Blizzard's rolling window, so
  that cost grows with the thing it is designed to grow. New `GuildBankLog:GetTimestamps(guildKey)`
  walks the buckets and collects `ts`, with no copy and no sort. It **mirrors `GetEntries`' bucket
  selection exactly**, including the part that reads like an oversight -- a guild key with no bucket
  falls through to the merged view -- because the calendar and the table must agree about which rows
  exist. Location: `Modules/GuildBankLog/GuildBankLog.lua:825`, `GUI/GuildBankLogSubTab.lua:242`.

- **The Time column showed an hour the server never recorded** (audit round 10, peer finding Y).
  Entry timestamps sit on `GetServerTime()`'s clock, but `date()` renders in the machine's timezone,
  so for a player not on realm time the absolute form disagreed with the relative form directly above
  it. That matters more here than it would elsewhere because this log's whole identity model is
  hour-precision: `tupleToTs` is hour-granular, the picker tells the user one hour is the smallest
  meaningful window, and `%H:00` hardcodes the zeroed minutes to say so -- so the column could show a
  different hour than the one the entry is keyed on. Timestamps are now shifted by the realm's UTC
  offset and formatted as UTC. `C_DateAndTime.GetServerTimeLocal` returns server time already offset
  by the server's timezone, so the difference between the two is that offset; it is feature-detected
  and a missing namespace degrades to the previous rendering. `date("!...")` was confirmed to work in
  this client from shipped addons that use it, not from the Blizzard tree, which never does.
  Location: `GUI/GuildBankLogSubTab.lua:57`.

- **A guildless character silently got the merged all-guilds view** (audit round 10, peer finding W).
  The Guild dropdown's tooltip promises it *"defaults to your currently logged-in character's guild"*,
  and with no guild there is none, so the default resolved to nil -- which is what the explicit
  **All guilds** option means. Nothing on screen distinguished the two. The behaviour is unchanged
  (showing every bucket beats showing none) and a line above the table now says which mode is in
  force. The existing empty-state message covered only the zero-row case, which is not the case that
  matters: a guildless character on an account whose alts have history has rows. Location:
  `GUI/GuildBankLogSubTab.lua:259`.

- **The guild-filter resolution tested `"__all"` twice and discarded a value it had just computed**
  (audit round 10, peer finding X). Correct, but only provably so by tracing all three inputs, and
  the second test was exactly the line a later edit drops -- which would have made the explicit
  All guilds option silently start showing one guild. Rewritten as one three-way choice. Location:
  `GUI/GuildBankLogSubTab.lua:217`.

  **Twelve specs cover these, and the two that could have been false greens were driven red before
  being believed**: the dump comparator was made to return `false` for ties and the type-key sort was
  deleted, and exactly the two intended specs failed. Suite 679 to 691, 0 failed.

- **`/togt logs gathering` opened nothing and said nothing** (audit round 11, finding tog-U). The
  slash handler's alias map covered five of the eight registered log categories -- Gathering, Graphs
  and Diagnostics were unreachable by name -- and an unmatched sub-key fell through a guarded `if`
  and returned having done nothing at all, which is indistinguishable from the addon being broken.
  This is the third-hand-maintained-list finding from audit round 6 turning up at a **fourth** site,
  and `Tests/logregistry_spec.lua` -- written for that finding -- had predicted in its own header
  that a fourth list would be the defect it existed to catch. The map is complete, an unknown key
  now names the valid ones, and the spec checks this map against `addon.logCategories` in both
  directions. Two defects fell out of reading it: the deep-link sequence existed in three copies, and
  `/togt logs diagnostics` would have bypassed the Developer Tools gate that `/togt diag` applies --
  both now go through one `openLogsSubTab` helper that owns the gate. Location:
  `SlashCommands.lua:22`, `Tests/logregistry_spec.lua`.

- **`/togt Settings` printed the help list** (audit round 11, finding tog-V). The handler folded case
  on the Logs sub-tab *argument* but not on the *command*, so `/togt logs Mail` worked while any
  capitalised command silently fell through to help. `input` is now lowercased once at the top;
  nothing below echoes it back or takes a case-sensitive argument. Location: `SlashCommands.lua:33`.

- **Retail: a secret `bnSenderID` was guarded against on the lookup and then written to
  SavedVariables** (audit round 21, finding tog-AE). `resolveBNName` documents that the field is not
  flagged NeverSecret and can be a secret value during Retail chat lockdown, and checks it before
  passing it to the account API. `append` guards `text` and `playerName` for the same reason, its
  comment saying that storing a secret "would taint our SavedVariables" -- and then stored
  `bnSenderID` unchecked. Both existing guards pass in the failing case, because the Battle.net paths
  replace `playerName` with an already-resolved name before `append` sees it. The entry is now stored
  with that one field nil; the whisper itself is kept, since nothing reads the stored id and the
  message is what the user wants logged. Location: `Modules/WhisperLog/WhisperLog.lua:176`.

  The existing spec, *"never passes a secret bnSenderID to the account lookup"*, was true and was
  about the lookup -- a guard on the read path had been reading as coverage of the field. Also worth
  recording: the obvious one-line fix, `isSecret and nil or bnSenderID`, always yields the right-hand
  side because `nil` is falsy, so it would have stored the secret value every time while looking
  correct. Suite 721 to 724, 0 failed.

- **The Guild Bank Log's 30-second debounce was spent even while the log was switched off** (audit
  round 4 finding C, confirmed still live in round 20). The bank-open trigger stamped its debounce
  timestamp before the enabled check, so opening the bank with the module off consumed the window on
  a pass that then did nothing -- every function downstream gates individually, so the work was
  correctly skipped and the cost was paid anyway. Re-enable inside those 30 seconds and the first
  real bank-open was silently dropped. The gate now runs before the stamp.

  **This had been marked "blocked on a TBC client" since it was filed, and it was two questions
  wearing one label.** Whether `GUILDBANKFRAME_CLOSED` -- the event that clears the stamp -- fires on
  TBC and Anniversary genuinely does need a client, because our own code records that
  `GUILDBANKFRAME_OPENED` demonstrably does not fire there. But whether a disabled pass should spend
  the debounce at all needs nothing: gating before the stamp removes the dependency on that answer
  entirely. Suite 719 to 721, 0 failed, with the mutation reproducing the exact failure. Location:
  `Modules/GuildBankLog/GuildBankLog.lua:634`.

- **A stray inbox update could wipe an alt's mailbox snapshot** (audit round 17). `Capture` overwrote
  the whole snapshot on every `MAIL_INBOX_UPDATE`, and that event demonstrably reaches handlers while
  the mailbox is closed -- Blizzard's own Classic `MailFrame.lua` registers it in `OnLoad`, never
  unregisters, and handles it with no is-shown check. A fire outside a real visit could therefore
  replace a good snapshot with an empty one and that alt would silently stop being watched. Whether
  the client actually returns zero items in such a fire is engine-side and unknowable offline, so the
  remedy is built not to need the answer: `Capture` now writes only between `MAIL_SHOW` and
  `MAIL_CLOSED`, both verified present in the Classic Era client's own `MailInfoDocumentation.lua`.
  "Is the mailbox open" is installed state with real edges; inferring it per-event from the payload
  was the substitution. The obvious shortcut -- never overwrite a non-empty snapshot with an empty one
  -- is explicitly rejected in the code, because an alt that genuinely empties its mailbox would then
  look full forever; a spec asserts that case still records. The spec helper was part of the hazard:
  it fired the update alone, modelling the abnormal path as the normal one. Location:
  `Modules/Mailbox/Mailbox.lua:49`.

- **A file listed twice in a TOC would partition the shared GUI namespace** (audit round 18). Not a
  live defect -- no manifest has a duplicate -- but the protection was one line of what reads as
  boilerplate. A duplicated entry executes twice, and the failure is not "the second copy wins": it
  is a partition, where consumers that bound a reference before the duplicate line keep the first
  table and consumers after it get the second, so writes land in one and reads come from the other.
  `Compat.lua`'s `addon.GUI = addon.GUI or {}` reuses the table and is what makes that harmless here;
  simplifying it to `addon.GUI = {}` would convert it into a partition across every file-scope alias,
  which the tog-AC fix has just made the house idiom. That line now says so, and `Tests/toc_spec.lua`
  refuses any manifest listing a file twice, driven red by duplicating an entry. Suite 703 to 708,
  0 failed. Location: `Compat.lua:86`, `Tests/toc_spec.lua`.

- **The Addon Load Monitor's load-time comment attributed the measurement to the wrong addon** (audit
  round 14, finding tog-AD). Comment only, no behaviour change -- the code and its spec were both
  already right. `ADDON_LOADED` fires after an addon's files have executed, so the interval between
  two events is the time spent loading the *second* one, which is what `GetData` charges and what
  `Tests/addonload_spec.lua` pins. The comment said the delta was "how long addon N-1 held the
  thread". Recorded because of the fix it invites: shifting the assignment back one row is a one-line
  edit that would blame every addon for its predecessor's cost, in the one column the tab exists to
  provide, with nothing erroring and the numbers still plausible. Same shape as the `retentionDays ==
  0` note -- the danger is that the plausible correction is the bug. Location:
  `Modules/AddonLoad/AddonLoad.lua:152`.

- **The Guild Log's Time column had the same timezone bug as the Guild Bank log, unfixed** (audit
  round 13b, finding tog-AC). `GUI/GuildLogSubTab.lua` held a byte-identical copy of the `fmtTime`
  that reviewer finding Y was raised against, on identical data: both logs build `ts` by subtracting
  Blizzard's relative `(y, m, d, h)` offsets from `GetServerTime()`, so both are server-clock values
  at hour precision, and `date()` renders in the machine's timezone. The fix landed in one twin and
  not the other because nothing connected them -- the third time this pair has diverged, and the
  second time a fix reached only half of it. The function now lives once as
  `addon.GUI.FormatServerHourTime` in `Compat.lua` with both sub-tabs binding a local alias, the same
  treatment `FormatGuildKey` got last time. Its comment states the line that matters: a `ts` stamped
  from `GetServerTime()` at capture -- Mail, Trade, Whisper, Gathering -- is *correct* rendered with
  plain `date()`, because it should agree with Blizzard's chat timestamps, and must not be unified
  onto this function. Every `date(` site in the addon was read and classified rather than counted.
  Location: `Compat.lua:193`, `GUI/GuildLogSubTab.lua:53`, `GUI/GuildBankLogSubTab.lua:51`.

  **The move created a load-order hazard, which is pinned rather than assumed.** Both sub-tabs bind
  the helper at file scope, so it resolves once at load; a TOC listing a GUI file before `Compat.lua`
  would leave the local nil and fail on the first rendered row rather than at load. `Tests/toc_spec.lua`
  now asserts `Compat.lua` precedes every `GUI\` entry in all six manifests, driven red by moving it
  below `GUI\RowList.lua` in one TOC. Suite 702 to 703, 0 failed.

- **Retail: the Login Digest's bag percentage ignored the reagent bag** (audit round 13, finding
  tog-AA). `fieldBuilders.bags` walked containers 0 to 4 -- backpack plus four bags -- which is right
  on every Classic flavour and wrong on Retail, where 10.0 added a fifth equipped bag at index 5. The
  same index means opposite things per flavour (first bank bag on Classic, reagent bag on Retail),
  which is why the hardcoded range read as correct. The figure was not stale, it was computed over
  the wrong denominator, in the field whose entire job is telling you whether to go and clean up.
  The range now comes from `NUM_TOTAL_EQUIPPED_BAG_SLOTS or NUM_BAG_SLOTS or 4`: verified per
  flavour in the client source, Retail's `Blizzard_FrameXMLBase/Constants.lua:184-186` defines the
  first as `NUM_BAG_SLOTS + NUM_REAGENTBAG_SLOTS` and the Classic Era tree's
  `Classic/Constants.lua:209` defines only the second, with no reagent constant at all -- so the
  right global exists exactly where the reagent bag does. **A spec had ratified the bug**, asserting
  that index 5 is ignored "because it is the bank"; it is now three specs, one per flavour case.
  Location: `Modules/LoginDigest/LoginDigest.lua:110`.

- **The Login Digest walked the entire guild roster at login to recompute a number the API had
  already returned** (audit round 13, finding tog-AB). `GetNumGuildMembers` returns `(total, online)`
  on every flavour -- measured on live Classic Era and Retail clients by the test harness, not
  inferred -- and the guild field read only the total, then called `GetGuildRosterInfo` once per
  member to count online players itself. In a 900-member guild that is 900 API calls at
  `PLAYER_LOGIN`, inside the three-second window the module already waits out because roster, mail
  and bag data have not settled. It now takes the second return; the walk survives only as a
  fallback for a client that does not supply it. The spec stub was part of the problem -- it returned
  a single value, a signature no client has, which made the fallback look like the normal path.
  Location: `Modules/LoginDigest/LoginDigest.lua:76`.

  Both were driven red by mutation before being believed, and the round also recorded a process
  finding against an earlier session of this addon: the round-8 harness adoption declared four APIs
  to have "zero occurrences" in this codebase and three of them were live -- `GetNumGuildMembers` and
  `GetGuildRosterInfo` here and in the Guild Log, `UIDropDownMenu_Initialize` in two GUI files. The
  claim came from a search, and a search reports only the spellings you thought of. Suite 697 to 702,
  0 failed.

- **The Mailbox tab's rows reshuffled while the inbox loaded** (audit round 12, finding tog-X). Both
  sorts in `GetAltSummaries` left ties unordered -- the per-alt mail list sorted on `expiresAt`
  alone, and the alt list on its soonest mail with no fallback. Equal expiry is the common case here,
  not an edge case: every mail in one capture gets `now + daysLeft * 86400` from the same `now`, so a
  batch that arrived together -- the five "Auction expired" mails the v0.5.2 Mail Log work is written
  about -- is equal to the second, and `table.sort` is free to reorder ties in Lua 5.1. The tab
  redraws on every `MAIL_INBOX_UPDATE` and deliberately does not debounce, so the user watched an
  unchanged list reshuffle. Mail now tie-breaks on sender then subject; alts fall through to
  `charKey`, which is unique and so makes that comparator a total order over what is a `pairs()`
  walk. Location: `Modules/Mailbox/Mailbox.lua:132`, `:141`.

  **A false green in the new specs was caught by the mutation run.** The alt-level spec first used
  two alts and passed with the fix reverted -- `pairs()` yielded that pair already in order. Rewritten
  with six keys the mutated build returns them as `Bex, Tao, Mia, Zed, Cal, Abe`, which is the hash
  order the finding is about. Suite 693 to 697, 0 failed.

- **`addon:Debug`'s comment named the wrong clock** (audit round 11, finding tog-W). It described
  `date("%H:%M:%S")` as "server-local wall clock". `date()` formats in the machine's timezone. The
  code is correct -- Blizzard's own chat timestamps are `date()` too, which is exactly what that
  timestamp is for -- but the comment is how the real version of that bug gets written again, and it
  had just been written: the Guild Bank log's Time column, above. The comment now says whose clock it
  is, why it is the right one there, and names the place where it was the wrong one. Location:
  `TOGTools.lua:340`.

### Improvements

- **Mail with more than 12 attachments lost the rest (audit finding tog-AN).** The Mail Log read
  inbox attachments with a hardcoded 12 slots. Blizzard's own Classic Era source declares
  `ATTACHMENTS_MAX_SEND = 12` but `ATTACHMENTS_MAX_RECEIVE = 16`, and its inbox code walks to 16 -- so
  slots 13 to 16 were never looked at, and anything in them vanished from the log. **12 is correct on
  the send path**, which is exactly why the same bare number looked right at all three sites. All
  three now read the matching client constant instead. Suite 768 to 769, 0 failed. Location:
  `Modules/MailLog/MailLog.lua:73`.

- **The Item DB's "Fill Gaps" button could never find the gaps (audit finding tog-AM).** When a build
  stops making progress it gives up on whatever is left so the count can reach zero -- and it did that
  by **deleting** the list of what it gave up on. Fill Gaps is documented as the cheap recovery pass
  over exactly that list, so after any build that ended this way it reported "no gaps to fill" and did
  nothing, while the build showed as complete. The reasoning for dropping them was that they are
  permanently unresolvable, which is usually true -- but the same stall is produced by a disconnect, a
  stalled server or a loading screen, and in that case fetchable items were discarded with no recovery
  short of a full ~240k re-walk that nothing prompted. They are now **set aside** instead of deleted:
  the build still converges and still completes, and Fill Gaps re-seeds from that pile, which is what
  its own description always claimed it did. A genuinely dead ID just stalls again, costing one wait
  and no data. Suite 764 to 768, 0 failed. Location: `Modules/ItemDB/ItemDB.lua:386`.

- **Turning Developer Tools on no longer arrives too late to see what went wrong (audit finding
  tog-AL).** The !!TOGT companion captures errors from the very first moment of load, buffering them
  because TOGTools itself has not started yet. TOGTools then drained that buffer on startup **whether
  or not it was allowed to store anything** -- so with Developer Tools off the login-time errors were
  consumed and discarded, and worse, attaching told the companion to **stop buffering** (`!TOGT.lua`
  treats an attached callback as "TOGTools is live now"). The user journey this breaks is the only one
  the switch exists for: you hit an error, you turn Developer Tools on to look at it, and it is
  already gone. **Measured: enabling the switch after two buffered login errors produced 0 entries;
  it now produces 2.** The buffer is left untouched while recording is off -- it is ring-capped at 250
  by the companion, which is exactly the mechanism for that wait -- and the Settings toggle now drains
  it at the moment you flip it rather than at the next reload. Locations:
  `Modules/Diagnostics/Diagnostics.lua:245`, `GUI/Settings.lua:110`.

  **The cause is worth recording: a correct rule applied one step past where its reason ran out.** An
  earlier audit established that the *fallback* error handler must be gated at install time, because a
  chained handler cannot be removed afterwards. True, and unchanged. The companion's capture has no
  such problem -- it is already installed and costs this addon nothing -- so extending the same gate to
  its buffer only destroyed data.

- **I'd Hit That reported "0% glancing" on Cataclysm and Mists, where glancing blows were still live
  (reviewer finding rev-K).** The zero was justified by a comment reading *"skill was removed in
  4.0"* -- true, and about **weapon skill**. Glancing blows are a different mechanic and survived
  until 6.0.2, two expansions later, so a player on those flavours was reading a confident zero for
  something costing them a real slice of their white damage. A true statement propping up an
  unrelated claim, which is the most deceptive form of this. The readout is now marked **unknown**
  rather than zero and the footer stays hidden there: **no number was invented**, because the
  Cata/MoP glancing chance is still unsourced and the existing formula is weapon-skill-based and
  cannot be used on that path. Location: `Modules/IdHitThat/IdHitThat.lua:330`.

  **The unknown marker is `false`, not `nil`, and that is not a style choice.** A spec added earlier
  guards the rule that every result field is *assigned* on every flavour -- it poisons the table and
  asserts nothing survives -- because a nil reaching a percentage format is a HUD crash. Assigning
  nil removes the key entirely, so that guard would have passed without ever checking this field.
  Caught by asking why the guard still passed, not by it failing.

- **Rows that tie now hold their order in EVERY log, not just two of them (reviewer finding rev-J).**
  tog-AI established that the `id` tiebreak only reached Guild Bank and Guild, because the other four
  logs stamp no row identity -- but I fixed the record and left the widget itself saying the old
  thing, in the comment a maintainer would rely on. Worse, the code matched the comment: with no
  `id`, `GUI/RowList.lua` fell through to a bare `return false` and quicksort was free to leave those
  rows anywhere. **The fix needs no stored identity and it is the one already written in the Trade
  log the same day:** the widget walks its input array to filter, so recording arrival order in a
  side table keyed by the row gives every consumer a total order -- no stored field, no migration, no
  extra pass, no call-site change. **Measured by reverting:** sixteen identity-less rows come back
  thoroughly shuffled without it, which is what a user would see between two redraws of one log. A
  three-row version of that test would have passed either way, since quicksort leaves a short array
  alone -- the size is load-bearing and the spec says so. Location: `GUI/RowList.lua:775`.

- **The GRM poll budget is now set in one place instead of reset in three (reviewer finding rev-I).**
  Two earlier fixes each caught one way a poll chain could end; the third was missed -- unticking the
  dedupe checkbox mid-chain left the counter stranded, so the next attempt got a fraction of its
  allowance. **Measured: four polls instead of twenty.** Rather than add a third reset, the counter is
  now set where a chain *begins* and reset nowhere, which dissolves that bug, the two before it, and
  any fourth exit path someone adds later. Location: `Modules/NamePrefix/NamePrefix.lua:296`.

- **The GRM duplicate-name tidy-up now starts working when you join a guild, instead of waiting for
  you to notice (reviewer finding rev-G).** The earlier fix restored the poll budget so re-ticking
  the checkbox got a full set of attempts again -- but that only helps a player who re-ticks, and the
  likelier one never does, because the box still reads "on" and nothing reported anything. GRM only
  hooks chat once it has a guild, so joining one is the exact moment the answer changes, and nothing
  in the addon was watching for it. `PLAYER_GUILD_UPDATE` now restarts the poll. **Implementing it
  surfaced a second problem the remedy did not mention:** that event fires more than once per join,
  so every fire would have started a fresh chain against one shared counter -- the fix racing itself.
  A latch stops that, driven red separately (two guild fires take one live chain to three without
  it). Location: `Modules/NamePrefix/NamePrefix.lua:400`.

- **Name Prefix failures are no longer silent on Classic (reviewer finding rev-H).** Both chat hooks
  wrap their work in a `pcall`; the Retail path printed the error and the Classic path discarded it.
  The reason recorded for leaving that alone was wrong twice: it is **not** a change to "every
  flavour" (Retail already reports, so making Classic report removes a divergence rather than
  introducing one), and the `pcall` covers the whole of `ModifyMessage` -- the DB read,
  `GetChannelName`, the editbox read and write -- not just the format-string bug that prompted it. So
  the addon's primary flavour was keeping a permanent silent-failure channel on its chat send path.
  Both paths now report through one handler, **latched to once per session**: the Classic hook runs
  on every Enter, so an unlatched print would be a message per line sent, and the Retail print had
  that same problem on any repeating failure. Quieter than Retail was, louder than Classic was.
  Location: `Modules/NamePrefix/NamePrefix.lua:338`.

- **A tab could be drawn while its module was switched off, and the spec exemption that covered it
  was wrong in one word (reviewer finding F).** `GUI/MainWindow.lua:86` gates tab drawing on
  `IsModuleEnabled(mod.tabKey)`, and `mod.tabKey` is a **second hand-written literal** in the tab
  file -- the registration key is a third, in a different file under `Modules/`. Nothing asserted
  they agree. Let one drift (a rename, a copied tab file, a new module whose author sets `configKey`
  and forgets `tabKey`) and the lookup fails open at `TOGTools.lua:374`: the tab is drawn while the
  module is disabled, silently, with the Settings toggle still reading "off". The missing-field case
  is worse in two directions from one absent field -- `GUI/Settings.lua:261` also refuses to render a
  toggle at all. **`moduleregistry_spec.lua` now asserts `addon.modules[k].tabKey == k`**, driven red
  first by mis-typing one character. All nine agree today; this is the pin.

  **The exemption I had written was wrong in its REASON rather than its conclusion**, which is the
  worse failure: it claimed `tabKey` *"comes from the registry it would be checked against"*, so
  anyone re-reading it would have concluded the property was structurally guaranteed and stopped
  looking. The comment now records what was false about it instead of quietly dropping it.

- **`.luacheckrc` reported a warning on correct code.** `StaticPopupDialogs` was in `read_globals`,
  so every popup registration -- which is an assignment into that table, and the documented way to
  declare one -- reported W122 *"setting read-only field"*. Moved to `globals`, the same fix and the
  same argument as the `TOGTools` entry already recorded there: a warning that fires on the right
  code trains you to ignore the report. `UISpecialFrames` deliberately stays read-only, since
  consumers `tinsert` into it rather than assigning.

- **I'd Hit That's model branch tests the data, not the flavour, and the comment said otherwise
  (audit finding tog-AK).** `IdHitThat.lua:306` picks between the weapon-skill model and Blizzard's
  flat miss table with `if _player.mhSkill > 0`, under a comment reading *"Cata / Mists"*. That
  describes a flavour branch and is not what the condition does -- and the same file asks the flavour
  directly in three other places (`gv.isMists` for the table, `gv.isRetail` twice), which makes
  "making it consistent" the obvious next edit. **It is also catastrophic, and this was measured
  rather than argued:** swapping in `if not (gv.isCata or gv.isMists)` and running the suite takes a
  level-60 player against a level-63 target from **92 to hit down to 32**, because `skillMissChance`
  is then handed a delta equal to the target's entire defense. Nothing raises -- it is a plausible
  number in the one column the feature exists to provide. No behaviour change: the comment now states
  what the condition does and why it must not be tidied, and three specs pin the degenerate case,
  which the existing Cata block could not because it drives its branch by removing the API entirely.
  Suite 746 to 749, 0 failed. Location: `Modules/IdHitThat/IdHitThat.lua:306`.

- **The row-tiebreak fix reached two of the six logs, and a test name claimed it reached all of them
  (audit finding tog-AI).** `GUI/RowList.lua` gained a display-sort tiebreak keyed on `id`, and the
  spec for it was named *"defaults to `id`, so every log consumer is fixed without changing its call
  site"*. Reading every log module for where a row identity is stamped: only **Guild Bank Log** and
  **Guild Log** write one. Mail, Trade, Whisper and Gathering entries have no stable identity, so they
  fall through to the sibling spec's degraded case and their ties were never ordered. Both specs were
  passing and both were right about the code -- the **name** was the thing that was false, which is
  the worse half, because a spec name is read as a statement of what the code guarantees. Renamed with
  the enumeration written beneath it. **Only findable by reading across modules**, which is why a
  careful line-by-line read of `RowList.lua` never would have produced it. Giving the other four logs
  an identity is a change to stored data and was deliberately not done as a tidy-up. Location:
  `Tests/rowlist_spec.lua`.

- **Two trades in the same second could swap places between redraws (finding tog-AJ).**
  `TradeLog:GetEntries` sorted on `ts` alone at one-second resolution, and Lua 5.1's `table.sort` is
  not stable, so a tie came out in whatever order quicksort left it. This is the defect already fixed
  in the Guild Bank Log at a third site -- and Trade is the one log where the engine is the *only*
  place it can be fixed, because per tog-AI its rows carry no `id` for the display layer to fall back
  on. Ties now break on insertion order, newest first, via a **side table keyed by the entry**:
  stamping an index onto the stored row would be a read writing to SavedVariables, which is a defect
  this audit has already found twice. Suite 743 to 746, 0 failed. **Two of the three new specs stay
  green under mutation and are annotated as guards rather than counted as coverage** -- repeating a
  call was always going to agree with itself, and the old code did not stamp anything either.
  Location: `Modules/TradeLog/TradeLog.lua:338`.

- **A format string the game refuses no longer switches Name Prefix off silently (audit finding
  tog-AF).** The format field is free text and the tab invites any pattern, but `cfg.format` went
  straight to `string.format` under a comment claiming *"safe substitution"*. Three things people
  actually type all raise: a literal percent, a numeric spec against a name, and a second placeholder
  with no second argument. **The two hook paths then disagreed about whether you found out.** Retail
  pcalls and prints an error; the Classic path pcalls and reports nothing at all, so on this addon's
  primary flavour the prefix simply stopped appearing, for ever, with nothing to search for. The tab's
  live preview does flag a bad format in red and then stores it anyway, so those values genuinely
  reach the engine. A rejected format now falls back to the default prefix rather than to no prefix:
  an unexpected-but-visible tag is self-correcting because you see it on the next message, whereas
  silence is indistinguishable from the module being broken. Location:
  `Modules/NamePrefix/NamePrefix.lua:126`.

- **Ticking the GRM duplicate-name dedupe before joining a guild could leave the checkbox permanently
  dead (finding tog-AH).** The poll that waits for GRM's chat hook is bounded at twenty tries, which
  is right -- but the counter was never put back when the chain gave up, and the reset further down is
  unreachable while the feature is still off. So a player who enabled it with no guild burned the
  budget, and re-ticking the box after joining one got a single attempt instead of twenty: if GRM had
  not hooked in that instant, the filters were never registered and nothing said so. The budget now
  belongs to each attempt rather than to the session. Location:
  `Modules/NamePrefix/NamePrefix.lua:283`.

- **One path, three reads, two spellings (finding tog-AG).** `dedupFilter` and `ApplyDedupSetting`
  both guard the `addon.DB.global` hop; `ApplyPrefix` did not, so a DB whose `global` section has not
  been built raised out of the send path instead of no-opping like its two neighbours eight lines
  away. Suite 733 to 743, 0 failed, with each fix reverted in turn to confirm the specs bite. **One of
  those new specs was a false green and its own guard is what caught it:** `loadAddonFile` does not
  run `OnEnable`, so the poll chain was never started and the exhaustion test passed without reaching
  the state it is named after.

- **`Tests/moduleregistry_spec.lua` pins the module gate the way `logregistry_spec` pins the log
  categories (audit finding E).** `addon:IsModuleEnabled(key)` fails open at four points
  (`TOGTools.lua:372-379`) -- no DB, no such module, no `configKey`, no section, `enabled == nil` all
  return true. Each is a deliberate default, but together they mean a renamed module key or a renamed
  `configKey` reports "enabled" for ever: no error, no warning, and the Settings toggle silently stops
  working. Nine assertions now hold the line -- every key passed to the gate resolves in
  `addon.modules`, every registered module declares a distinct `configKey`, and any module that gates
  nothing must be exempted with a written reason. Discovery is TOC-driven with a self-check that fails
  if the scan matches nothing, the same shape as the log-side spec.

  **The mutation run measured the gap rather than arguing it.** Mis-typing `Gratz.lua:124` to
  `IsModuleEnabled("gratzz")` -- which disables Gratz permanently and silently -- left `gratz_spec`
  and all 723 other tests green. Only the new spec failed. Two further mutations confirmed the
  exemption table and the alias resolver are load-bearing rather than decorative.

- **Four modules that gate nothing now say so, and the audit's count of them was wrong twice
  (finding D).** The finding named `Diagnostics`, `AddonLoad`, `ItemDB` and `LogGraphs`. `Diagnostics`
  is not in that set at all -- it gates on `recordingEnabled()` (`:104-106`) and argues the case in
  three separate comment blocks; it reads as ungated only because it uses a different predicate than
  the sweep searched for. `ItemDB` was described as storing nothing of its own, and it is the largest
  thing this addon persists (`db.global.itemDB`); it is exempt for a different and sturdier reason --
  nothing in it runs on its own, every write sits behind `_running`, and the user pressing Build is
  the gate. `AddonLoad` was the one the audit could not settle, and reading it end to end confirms it
  persists nothing: the whole capture lives in the file-local `_loadData`. Each of the three carries a
  short comment naming the spec that holds its exemption, rather than a second copy of the reason --
  two copies of a justification is the divergence this audit keeps finding. Locations:
  `Modules/AddonLoad/AddonLoad.lua`, `Modules/ItemDB/ItemDB.lua`, `Modules/LogGraphs/LogGraphs.lua`.

- **A one-hop alias resolver reported `itemdb` as having no `configKey`, and it has one.**
  `GUI/ItemDBTab.lua` binds `local ItemDB = addon.modules["itemdb"]`, then `local Tab = ItemDB`, then
  declares `Tab.configKey = "itemDB"`. Every other tab file goes straight from the registry to `Tab`,
  so a single-hop scan is right eight times out of nine and silently wrong on the ninth -- which is
  how the audit recorded the wrong answer, and how the first draft of the new spec reproduced it. The
  resolver propagates to a fixed point and the reason is written at the line. Suite 724 to 733,
  0 failed.

- **Harness pin moved `1d5ed89` to `fb19abd`, and one of the new entries is the harness fixing a bug
  this addon reported.** `wow.reset()` now restores `_G.LibStub` itself. `env/LibStub.lua` installed
  the registry once at require time and never again, putting it outside the harness's own rule that
  the env must reinstall every global it owns on each reset -- so a spec that stubbed `_G.LibStub`
  left that stub in place for every later spec file. This addon measured one of its own specs at
  **9/9 alone and 0/9 in the full suite**, dying inside real Ace3 with *"attempt to index global
  'LibStub' (a function value)"*, an error naming neither the culprit nor even this addon. The local
  workaround in `Tests/guildkey_spec.lua` is deleted; the account of why it existed is kept in place,
  because the failure shape -- a spec that passes alone and fails in the suite is almost always
  cross-file state -- is the part worth not relearning.

  The other entries in the range were checked against this addon and none applies:
  `UIDropDownMenu_Initialize` now runs the initialiser (no spec here drives one by hand), and the
  guild-roster model was corrected on Retail (the Guild Log reads Blizzard's guild **event** log, not
  the roster). Suite green before and after at **648**.

- **`Tests/logregistry_spec.lua` pins the three hand-maintained lists of log categories against each
  other.** `addon.logCategories` is populated by each module at load; `GUI/Settings.lua` carries a
  hardcoded list of sub-toggles and `LogGraphs.GRAPHABLE` carries a third. Nothing checked that they
  agreed, and they did not -- audit round 4 found six sub-toggles against the registered set, round 6
  found `GRAPHABLE` holding three. The spec requires every registered category to be **present or
  explicitly excluded with a written reason** at both consumer sites, and refuses an exemption for a
  category that no longer exists. Discovery is driven by the TOC manifest rather than by a list in
  the spec, because a fourth hand-maintained list is the very defect being checked.

  It reads source rather than loading `GUI/Settings.lua`, which needs AceConfig and AceDB wired up
  and is unproven offline -- the same approach and the same reason as the wrap-flag spec. Two
  scanner bugs were caught by its own self-checks while being written: a non-greedy `(.-)}` that
  stopped at the first inner brace and scanned one row of a three-row table, and a
  `match(...) or wholeFile` fallback that would have found each `configKey` in unrelated closures
  and passed regardless. **The audit's count was low**: eight categories are registered, not seven --
  `graphs` and `diagnostics` are both real registrations, and both are now exempted in writing.

- **`Tests/tooltipwrapflag_spec.lua` enforces the wrap rule, and found a site the hand-made list
  missed.** It reads the shipped source rather than driving widgets, because the property being
  asserted is "every call site passes the flag" -- driving a real tooltip would only cover the sites
  a test happened to exercise, and the ones nobody exercises are exactly the ones that rot. It parses
  each call's arguments on top-level commas so a nested `string.format(...)` counts as one argument,
  ignores calls named inside comments, and **fails on a call split across lines** rather than
  silently exempting what it cannot see into. Four self-tests keep the scanner honest, including one
  asserting it matches at least 20 real call sites -- without it the suite could go green because the
  scan found nothing.

  It went red on `GUI/MainWindow.lua:233` the first time it ran: a seventh offender that the grep
  behind the original todo item had missed. Both branches were then mutation-checked -- removing an
  `AddLine` flag and removing a `SetText` flag each turn it red. Suite 525 to 537, 0 failed.

- **The guild filter dropdown is one implementation instead of two, and the Guild log finally has
  its tooltip.** `GuildLogSubTab.lua` and `GuildBankLogSubTab.lua` carried byte-identical private
  copies of `fmtGuild` and near-identical `buildGuildDropdown` bodies differing only in which module
  they read. Both are now `addon.GUI.FormatGuildKey` and `addon.GUI.GuildFilterDropdown` in
  `Compat.lua`, which already owns the shared `addon.GUI.*` helpers.

  **The duplication had already drifted, which is the argument for merging rather than a tidiness
  one**: only the Guild Bank copy attached a tooltip, so the same control explained itself in one tab
  and was silent in the other. Both now get one. The new helper also **asserts** its module exposes
  `GetCurrentGuildKey` and `GetGuildKeys` -- a typo in a module name would otherwise produce an empty
  dropdown, which reads as "this player has no guild history" rather than as a bug.

  **This is the opposite call from the Gratz/Smack pair, deliberately.** Those two share a shape but
  not a schema and are actively diverging, so they stay duplicated with the reasoning in their
  headers. These two share an actual domain concept over the same data. `dupscan` located both; it
  does not decide which is which.

- **`IdHitThat` is at 100% line coverage (209/209), up from 88.52%.** The uncovered block was almost
  entirely `OnEnable` -- the event wiring, which is the code that decides *when* the expensive player
  rebuild runs and had never executed offline. Seven specs now drive the real handler: that a
  player's own aura change rebuilds, that **another unit's aura churn does not** (the point of the
  unit-filtered registration -- in a 40-man raid that is one rebuild versus hundreds), that a mere
  target change does not, that the fallback for a client with no `RegisterUnitEvent` still works, and
  that enabling three times still creates **one** frame. That last guard is worth having: without it
  each enable leaks a frame and every event is handled once per leak, which is a performance bug no
  output ever contradicts.

- **`GatheringLog` is at 100% line coverage (395/395), up from 66.84%** -- 42 new specs over the two
  largest untested blocks in the addon. `ComputeStats` (~115 lines) had **no spec at all**, and it is
  what the Gathering sub-tab and summary bar render directly: per-item, per-zone, per-profession and
  per-source aggregation, the distinct-action counting that makes yield-per-node mean anything, and
  the tie-breaks that stop the rendered table reordering itself between draws. The whole
  `UNIT_SPELLCAST_SENT` / `LOOT_OPENED` source-attribution branch was likewise unexecuted, **including
  all four Retail secret-value guards** -- each of which raises rather than returning a wrong answer
  if removed, so they were untested code protecting against a crash. Also now covered: the continent
  map-walk and its cyclic-chain guard, the Retail `C_Spell.GetSpellInfo` path, and the live sub-tab
  refresh.

- **`LogGraphs` is at 100% line coverage (201/201), up from 79.60%** -- 13 new specs. The uncovered
  block was `getSeries`, which decides what the user actually sees on the chart: per-item ranking by
  total gathered, the twelve-line cap, the link-then-name-then-id label fallback that keeps two items
  sharing a name distinct, and the mail/trade series. Also the `distinctOptions` comparator, which had
  never run because every existing fixture had a single distinct value -- so a filter dropdown was
  being built in whatever order entries happened to be logged.

- **`Tests/guildkey_spec.lua`** pins the extracted formatter -- the realm-suffix rule, the
  last-hyphen split (`Knights-of-Cydonia-Firemaw` must not become `Knights`), the space-stripped
  realm comparison, and a nil `GetRealmName`. Mutation-checked: dropping the `gsub` turns it red.
  It is the first TOGTools spec to load real Ace3, and it exposed a live cross-file hazard --
  `gratz_spec.lua:83` and `loggraph_spec.lua:25` replace `_G.LibStub` with a stub function and never
  restore it, so this spec passed alone and failed in a full run. It now restores the canonical
  registry in its own setup.

- **`Tests/wowapi` moved from `05d04e1` (2026-08-06) to `1d5ed89`, 113 commits.** Suite green
  before and after, which for once is the honest result rather than a warning sign: the adoption log
  entries across that range are additive or tooling, and the two behaviour changes in it
  (`GetTexCoord` returning eight values, `Settings` category `ID` becoming a number) are surfaces no
  spec here asserts.

- **`.luacheckrc` added at the repo root.** There was none, so luacheck ran with stock settings and
  reported every WoW API global as undefined: twenty on `IdHitThat.lua` alone, which is a
  permanently non-empty report and therefore one nobody reads. It mirrors `.luarc.json`'s global
  list so the two checkers agree, and carries the `files["Tests"]` block for the busted DSL. Its
  header records why it must be invoked from the addon root: luacheck resolves config from the
  **working directory**, not from the file, so linting from inside `Tests/wowapi` silently picks up
  the harness's config instead.

- **`Tests/HARNESS_CONTRACT.md` created**, the addon's half of the harness conversation in both
  directions since the 2026-08-14 one-file protocol change. It records that the frozen thread at
  `Tests/wowapi/docs/contracts/TOGTools.md` held no requests, so nothing needed carrying across.

- **`.busted` no longer tells a reader to run `busted`.** Its header said "Run `busted` from the
  TOGTools root", which is the stale instruction the harness swept the fleet for: the file is inert
  config, `busted` does resolve on this machine, and invoking it from an addon root collects the
  harness's own self-tests and fails on fixture paths that were never ours. The file is kept, since
  it is the safety net rather than the entry point.

---

## [v0.8.3] (2026-08-06) - Harness catch-up, and four false-greens it exposed

The `Tests/wowapi` submodule was pinned at `4161af8` (2026-07-19), **83 commits behind**. Upstream
had re-initialised its history, so the local `main` and `origin/main` had no common ancestor -- the
six "local" commits were just the old lineage back to the initial commit, nothing authored here, so
the pointer was reset to `origin/main` (`e38b8b8`). The suite was green before and after the move.

**Green before and after is exactly the problem.** The pass count did not change, because the specs
were shadowing the globals whose behaviour changed. Removing the shadows is what found the
following -- every one a path that had never executed offline.

### Bug Fixes

- **`Gratz`'s guild level-up trigger never armed on five of six flavours -- `GuildRoster` was
  declared as a dependency in the Vanilla TOC only.** `Modules/Gratz/Gratz.lua:66` documents it as a
  hard dependency (`## Dependencies: GuildRoster`) and resolves it with
  `LibStub("LibGuildRoster-1.0", true)`; the silent flag keeps a missing library from erroring at
  file scope, so `LGR` comes back nil, `HAS_LEVEL_CB` goes false, and the trigger simply never
  registers. No error, no log line -- the "an absent dependency does not fail, it disables" shape.

  **Scoped honestly, because the first draft of this entry overstated it.** `.pkgmeta` already
  lists `libguildroster` under `required-dependencies`, so CurseForge was installing the library;
  and "GuildRoster" sorts before "TOGTools", so WoW's alphabetical load order generally put it
  first anyway. The declaration is what makes that a *guarantee* rather than a coincidence: a
  `## Dependencies:` entry forces the addon to be present, enabled, and loaded first. Without it,
  a player who unticked GuildRoster in the addon list, or installed by hand without it, lost guild
  level-up gratzing with nothing to indicate why -- and the load-order protection rested on a
  filename.

  `GuildRoster` is now declared in all six. Verified safe before making it hard on five more
  manifests: the library ships TOCs for every flavour this addon supports (Vanilla `11509`, TBC
  `20506`, Wrath `30403`, Cata `40402`, Mists `50504`, Mainline `120007`), so no flavour can be left
  unable to load. Achievement and party level-up triggers were unaffected -- neither touches the
  library. Location: all six `.toc` files.

- **Gathering Log stored no item quality or expansion, so the Gathering chart could not group by
  expansion.** `GatheringLog` and `LogGraphs` both reached for the **bare** `GetItemInfo`, each
  behind its own feature-test. On Classic Era that bare name is not the API -- it is a deprecation
  fallback defined only in `Blizzard_DeprecatedItemScript/Deprecated_ItemScript.lua:42`, behind a
  `return` at that file's line 4 unless `GetCVarBool("loadDeprecationFallbacks")`. Verified against
  the Classic Era client tree: **zero** bare `GetItemInfo(` call sites in the whole `Interface/`
  tree against **66** for `C_Item.GetItemInfo`.

  With fallbacks off the global is nil, both guards are false, and neither branch runs. Nothing
  errors -- an absent global does not *fail* a feature-tested call site, it *disables* it. Every
  gather was stored with `quality = nil`, `expac = nil` and the parsed display name instead of the
  canonical one, and the repair path failed with it: `LogGraphs.entryExpac` exists precisely to
  back-fill `expac` for pre-v0.6.2 entries and reached for the same absent global. Capture and
  fallback failed together, silently.

  Both now localise at load through the namespaced form -- `(C_Item and C_Item.GetItemInfo) or
  GetItemInfo` -- matching the idiom `Modules/ItemDB/ItemDB.lua:51-55` already used. Location:
  `Modules/GatheringLog/GatheringLog.lua`, `Modules/LogGraphs/LogGraphs.lua`.

  **Both specs were complicit, in different ways**, which is why a green suite never saw it:
  `loggraphs_spec` stubbed the bare global -- the branch no client takes, the same false-green as
  `AddonLoad` and `LoginDigest` above -- and `gatheringlog_spec` stubbed *neither* form, so the
  enrichment branch had never executed offline at all. Both now drive `C_Item.GetItemInfo` and set
  `_G.GetItemInfo = nil` so the deprecated global is absent exactly as on a real client. Two new
  tests cover the enriched and uncached cases. Found by round 2 of the `docs/AUDIT.md` peer review,
  which led with the flavour-reach steer from round 1.

  A follow-up sweep applied the same tripwire to the two `v0.8.3` specs of this shape.
  `Tests/addonload_spec.lua` was half-guarded -- it assigned into `C_AddOns` but left the bare names
  present, so a regression in that wiring could have gone green through the `or GetAddOnInfo`
  fallback; the bare three are now nil. `GetAddOnMemoryUsage` / `UpdateAddOnMemoryUsage` deliberately
  stay bare, since `PerformanceDocumentation.lua` declares them under `PerformanceScript` with no
  namespace. `Tests/logindigest_spec.lua` already had the guard and drives both branches on purpose,
  which is correct there -- unlike `GetItemInfo`, the bare container global is a live path on the
  Retail tree, and TOGTools ships `TOGTools_Mainline.toc`.

- **`AddonLoad` was tested exclusively through a branch no supported flavour takes.** The module
  feature-detects `(C_AddOns and C_AddOns.X) or X`, and `Tests/addonload_spec.lua` set
  `_G.C_AddOns = nil` and installed the bare `GetNumAddOns` / `GetAddOnInfo` / `IsAddOnLoaded`
  globals -- so the `or` fallback was the only branch ever exercised. Verified in the client source
  rather than assumed: `AddOnsDocumentation.lua` declares those three under
  `Namespace = "C_AddOns"`, and the Classic Era `Interface/` tree has **zero** bare call sites
  against **21** namespaced ones. The spec now drives `C_AddOns` (assigning *into* it, never over
  it). The module's comment claimed "Classic Era keeps the bare globals", which is backwards, and
  has been replaced with the per-function truth -- `GetAddOnMemoryUsage` /
  `UpdateAddOnMemoryUsage` genuinely *are* bare globals there, because
  `PerformanceDocumentation.lua` declares them under `PerformanceScript` with no namespace.
  Location: `Modules/AddonLoad/AddonLoad.lua`, `Tests/addonload_spec.lua`.

- **`MailLog`'s hook chain was simulated, never run.** `Tests/maillog_spec.lua` installed
  `_G.hooksecurefunc = function(name, fn) hooks[name] = fn end`, which *recorded* callbacks and
  wrapped nothing. Two consequences: the spec invoked the captured function directly instead of
  going through the chain, and `_ourTakeInboxItemRef = _G.TakeInboxItem` (`MailLog.lua:289`) saved
  the **unwrapped original**. That saved reference is the entire TSM-replacement signature -- the
  justification for the snapshot capture path -- and it was being compared against a value it could
  never hold in game. The shadow is gone; the spec's `hooks` table now dispatches through the live
  global, so the existing call sites exercise the real chain.
  Location: `Tests/maillog_spec.lua`.

- **Every `C_Timer`-deferred path was dead offline.** Eight specs assigned
  `_G.C_Timer = { After = function() end, ... }` -- a no-op that silently discarded the callback, and
  a wholesale namespace assignment besides. So `Diagnostics`' two `After(0)` handlers, `Mailbox`'s
  login alert, `LoginDigest`'s print, `Gratz`'s level-settle delay and party-tracker refresh,
  `NamePrefix`'s dedup re-apply and `ItemDB`'s walk ticker had never executed in a test. The
  harness's `C_Timer` is driven by `wow.advanceTime`, so those now run at their real delays.
  `GuildBankLog`'s cancellable `NewTimer` handle was likewise a `Cancel` that did nothing.

- **`LoginDigest`'s bag read was tested on the wrong branch by default.** The spec's flag was named
  `retailBags` and defaulted to the **bare** `GetContainerNumSlots` globals as though those were the
  Classic form. It is the reverse: bare has **zero** call sites in the Classic Era, Anniversary and
  Classic trees (9 `C_Container.` in each) and one only in Retail. `C_Container` is now the default,
  and the fallback case is labelled as the Retail-only path it is -- it still earns its place because
  `TOGTools_Mainline.toc` ships. Location: `Tests/logindigest_spec.lua`.

### Improvements

- **Every spec now calls `wow.reset()` first.** Not one of the 20 did. The suite runs in a single
  Lua state and the world-builders assign globals freely, so leftovers leaked from one spec *file*
  into later ones -- invisible in a single-file run, which is how it survived. This matters most in
  `Tests/idhitthat_spec.lua`, whose whole purpose is that a later-expansion API must be **absent**
  on a flavour that lacks it: a global left behind by an earlier file would make exactly the
  accidental dependency it exists to catch pass silently. Ordering is load-bearing -- `reset()`
  reinstalls every global the env owns, so it must precede the builder's own assignments (putting it
  after `smack_spec`'s `GetTime` override wiped that override and broke a cooldown test, which is
  how the rule got learned).

- **The 13 frozen-clock `_G.time` overrides are now `wow.epoch`.** The harness's `time()` reads it
  live, so it is still steerable per test, but `GetTime()` and `time()` now advance **together**
  under `wow.advanceTime` -- as in the client, where nothing an addon can do makes the two disagree
  about elapsed time. `Tests/gratz_spec.lua` had a local `now`/`GetTime` pair that could drift from
  a separately-frozen `time()`.

- **`Tests/gratz_spec.lua` no longer assigns `_G.C_GuildInfo` wholesale**, which would have
  discarded the harness's `GetInfoText`. The "never polls the guild roster" assertion now reads the
  guild env's own `rosterUpdates` counter instead of a hand-rolled one.

- **Adopted `docs/AUDIT.md`, the harness's peer-review protocol** -- the inverse of a harness
  contract: a contract is raised here and answered by the harness, an audit finding is raised by a
  review session and answered by **us**. A review session writes its findings straight into that
  file -- the one thing a harness session touches in this repo; it does not edit `CLAUDE.md`, the
  docs index or `.pkgmeta`, and does not commit here. Suite green at every step of the catch-up;
  the final pin is recorded once at the end of this entry.

  **Round 1 has already run** and found no defects in `Modules/Logs/Logs.lua`, with four hazards
  chased and disproved -- recorded in the file's `Checked and correct` section so the next reviewer
  does not re-derive them. One suggestion taken: `Logs.lua:56`'s `cfg.retentionDays or 90` reads
  like the classic Lua default-value trap and is not one, because **`0` is truthy in Lua** -- so a
  configured `0` survives `or 90` and correctly disables pruning. Commented in place, because the
  plausible "fix" is what would break it. That is one file of eighteen and must not be read as a
  clean bill of health.

  The first cut of this file was built against the older docs and got two rules wrong, corrected on
  the harness's own notice: it had a `## Fixed` section, which means a resolved finding gets **moved**
  -- a slower form of deleting it, when the whole value is the failure scenario sitting next to the
  code it describes. A fixed finding now stays in place, gets a response block, and flips its row in
  the **Status table**, which is the only place state is tracked. `CLAUDE.md`'s pointer said "struck
  through in place" and was corrected to match.

- **`wow-version-replication.ps1` was replicating almost everything it was supposed to exclude.**
  The dev watcher is a second implementation of the packager's exclusion rules, so it is only
  correct while it agrees with them. Two independent bugs, both silent, found by running
  `-DryRun` and reading the output rather than trusting the startup banner:

  1. **A bare folder name in `ignore:` matched nothing.** `.pkgmeta` uses `docs` and `Tests` -- the
     canonical form, since `release.sh`'s `parse_ignore()` strips a trailing `/*`, not a bare `/`,
     and then rewrites a directory entry to `dir/*`. `Convert-GlobToRegex` only treated an entry as
     a directory when it *ended* with a slash, so `docs` compiled to `^docs$` and matched only a
     file literally named `docs`. Both folders were printed at startup as loaded ignore globs and
     replicated anyway -- pushing `docs\` and the **entire `Tests\wowapi` harness submodule**,
     several hundred files, into every synced client. Bare non-wildcard entries are now resolved
     against the repo with `Test-Path -PathType Container` and treated as a prefix match.
  2. **The dotfile rule was an enumerated list, not a rule.** It named `.git`, `.gitignore`,
     `.gitattributes`, `.gitmodules` and `.pkgmeta`, so `.busted`, `.luarc.json`,
     `.markdownlint.json` and `.markdownlintignore` were replicated. The packager prunes dotfiles
     unconditionally (`-name ".*" -prune`), which is also why listing them in `.pkgmeta` is a no-op
     and the script has to own the rule. Replaced with the general `(^|\\)\.` pattern.

  Verified with `-DryRun`: `docs\`, `Tests\`, every dotfile, `CLAUDE.md`, `*.ps1` and the
  `.code-workspace` all report `[skip]`; `README.md`, `CHANGELOG.md`, all six TOCs, `Version.lua`,
  every module, `libs\` and `textures\` still copy. Synced installs now match the released zip.

- **Added `README.md`** -- the repo had none. User-facing, in the same shape as FastGuildInvite's:
  what each tool does in plain language, a quickstart, the slash-command table, and the three
  companions CurseForge installs for you. `README.md` is **not** in `.pkgmeta`'s `ignore:`, so it
  ships with the addon, which is the point.

- **TOC pass across all six manifests.** `TOGTools_BCC.toc` is now `TOGTools_TBC.toc` -- its
  `## Title` already read "TBC" and only the filename disagreed. Both suffixes are valid client
  flavour names (counted across this install: 52 `_TBC.toc` against 11 `_BCC.toc`), so this is a
  naming-consistency change, not a load fix. Renamed with `git mv` so history follows;
  `Tests/toc_spec.lua` and the replication script's comment were updated with it. Note the sibling
  `GuildRoster` addon still uses `_BCC` -- worth a sweep some time, but out of scope here.

  The empty `## SavedVariablesPerCharacter:` directive was removed from all six. It declared
  nothing: AceDB scopes per-character data inside the account-wide `TOGTools_DB`, which is what
  `DB_DEFAULTS.char` is.

- **`Tests/toc_spec.lua` now pins the manifests' *directives*, not just their file lists.** Four new
  assertions: `GuildRoster` present in every flavour (the regression above), `Ace3` and `!!TOGT` in
  every flavour, `VersionCheck-1.0` in every flavour **except** Retail -- where `/togt vc` is
  unsupported, so its absence is deliberate and now asserted as such rather than merely tolerated --
  and a Title present with no empty `SavedVariablesPerCharacter`. The spec previously read only the
  file entries, so a dependency could go missing from five manifests with the suite fully green.
  A duplicate directive parser inside the file was folded into the shared one.

Suite: **520 passed, 0 failed**. `Tests/wowapi` pin `4161af8` -> `05d04e1`.

---

## [v0.8.2] (2026-08-01) - Version.lua missing from five TOCs

### Bug Fixes

- **`Version.lua` was listed only in the Vanilla TOC, so `addon.gameVersion` was nil on TBC, Wrath, Cata, Mists and Retail** -- v0.8.1 split the `GetBuildInfo` -> flavour-flag derivation out of `TOGTools.lua` into a new dependency-free `Version.lua`, but added the new file to `TOGTools.toc` only. The other five manifests never loaded it, so `addon.gameVersion` was never assigned and every `local gv = addon.gameVersion` resolved to nil on five of the six shipped flavours. One omission, four distinct user-visible errors:

  - `TOGTools.lua:302` -- `gv.isRetail` in `Ace:OnInitialize`, which aborts initialization inside AceAddon's `InitializeAddon`.
  - `Modules/NamePrefix/NamePrefix.lua:275` -- `gv.isRetail120Plus` in `Install`, reached from `OnEnable`, so no chat hook was installed at all.
  - `Modules/IdHitThat/IdHitThat.lua:53` -- `gv.isMists` at **file scope**, picking the physical miss table. This one throws during file load, so the file dies before `addon.modules["idhitthat"]` is registered...
  - ...which is why `GUI/IdHitThatTab.lua:15` then failed with "attempt to perform indexed assignment on local 'Tab' (a nil value)". The tab file merges its UI fields into the engine's registered table; a dead engine file leaves nothing to merge into. Not a second bug -- the same one, one file later.

  Added `Version.lua` ahead of `TOGTools.lua` in `TOGTools_BCC.toc`, `TOGTools_Wrath.toc`, `TOGTools_Cata.toc`, `TOGTools_Mists.toc` and `TOGTools_Mainline.toc`.

### Improvements

- **`Tests/toc_spec.lua` -- the manifests are now under test** -- Nothing in the suite read a `.toc`. Specs load modules by path directly, so a file missing from a manifest tests perfectly green right up until the client loads it; v0.8.1 shipped 503 passing tests and this defect. The new spec parses all six manifests and asserts:

  - **`Version.lua` is listed in every flavour**, and loads **before every non-lib entry** -- the exact regression, plus the ordering constraint that makes it matter (files read `addon.gameVersion` into a local at load time, so anything above it sees nil). Embedded `libs\` entries are exempt; they take no namespace and are deliberately first.
  - **Every listed file exists on disk** -- catches a rename or move that updated the file but not the manifest.
  - **Cross-flavour parity against the union of all six manifests**, so a file added to *any* single TOC fails regardless of which one. Deliberate omissions live in an explicit `INTENTIONAL_OMISSIONS` table with the reason (currently only Smack on Retail, where automated public chat is blocked), and a second assertion fails if an allowlisted entry is actually present -- a stale exemption would silently mask a future real gap.
  - **Relative load order matches across flavours.** Order is behaviour here: engines register `addon.modules[key]` and the GUI tab files merge into that table, so a swapped pair is a nil index in one flavour only.
  - **Each TOC's `Interface` sits in the band `DeriveGameVersion` maps it to.** A TOC in the wrong band doesn't error, it routes a whole flavour down another expansion's code path. `Interface` is parsed as the comma-separated list it is, not via a bare `tonumber`.

- **Retail TOC interface list pruned to `110207, 120008`** -- `TOGTools_Mainline.toc` had accumulated `120000, 120001, 120005, 120007` alongside the 11.x floor. The 11.x entry stays so the addon keeps loading on The War Within; the 12.x side now tracks only the current Midnight build, so bumping it is an edit rather than an append. `toc_spec.lua` pins that shape -- exactly one pre-12.0 entry and exactly one 12.x entry -- so the list can't grow back.

---

Older releases (v0.8.1 and earlier) are archived in [CHANGELOG_ARCHIVE.md](CHANGELOG_ARCHIVE.md).
