Guild Bank Ledger

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

File Details

v0.30.4

  • R
  • Apr 30, 2026
  • 341.75 KB
  • 52
  • 12.0.5
  • Retail

File Name

GuildBankLedger-v0.30.4.zip

Supported Versions

  • 12.0.5

GuildBankLedger

v0.30.4 (2026-04-30)

Full Changelog Previous Releases

  • Merge pull request #10 from RussellFeinstein/chore/v0.30.4-release
    v0.30.4: combined Layout tab + Sort planner/executor release
  • Merge branch 'main' into chore/v0.30.4-release
  • Merge pull request #11 from RussellFeinstein/chore/branch-lifecycle-rule
    Adopt freeze-after-PR rule for single-purpose branches
  • Adopt freeze-after-PR rule for single-purpose branches
    Renames the Branch Workflow subsection from "Short-lived branches
    (one-off, off main)" to "Single-purpose branches (frozen after PR
    closes)" and replaces the "deleted after merge" closing paragraph
    with the freeze contract: branches are retained in git locally and
    remotely, accept no new commits once their PR closes, and can be
    unfrozen later by explicit user decision. Adds a pointer to the
    global rule in ~/.claude/CLAUDE.md, which carries the full taxonomy
    and the agent-side check.
    The freeze model removes the up-front "is this work one-off or the
    start of an area?" decision: if a single-purpose branch grows into
    recurring work, the user can promote it later. No work is lost to a
    mistaken "this is done" call.
    Repo-internal doc change only: no addon behavior change, no version
    bump, no CHANGELOG entry per feedback_no_bump_for_trivial_docs.md.
  • Merge layout-sort into chore/v0.30.4-release for combined v0.30.4
    Bundles ui's Layout tab work (nested tabs, save bar, bulk-apply) and
    layout-sort's planner/executor work (Phase 0 overflow pre-merge,
    max-stack invariant, src-drained predicate, capacity-aware Phase 1B,
    per-phase + per-op instrumentation) into a single 0.30.4 release.
    Conflict resolution: VERSION, .toc, Core.lua VERSION constant, and
    spec/core_spec.lua assertion all picked 0.30.4. CLAUDE.md kept
    layout-sort's SortPlanner/SortExecutor architecture rewrites.
    CHANGELOG.md and UI/ChangelogView.lua merged both halves under one
    0.30.4 entry; the [0.30.5] heading is dropped (its content folds into
    [0.30.4]). Layout-sort commit subjects keep their "(v0.30.5)" suffix
    for archaeology; the tag is authoritative.
    Tests: 1047 pass, 0 fail. Lint: 0 warnings.
  • Merge ui into chore/v0.30.4-release: layout nested tabs, save bar, bulk-apply
  • Sort: enforce max-stack invariant + verify src drained (v0.30.5)
    Targets the bug confirmed by the previous diagnostic captures: Phase 4
    packing emitted cascades that built up illegal x400 stacks in the
    planner's working state of items with maxStack 200, then the WoW server
    refused the merges and the executor's [sync] success path advanced past
    no-op pickup-drops because the optimistic client view made them look
    identical to real moves.
    Two layered fixes plus a mock change:
    Tier B (planner correctness)
    - canExecute(op, state, getMaxStack) refuses same-item dst merges
    that would exceed maxStack.
    - applyOpToState(state, op, getMaxStack) asserts on the same
    condition (catches future bugs that bypass canExecute).
    - getMaxStack threaded through emitAssignment, greedyDrain, the
    Phase 2 pivot move, and the Phase 3 sweep emission. Optional;
    when nil (cold cache) the guard is skipped, preserving the
    per-item graceful-degradation semantic.
    - Net result: Phase 4 cascades that would over-stack get reordered
    naturally by greedy drain (each op fires only once its dst is
    truly empty / has capacity), or bail via the existing pivot/cycle
    paths if no resolution is possible.
    Tier A (executor correctness)
    - New srcDrainedAsExpected(w) predicate: for "move" ops src must be
    empty or hold a different item; for "split" ops src.count must
    have decreased by at least op.count. Reuses the already-captured
    state.waiting.srcPreOp / opLabel snapshots.
    - Wired into all four advance paths: [sync] in step(), async
    OnSlotsChanged success branch, [late-poll] timeout-completion
    branch, and the late-ACK reclassify branch (latter required
    extending state.lastTimedOutOp to carry src info too).
    - When the predicate fails, audit a "no-op suspected [<branch>]"
    line and fall through to the timeout-poll path which records the
    op as a real failure rather than a phantom success.
    - OnSlotsChanged async branch additionally gated on cursor empty -
    the pickup half of a Pickup pair fires its own event with src
    empty and cursor held; without this guard the executor would
    advance mid-operation before the drop has resolved.
    Mock change (spec/mock_wow.lua)
    - PickupGuildBankItem drop logic now respects MockWoW.itemNames[id]
    .stackCount when set: refuses same-item merges past maxStack and
    bounces the cursor item back to its source slot, mirroring real
    WoW. Opt-in via stackCount being set; existing tests that don't
    set stackCount keep the legacy "merge unconditionally" behavior
    (matches the cold-cache fallback semantic). Verified all 1030
    pre-existing tests still pass after this change in isolation.
    Tests: 1030 -> 1038 (+5 Tier B, +3 Tier A). Lint clean.
    Continues the v0.30.5 layout-sort batch - no version bump.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sort: detect server-rejection rollback as foreign activity (v0.30.5)
    Targets the bug surfaced by the previous diagnostic pass: ops report
    [sync] success because the WoW client optimistically updates its bank
    view the instant Pickup is called, but the server later rejects the
    move and sends a rollback event. The executor's foreign-activity
    branch was treating that rollback as "something else changed the
    bank, replan" - which is correct behavior but lost the signal
    that the previous op was a phantom.
    Three layered additions:
    1. Pre-op slot snapshots
    state.waiting now carries srcPreOp / dstPreOp captured via a new
    snapshotLiveSlot helper at the moment the executor arms waiting,
    i.e., the live bank state right before issuing Pickup.
    2. Per-op-success line shows observed src/dst post-state
    auditOpSuccess now suffixes the line with src=<post> dst=<post>
    so the audit trail records what the client believes the slots
    hold immediately after the op. In the failing repro this lets
    a reader confirm "client says src empty, dst occupied" - then
    the next bullet identifies whether the server agreed.
    3. Foreign-activity branch detects reversion
    auditOpSuccess also stashes state.lastCompletedOp with the
    projected post-op state for src and dst. When OnSlotsChanged
    fires the foreign-activity branch (state.waiting nil, no late
    ACK match), it now reads live slot state and compares against
    the projection. If either slot diverges, the audit gets a
    Sort: server reversion suspected on op N (move T<src>->T<dst>)
    line followed by per-slot projected X, observed Y diffs.
    The replan still fires either way; the difference is whether the
    audit names the rejected op so a follow-up fix can target it (e.g.
    the [sync] success path is the obvious candidate to gate, since
    that's the path that advances on optimistic state).
    Tests: 1028 -> 1030 (+1 reversion-detected, +1 no-reversion-when-clean).
    Lint clean.
    Continues the v0.30.5 layout-sort batch - no version bump.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sort: planner-vs-reality + per-op timeline diagnostics (v0.30.5)
    Targets the gap surfaced by the previous instrumentation pass: when
    "dst occupied by wrong item" failures recur across replans, the audit
    trail couldn't tell whether the snapshot read the slot wrong or whether
    an earlier op didn't actually do what the planner projected.
    Five additions, all routed through the existing audit pipe:
    1. plannerSrcAt / plannerDstAt on every emitted op
    A frozen view of what state[srcTab][srcSlot] and state[dstTab][dstSlot]
    held at emit time. Threaded through emitAssignment, the Phase 2 pivot
    move, and the Phase 3 sweep emission via a snapshotSlot helper.
    2. Pre-check-fail audit shows planner-expected slot
    Both src and dst pre-check failures now log a "planner expected ...
    at emit" line that pairs with the existing "got" line. A divergence
    between the two directly identifies the earlier op that the planner
    thought would clear/transform the dst but in practice didn't.
    3. Per-op success timeline
    A new auditOpSuccess helper emits one terse line per advance, tagged
    [sync] / [late-poll] depending on which success path resolved it.
    Combined with the pre-check-fail / timeout entries, /gbl synclog now
    serves as a complete per-op timeline of any sort run. state.waiting
    gains srcTab/srcSlot/opLabel fields so the line has src->dst context.
    4. Pass 2b adjacency labels
    With slotOrder empty, every demand previously fell to "first-empty",
    hiding the natural seed+extend structure of an items-only layout.
    Pass 2b now re-checks claimedByItem adjacency for each add: a 5-slot
    item produces 1 first-empty seed + 4 extend-right rather than 5
    first-empty. Behavior unchanged - just labels.
    5. Item-name resolution in audit lines
    New GBL:DescribeItem(itemID) helper renders cached items as
    "<name> (it:NNN)" and falls back to bare "it:NNN" cold. Used by
    describeSlot, describePlannerSlot, and every audit format string
    that mentioned an item ID. Helper deliberately does not warm the
    cache so audit emission cannot trigger async loads.
    Also fixes two em dashes in user-facing audit format strings (the run
    summary and the late-ACK reclassify message), per the no-em-dashes rule.
    Tests: 1024 -> 1028 (+1 items-only seed+extend, +3 plannerSrcAt/DstAt).
    Lint clean.
    Continues the v0.30.5 layout-sort batch - no version bump.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sort: per-phase planner + executor instrumentation (v0.30.5)
    Adds two diagnostic layers to the sort path:
    SortPlanner.lua
    - Tracks per-phase counters (Phase 0 merges, Phase 1a assigns,
    Phase 1b spills by mode, Phase 2 pivots, Phase 3 sweeps, Phase 4
    position shifts) and demand origins (pinned/ext-R/ext-L/first-empty).
    - Emits two extra audit lines whenever a plan has work, on top of the
    existing "Sort plan:" timing line:
    phases: P0 merge=N(free=M) P1a assign=N P1b spill=N(top=,r=,l=,fe=,unp=)
    P2 pivot=N(abort=M) P3 sweep=N P4 pack=N
    demands: N total (pinned=, ext-R=, ext-L=, first-empty=)
    - Empty/no-op plans stay quiet (replans on a clean state don't spam).
    - The same counters are exposed on the returned plan as plan.diag
    so tests and UI consumers can read them without scraping audit text.
    SortExecutor.lua
    - Tracks reclassified count (late-ACK), preCheckFails (replan-trigger
    pre-check failures), cursorStuck count, and a timeoutByClass split
    (none/partial/complete/other).
    - Emits a single completion-summary line on every run end:
    Sort: complete in 24.3s — N ops (D done, F failed, R replans, X reclass)
    preCheck=P cursor=C timeout[n=,p=,c=,o=] avg N.Ns/op
    - Same counters land on the onComplete result table.
    Tests
    - sortplanner_spec: 7 new cases covering empty-plan zero counters,
    Phase 0 merge+free, Phase 1a vs Phase 1b separation, the four
    Phase 1b modes, phase1bUnplaced when overflow is full, Phase 4
    position-shift count, and demand-origin counts matching the
    existing demandMap test fixture.
    - sortexecutor_spec: 2 new cases verifying onComplete carries the
    counters and reclassified bumps under the existing
    _sortExecutorInjectTimeout late-ACK path.
    Total: 1024 tests pass (was 1015), lint clean.
    Continues the v0.30.5 layout-sort batch — no version bump.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sort: pre-merge overflow + capacity-aware Phase 1B routing (v0.30.5)
    Fixes the cascade where the stock tab is reported 'out of space'
    because same-item partial stacks consume slots that could be merged.
    Sorting the stock tab is now active throughout the planner, not just
    the last step.
    • Phase 0 (new): pre-merges same-item partials within the overflow
      tab BEFORE any cross-tab routing. Two-pointer pour up to the
      per-item max stack size. Compacts the tab to its minimum slot
      count so Phase 1B sees maximum free capacity.
    • Phase 1B (refactored): pickOverflowSlot is capacity-aware. Order
      is now (1) top up an existing same-item partial slot, (2) right-
      extend, (3) left-extend, (4) first-empty. The supply loop iterates
      while sup.available > 0 so a single supply may split across a
      topup destination plus a fresh slot.
    • Phase 3 (sweep): same multi-destination loop pattern as Phase 1B.
    • Phase 4 (compaction): merge sub-phase removed (Phase 0 covers it);
      Phase 4 now does position-only packing.
      Refactor: state deep-copy + emitAssignment + getMaxStack hoisted to
      the top of PlanSort so Phase 0 can use them. supplies now built
      from POST-Phase-0 state (not bank). Old overflowVirtual replaced by
      overflowSlotInfo with {itemID, count, capacity} per slot.
      Cold cache (unknown maxStack) is the conservative fallback: capacity
      treated as 0, top-up never fires, behavior matches pre-Phase-0
      routing. A follow-up sort once item info loads completes the work.
      Bundled into v0.30.5 with the prior two commits on this branch (no
      separate version bump; CHANGELOG and ChangelogView entries extend
      the existing 0.30.5 block).
      7 new tests added (1015 total). luacheck clean.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sort: planner timing diagnostic in audit trail (v0.30.5)
    Every PlanSort call now writes one line to the audit trail of the
    form 'Sort plan: 12.3ms, 47 ops, 0 deficits, 1 unplaced (input: 240
    slots / 4 tabs)'. Visible via /gbl synclog. Captures both first-plan
    and replan latency on the same code path.
    Motivation: large plans and the replans triggered when SortExecutor
    detects foreign activity cause a single-frame hitch in-game. This
    line gives the ms-per-input-size data points needed to decide whether
    the planner should be split across frames via coroutines, and which
    sub-phase to instrument next.
    Folded into the same v0.30.5 release as the overflow merge feature
    in the prior commit; no separate version bump.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Sort: merge partial overflow stacks up to per-item max stack (v0.30.5)
    Phase 4 of SortPlanner now reads per-item stack size from ItemCache (or
    opts.maxStackByItem in tests) and pours partial stacks together within
    each same-item run on the overflow tab, so a run that previously ended
    as [160, 160, 100] of Healing Potions (max 200) now ends as [200, 200,
    20]. Items whose stackCount has not yet loaded fall back to grouping
    only and merge on a follow-up sort. Repeat sorts of an already merged
    run produce zero ops.
    ItemCache now caches the 8th return of GetItemInfo (itemStackCount)
    alongside name and link, with a new GBL:GetMaxStack(itemID) accessor.
    PlanSort gains an optional opts arg for tests.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Promote overflow-stack-merge and audit-log-upload plans into docs/
    Move two design plans from machine-local ~/.claude/plans/ into the repo so
    they survive across machines and can be referenced from durable docs:
    • docs/PLAN-overflow-stack-merge.md
    • docs/PLAN-audit-log-upload.md
      Internal-only addition. No addon code, .toc, or user-facing docs touched.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Add Layout bulk-apply: set slots/perSlot for all items on a tab (v0.30.4)
    A common Layout-tab workflow is "every item on this tab should have
    the same shape" — e.g. every gem on a gems tab should be 5 slots × 1
    per slot. Editing each per-item row by hand for tabs with dozens of
    items is tedious. This adds a "Set all items to:" row above the
    per-item list with two inputs (Slots, Per slot) and an Apply to all
    button.
    Behavior:
    • Either field can be left blank to keep that field's current value
      for each item — supports bulk-set of just slots, just per-slot, or
      both.
    • Shrinking an item's slot count trims that item's slotOrder pins
      from the highest slot down, mirroring the per-row OnEnterPressed
      shrink logic. Pin removal is per-item — one item's pin-shrink does
      not bleed into another item's pins.
    • Hidden when the tab has no items (nothing to apply to).
    • Edits buffer in the layout draft like any other Layout edit; click
      Save Layout to commit and broadcast.
      Implementation:
    • Pure helper applyBulkToItems(tab, newSlots, newPerSlot, maxSlots)
      exposed as GBL._layoutEditorApplyBulkToItems for the spec suite.
      Mirrors the per-row trim algorithm so the helper and the existing
      per-row callback agree on shrink semantics.
    • 9 new tests cover empty input, both fields, slots-only, perSlot-
      only, shrink-with-pin-trim (per-item scoped), no-trim-on-grow, nil
      defenses, and missing-slotOrder lazy creation.
      CHANGELOG and ChangelogView entries describe the new bulk-apply
      workflow.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Move Layout save bar to top of scroll instead of anchored footer (v0.30.4)
    The previous attempt to pin the save bar as an anchored footer
    (ClearAllPoints + four explicit SetPoints on both the SimpleGroup and
    the ScrollFrame) collided with AceGUI's internal anchoring and broke
    the scroll's content area entirely — every bank tab rendered with
    just the save bar and no items / mode dropdown / slot map visible.
    Reverting to the simple BuildTransactionsTab-style fill-remainder
    ScrollFrame and rendering the save bar as the FIRST child inside it.
    That gives:
    • Save / Discard always reachable via a single scroll-to-top, on every
      bank tab regardless of how long the items list and slot map get.
    • No anchor-vs-AceGUI fights — uses the same SetFullWidth +
      SetFullHeight + BOTTOMRIGHT-anchor pattern that BuildTransactionsTab
      has used reliably for years.
    • Per-tab scroll status tables still preserve scroll position across
      rebuilds and inner-tab switches.
      In-code comments document why save-bar-at-bottom (the natural choice)
      doesn't work in this layout: AceGUI content-height bookkeeping in the
      deeply nested layout buries trailing widgets, and anchored-footer
      breaks the ScrollFrame.
      CHANGELOG and ChangelogView entries updated to describe top placement.
      Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Pin Layout tab save bar to a footer so it stays visible (v0.30.4)
    After the v0.30.4 nested-tabs refactor, the Save / Discard buttons
    appeared only on tabs in overflow / ignore mode (where the tab content
    is short) and silently vanished on display-mode tabs (where the items
    list, slot map, and add-item row push the save bar below the
    viewport). The status banner above the buttons rendered fine because
    it was the last full-width child the layout fully accounted for, but
    the SimpleGroup row holding the buttons was effectively unreachable —
    content-height bookkeeping in a deeply-nested AceGUI scroll layout
    falls over in this mid-content arrangement.
    Fix: lift the save bar out of the per-tab ScrollFrame and pin it as a
    footer at the bottom of the inner tab's content frame. The ScrollFrame
    above it is anchored TOPLEFT/TOPRIGHT to parent.content and
    BOTTOMLEFT/BOTTOMRIGHT to the save bar's top, so the two tile cleanly.
    The save bar height is fixed at 50px (status banner row + button row)
    which doesn't depend on AceGUI auto-sizing kicking in before the
    anchor takes effect. Sort Access keeps its full-height scroll (no save
    bar — it has its own immediate-save semantics).
    CHANGELOG and ChangelogView entries for v0.30.4 updated to call out
    the always-visible footer.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
  • Refactor Layout tab to use nested tabs (v0.30.4)
    The Layout tab's monolithic vertical scroll over eight bank-tab sections
    plus a Sort Access section becomes an inner TabGroup: one inner tab per
    bank tab (Tab 1..Tab 8) plus a final Sort Access tab. Editing one bank
    tab at a time keeps slot maps and item lists short, and Sort Access
    policy gets its own focused screen.
    The active inner tab persists across rebuilds (self._layoutInnerTab) so
    edits do not bounce the view back to Tab 1. Each inner tab keeps its
    own scroll position via a per-tab status table, so switching back to a
    tab returns the user to where they were.
    Mouse-wheel scrolling works inside every inner tab because the
    ScrollFrame lives inside each inner tab's content rather than wrapping
    the whole TabGroup; this is the canonical fill-remainder pattern used
    by BuildTransactionsTab and friends in UI/UI.lua.
    Save and Discard live at the bottom of each bank-tab inner tab and
    operate on the full draft (changes across all bank tabs save together).
    Sort Access keeps its own immediate-save semantics.
    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com