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 withsrc=<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-slotprojected X, observed Ydiffs.
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 asnapshotSlothelper.
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 asplan.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
_sortExecutorInjectTimeoutlate-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
- Phase 0 (new): pre-merges same-item partials within the overflow
- 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
- Either field can be left blank to keep that field's current value
- 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
- Save / Discard always reachable via a single scroll-to-top, on every
- 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