Guild Bank Ledger

*BETA* - Persistent guild bank transaction logging with automatic multi-player sync.

File Details

v0.30.5

  • R
  • May 5, 2026
  • 350.74 KB
  • 16
  • 12.0.5
  • Retail

File Name

GuildBankLedger-v0.30.5.zip

Supported Versions

  • 12.0.5

GuildBankLedger

v0.30.5 (2026-05-05)

Full Changelog Previous Releases

  • Docs: append Codex P2/P3 follow-up bullets to v0.30.5 changelog
    Doc-only follow-up to PR #13. CHANGELOG.md and UI/ChangelogView.lua CHANGELOG_DATA v0.30.5 entries gain two parallel bullets describing the Codex P2/P3 follow-up fixes that landed in code in commits a4ed3b0 + dd3d633. No code changes, no version bump.
  • Docs: append Codex P2/P3 follow-up bullets to v0.30.5 changelog
    The v0.30.5 PR (#13) merged with the Codex P2 (Core.lua
    MigrateRecoverPeerRealms realm normalization) and Codex P3 (Sync.lua
    InitSync seed recency check) fixes in code (commits a4ed3b0 and dd3d633
    within #13), but the CHANGELOG.md and UI/ChangelogView.lua CHANGELOG_DATA
    v0.30.5 entries only described the original 8-commit work and the
    Codex P1 follow-ups. Adding two parallel bullets to each so the
    user-facing changelog matches the shipped code before the v0.30.5 tag
    goes out.
    Doc-only. No code changes. Tests 1135 / 0 / 0 / 0. Lint clean.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: realm-aware peer keys + record realm normalization (v0.30.5)
    Replaces broken Ambiguate(name, "none") at 16 sites with GBL:CanonicalPeerKey, which uses custom local-realm-only strip via _isLocalRealm so two distinct same-name characters across connected realms keep distinct peer keys. Adds schema migrations 8 -> 9 (realm-aware peer-key collapse), 9 -> 10 (normalize stored realm strings + rebuild record.id and seenTxHashes), and 10 -> 11 (best-effort recovery for users on the intermediate strip-everything build). BuildRosterCache tracks bare-name ambiguity via a false sentinel. RepairCorruptedPlayerRealms trims hyphen-corrupted realm strings. ConsolidatePeerKeys runtime sweep on GUILD_ROSTER_UPDATE. ResolvePlayerName cross-guild fallback removed. Codex P1/P2/P3 review findings all addressed.
    Tests 1135 / 0 / 0 / 0. Lint clean.
  • Sync: prose cleanup, fix em dashes on PR-added lines
    Replace em dashes with periods, semicolons, or colons on every PR-added
    line in CHANGELOG.md, CLAUDE.md, and docs/PROMPT-quality-audit.md, per
    the global style rule. Pre-existing em dashes on lines this PR did not
    touch are left as-is (the rule only applies to lines being written or
    edited).
    Also: rename "highest-leverage" to "highest-impact" in
    docs/PROMPT-quality-audit.md (the "leverage" term is on the AI-vocab
    banlist).
    No code changes. Tests + lint unaffected.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: address Codex P2 + P3 follow-up
    Two fixes for the second Codex review pass on this PR.
    F3 (Codex P2): MigrateRecoverPeerRealms (schema 10 -> 11) was building
    its bare-name -> realm lookup from GetGuildRosterInfo without normalizing
    the realm portion. GetGuildRosterInfo can return raw spaced realm names
    ("Aerie Peak") for cross-realm guildmates depending on the realm topology.
    The unnormalized realm then flowed into CanonicalPeerKey at the rewrite
    site, which preserved the cross-realm form as-is. Result: recovered keys
    like "Alice-Aerie Peak" while every other call site of CanonicalPeerKey
    produced "Alice-AeriePeak", silently splitting the same peer across two
    keys after the recovery migration. This is exactly the duplicate-peer-key
    scenario the recovery exists to fix, surviving for users on multi-word
    realms (Aerie Peak, Burning Blade, Argent Dawn, etc.).
    Fix: normalize realm once after the no-base fallback at Core.lua:1442,
    before storing in lookup or the rewrite call.
    F4 (Codex P3): InitSync's knownPeers seed loop wrote to
    syncState.peers[clean] unconditionally per pairs() iteration. When
    knownPeers contains both legacy bare and canonical qualified forms of
    the same peer (the upgrade scenario this PR addresses), both raw keys
    canonicalize to the same clean key, and pairs() iteration order is
    undefined, so an older snapshot can nondeterministically overwrite a
    newer one in the runtime cache. The knownPeers consolidation right
    below already had a recency check; the syncState.peers write did not.
    Fix: wrap the syncState.peers write in the same recency check, mirroring
    the knownPeers pattern.
    Tests: 1 new for F3 (raw spaced realm in roster recovers to normalized
    canonical key) + 2 new for F4 (recency wins in both directions: bare
    older / qualified newer, and bare newer / qualified older). Full suite
    at 1135 / 0 / 0 / 0. Lint clean.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: ConsolidatePeerKeys + early corruption repair fix cold-startup race
    In-game smoke after the last commit revealed peers still keyed bare even
    though CanonicalPeerKey worked correctly when invoked directly. Root cause:
    the InitSync seed loop runs in OnEnable BEFORE GUILD_ROSTER_UPDATE fires,
    so on first reload after a corruption-bearing session, playerRealms was
    still corrupt at seed time. CanonicalPeerKey rejected the hyphen-bearing
    realm and fell through to bare passthrough. Bare entries got written to
    syncState.peers and knownPeers; subsequent /reload after the corruption
    was repaired-on-disk still seeded from already-bare knownPeers (the seed
    loop's clean == name short-circuit didn't trigger consolidation).
    Two-part fix:
    1. Move RepairCorruptedPlayerRealms call into MigrateAllGuilds so it runs
      for every guild's playerRealms before the migration ladder AND before
      InitSync's seed loop. Closes the cold-startup window — by the time the
      seed loop reads playerRealms, corrupt entries have been trimmed to
      their first hyphen-free segment.
    2. New GBL:ConsolidatePeerKeys (Sync.lua) walks syncState.peers and
      guildData.knownPeers, re-runs CanonicalPeerKey on each key, and merges
      stale-form entries into their current canonical form by recency.
      Idempotent. Called from GUILD_ROSTER_UPDATE in Core.lua after
      BuildRosterCache + the migration retrigger, so any peer-state staleness
      from earlier in the session (or from prior sessions whose canonicalization
      differed) sweeps to current canonical form once the roster is warm.
      Tests: 5 new ConsolidatePeerKeys cases (bare→qualified rewrite in syncState
      and knownPeers, recency-merge collisions, no-op for same-realm bare, idempotent).
      One existing GUILD_ROSTER_UPDATE retrigger test adjusted to use a knownPeers
      entry that doesn't incidentally consolidate (the test was checking schema
      version stayed at 10; the consolidation now runs every event but is
      orthogonal to the migration retrigger). Full suite at 1132 / 0 / 0 / 0.
      Lint clean.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: name-handling robustness audit (cross-realm + cross-guild)
    Audit pass on the full name-handling surface after the connected-realm
    runtime fix landed. Three concrete improvements plus doc cleanup.
    1. ResolvePlayerName: drop the cross-guild fallback (Core.lua:290-297).
      Previously, when a bare name wasn't in the current guild's playerRealms,
      the function iterated self.db.global.guilds and used any other guild's
      mapping it found. For users who'd been in multiple guilds, this could
      resolve a bank-log name to a realm from a guild the player no longer
      belongs to. New behavior: explicit pr arg → current guild → local
      realm fallback. Migrations still pass per-guild tables explicitly.
    2. BuildRosterCache: track bare-name ambiguity via a false sentinel.
      When two guildmates share a bare name on different realms (e.g. an
      Alice on Stormrage and a different Alice on Area52 in one connected-
      realm guild), last-write-wins would let CanonicalPeerKey false-re-realm
      bare arrivals to whichever realm won the race. Two-pass design now
      collects distinct realms per bare name; ambiguous bare names get
      false. CanonicalPeerKey type-checks for string realm and treats the
      sentinel as "unresolvable, keep bare." RepairCorruptedPlayerRealms
      already type-checked, so it leaves false untouched.
    3. CanonicalPeerKey: explicit type(realm) == "string" guard for clarity.
      The earlier if realm and not realm:find("-") short-circuited correctly
      on false, but the explicit type check makes the design intent
      readable and protects against any future non-string sentinels.
      Doc cleanup: removed stale Ambiguate("guild") references in docstrings
      and Sync.lua comments now that production no longer calls Ambiguate.
      Added a "Name forms quick reference" table to project CLAUDE.md so the
      qualified / canonical-peer-key / bare distinction is easy to look up.
      Tests: 9 new (3 ResolvePlayerName cross-guild isolation + 4 BuildRosterCache
      ambiguity tracking + 2 CanonicalPeerKey under ambiguous bare lookup).
      Full suite at 1127 / 0 / 0 / 0. Lint clean.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: custom local-realm-only strip in CanonicalPeerKey
    In-game smoke confirmed that retail's Ambiguate("guild") strips realm for
    ALL guildmates of a connected-realm group, not just same-realm. That
    collapses two distinct same-name characters across connected realms (e.g.
    Alice-Stormrage and Alice-Area52 on a Tichondrius user) into one peer key.
    Replace the Ambiguate("guild") delegation with a custom strip that only
    fires when the realm equals the local realm (raw or normalized). Cross-
    realm suffixes are preserved so connected-realm guildmates with the same
    first name stay distinct.
    Implementation:
    • New GBL:_isLocalRealm(realm) helper centralizes the comparison.
    • CanonicalPeerKey: qualified input strips iff _isLocalRealm; bare input
      consults playerRealms and re-realms cross-realm bare names to Name-Realm.
      Behavioral diff vs the previous Ambiguate("guild") delegation:
    • Same-realm peers: unchanged (still bare).
    • Cross-realm-but-connected-realm peers: now keyed as Name-Realm where
      before they collapsed to bare. Distinguishes same-name distinct
      characters across connected realms.
    • Bare arrivals: re-realmed via playerRealms when known.
    • Same-name peers across NON-connected-realm cross-realm boundaries: would
      preserve before too; unchanged.
      The InitSync knownPeers seed loop already runs every persisted key
      through CanonicalPeerKey, so existing saved variables self-heal to the
      new keying scheme on next /reload.
      Tests: 2 new (connected-realm distinct-character + bare/qualified
      convergence). Mock Ambiguate("guild") behavior is now unused by
      CanonicalPeerKey but still implements local-realm-only strip. Full suite
      at 1118 / 0 / 0 / 0. Lint clean.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: repair hyphen-corrupted playerRealms entries
    Smoke test revealed an old corruption hiding in playerRealms: an offline
    guildmate's entry held "Stormrage-Stormrage-Stormrage..." (a realm string
    with embedded hyphens). Origin is a long-fixed code path; BuildRosterCache
    only writes for currently-rostered members, so corrupt entries for offline
    or departed peers persist indefinitely.
    Two layers:
    • RepairCorruptedPlayerRealms walks playerRealms once per BuildRosterCache
      invocation. A realm string containing a hyphen is treated as corrupt
      (retail realms never contain hyphens); the helper trims it to the first
      hyphen-free segment, or removes the entry if no recoverable segment
      exists. Idempotent.
    • CanonicalPeerKey defensively rejects a hyphen-bearing realm and falls
      back to bare passthrough. Belt-and-suspenders for the case where the
      cleanup hasn't run yet (e.g. between OnEnable and first GUILD_ROSTER_UPDATE).
      Project CLAUDE.md updated to reflect the empirical Ambiguate("guild")
      behavior in connected-realm guilds: it strips realm for ALL guildmates
      regardless of realm, eliminating same-character duplicate keys at the cost
      of leaving same-name distinct-character connected-realm collisions
      unresolvable. Earlier "preserve cross-realm suffix" framing was overclaimed.
      Tests: 6 new (4 RepairCorruptedPlayerRealms + 1 BuildRosterCache wires it
    • 1 CanonicalPeerKey defensive check). Full suite at 1116 / 0 / 0 / 0.
      Lint clean.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: route bare peer names through playerRealms in CanonicalPeerKey
    Smoke test of PR #13 surfaced a connected-realm gap: the same character's
    messages can be keyed under both bare and qualified forms in syncState.peers,
    producing duplicate entries in the Online Peers list. Two interacting causes:
    1. MigrateRecoverPeerRealms premature-bump. When GetLocalRealm() is valid
      but GetNumGuildMembers() returns 0 (cold roster), the migration walked an
      empty roster, recovered nothing, and still bumped schemaVersion to 11.
      Affected users are now stuck at 11 with the recovery never having run.
    2. Ambiguate(name, "guild") cannot re-realm a bare name. Stale bare keys in
      knownPeers propagate through Sync.lua's seed loop into syncState.peers,
      keyed alongside qualified arrivals from current HELLO traffic.
      Fix:
    • CanonicalPeerKey now consults guildData.playerRealms for bare input. Same
      character hashes identically regardless of arrival form. Bare names with
      no roster mapping pass through unchanged.
    • MigrateRecoverPeerRealms returns 0 without bumping when numMembers == 0,
      so the migration retries on a later session.
    • GUILD_ROSTER_UPDATE retriggers MigrateAllGuilds once on first warm-roster
      fire (gated by _migrationsRetried), closing the cold-roster gap without
      requiring another login.
    • InitSync's knownPeers seed loop runs each persisted key through
      CanonicalPeerKey and consolidates knownPeers in place when a stale key
      collapses to a new canonical form. Self-heals stuck-at-11 saved variables
      on next /reload.
      Tests: 13 new (5 CanonicalPeerKey roster fallback + 1 cold-roster
      premature-bump + 3 GUILD_ROSTER_UPDATE retrigger + 4 InitSync seed
      consolidation). Full suite at 1110 / 0 / 0 / 0. Lint clean.
      No version bump per the bundle-and-PR rule; this work folds into PR #13's
      v0.30.5 stamp at PR-merge time.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: strict-gate later migrations + recompute record.id after realm rewrite
    Two P1 fixes from Codex review of #13.
    1. Skip-chain prevention. MigrateNormalizePeerNames short-circuits on
      cold realm APIs (schemaVersion stays at 8). Both later migrations
      used loose >= target gates, which let them bump straight from
      schema 8 to 10 / 11 in that case, permanently skipping the 8 -> 9
      work on the next session. Tightened MigrateNormalizeStoredRealms
      to require schemaVersion == 9 and MigrateRecoverPeerRealms to
      require schemaVersion == 10. Strict equality keeps the chain in
      order and lets cold-start sessions retry on the next load.
    2. Recompute record.id after rewriting record.player.
      ComputeTxHash includes the player field, so any record whose
      player gets normalized has a stale id afterwards. Without rebuild,
      sync would re-import the same transaction under a new id and
      seenTxHashes would not catch the duplicate. The migration now
      recomputes record.id inline whenever it mutates record.player and
      rebuilds seenTxHashes once at the end, mirroring the pattern in
      MigrateRepairEpochTimestamps.
      Tests: 1097 / 0 / 0 / 0 (1092 baseline + 5 new across strict-gate
      behavior on schemas 8 and 9, record.id recomputation, and a negative
      test confirming seenTxHashes is not rebuilt when no record.player
      changed).
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Add docs/PROMPT-quality-audit.md (agent-runnable quality-item audit)
    A self-contained agent prompt for evaluating four potential infra/quality
    investments against this repo's actual code paths: perf benchmarks, memory
    audit, mutation testing, and a second linter (selene). Each item gets KEEP
    / MAYBE / DROP framing with acceptance criteria. The prompt was originally
    drafted in the context of deciding whether the items that were rejected as
    premature for RCLootCouncil_PriorityLoot would land differently here, where
    the sync protocol, planner, and audit-log surface make this addon a much
    better candidate for actual perf and memory investment.
    Repo-internal doc, no version bump or CHANGELOG entry per the
    no-bump-for-trivial-docs convention.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sync: realm-aware peer keys + record realm normalization (v0.30.5)
    Three related fixes for the WoW realm-name pipeline, all triggered by
    the in-game observation that rexxybear was showing up as multiple
    realm-tagged entries in the Online Peers list despite only being on
    Tichondrius.
    1. Peer-identity canonicalization. The sync layer used
      Ambiguate(name, "none") at sixteen sites intending to strip realm
      from incoming sender names, but in retail "none" returns the name
      unchanged ("guild" is the realm-aware strip). All sixteen sites now
      route through a new GBL:CanonicalPeerKey helper which wraps
      Ambiguate("guild"). Same-realm senders collapse to bare names so
      the rexxybear bloat goes away; cross-realm senders keep their realm
      suffix so connected-realm same-name peers stay distinct.
      Schema-9 migration is realm-aware: collapses same-realm only.
      Schema-11 MigrateRecoverPeerRealms is a best-effort recovery for
      anyone who ran the intermediate development build that
      unconditionally stripped realm.
    2. Record realm normalization. BuildRosterCache and the v2-to-v3
      migration stored the realm portion of player names raw
      ("Aerie Peak"), while ResolvePlayerName's local-realm fallback
      produced the normalized form ("AeriePeak"). The asymmetry let the
      same player surface as two different record.player values across
      the dedup boundary on realms with whitespace in their name. New
      GBL:NormalizeRealm helper plus schema-10 MigrateNormalizeStoredRealms
      rewrites stored realms in place. Local-realm fallback centralized
      in new GBL:GetLocalRealm.
    3. Connected-realm IsGuildMemberOnline disambiguation. The function
      compared stripped name against stripped roster fullName, which
      would return the wrong online state for two same-name members on
      different realms in a connected-realm guild. Now compares both
      sides through CanonicalPeerKey.
      Test mock for Ambiguate now matches retail semantics across all four
      contexts ("none", "all", "short", "guild") with both raw and
      normalized realm forms compared. Mock GetGuildRosterInfo returns the
      14th lastLogoff value matching retail. Mock GetNormalizedRealmName /
      GetRealmName correctly diverge on whitespace.
      Test suite: 1092 passing (1080 baseline + 12 net-new across
      CanonicalPeerKey, MigrateNormalizePeerNames realm-aware,
      MigrateRecoverPeerRealms, connected-realm peer disambiguation, plus
      fixture refits for v0.30.5 tests that used Tichondrius against the
      default TestRealm).
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Merge pull request #12 from RussellFeinstein/chore/sync-contributing-md
    Sync CONTRIBUTING.md to single-purpose-frozen branch wording
  • Sync CONTRIBUTING.md to single-purpose-frozen branch wording
    PR #11 (v0.30.4) renamed the short-lived chore/infra/hotfix category to
    "single-purpose, frozen after PR closes" in CLAUDE.md and replaced the
    delete-after-merge contract with the freeze contract. CONTRIBUTING.md's
    maintainer-note paragraph still described the old "short-lived...for
    one-off changes" model, which is now stale.
    Updates the maintainer-note sentence to match the new wording and adds
    "freeze contract" to the cross-reference list. No code, no addon
    behavior change, no version bump per feedback_no_bump_for_trivial_docs.md.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Merge pull request #9 from RussellFeinstein/chore/dev-link-check
    Add check-dev-link.sh dev verification script
  • Add check-dev-link.sh dev verification script
    Detects when the AddOns/GuildBankLedger symlink on a dev machine has
    been silently replaced by a real directory copy, a regression mode
    where WoW loads stale code regardless of repo state. Verifies the
    AddOns entry is a symlink, the link's VERSION matches the repo's
    VERSION, Libs/ exists, and VERSION agrees with the .toc Version field.
    Repo-internal dev tooling. No addon behavior change, no version bump
    per feedback_no_bump_for_trivial_docs.md.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com