File Details
v0.30.1
- R
- Apr 25, 2026
- 337.76 KB
- 42
- 12.0.5
- Retail
File Name
GuildBankLedger-v0.30.1.zip
Supported Versions
- 12.0.5
GuildBankLedger
v0.30.1 (2026-04-25)
Full Changelog Previous Releases
- Merge pull request #5 from RussellFeinstein/infra/record-seen-helper
Extract SafeRecordTimestamp helper, restore Core.lua line-length lint - Extract SafeRecordTimestamp helper, restore Core.lua line-length lint (v0.30.1)
Replaces ten copies of the IsValidTimestamp(record.timestamp) and
record.timestamp or GetServerTime() ternary with a single
GBL:SafeRecordTimestamp(record) helper in Dedup.lua. Affects nine
migration paths in Core.lua and one site in Sync.lua's NormalizeRecordId.
Three previously two-line wrapped sites collapse back to one line.
Removes the .luacheckrc files["Core.lua"] = { max_line_length = false }
override that was added in v0.28.12 as a holding pattern for this
follow-up. Lint now enforces the 120-char limit on Core.lua again.
Adds five regression tests in spec/epoch_timestamp_spec.lua covering
valid timestamp pass-through and four fallback shapes (nil, 0, string,
negative).
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Merge pull request #4 from RussellFeinstein/feature/sort-stock
Sort + Stock initiative: bank layout, sort planner/executor, layout write tier (v0.30.0) - Merge main into feature/sort-stock for PR
Brings the v0.28.10 (PR #1 bottom-frame fix), v0.28.11 (contributor
docs), and v0.28.12 (CI workflow) commits onto feature/sort-stock so
the v0.30.0 layout-write-access work can land via a PR under the new
regime instead of a direct push.
Conflict resolutions, all version-string and changelog interleave:- VERSION, GuildBankLedger.toc, Core.lua, CLAUDE.md, spec/core_spec.lua
keep 0.30.0 (feature/sort-stock's value, which is greater than
main's 0.28.12 so the version chain stays monotonic). - CHANGELOG.md and UI/ChangelogView.lua interleave: HEAD's [0.30.0]
through [0.29.0] entries first, then origin/main's [0.28.12],
[0.28.11], [0.28.10], [0.28.9] in descending order, then [0.28.8]
and earlier from common history.
Side cleanups required by the merged .luacheckrc: - Removed a duplicate GetNormalizedRealmName entry that landed in two
separate sections via the merge. - Added 213/_.* to the ignore list so a
for _, _slot in pairs(...)
loop in Core.lua passes lint, matching the existing 211/212 pattern
for underscore-prefixed unused names.
- VERSION, GuildBankLedger.toc, Core.lua, CLAUDE.md, spec/core_spec.lua
- Merge pull request #3 from RussellFeinstein/infra/ci-workflow
Add GitHub Actions CI for tests and lint (v0.28.12) - Exclude .luarocks and .github from luacheck
CI installs busted and luacheck into .luarocks/ at the repo root via
leafo/gh-actions-luarocks. Without an explicit exclude, luacheck
recursed into those installed deps and produced 1826 warnings, failing
the build. Local runs missed this because LuaRocks lives at
C:\LuaRocks\systree on rex-desktop, outside the repo tree.
Also excluding .github since YAML / Markdown shouldn't be linted as Lua. - Add GitHub Actions CI for tests and lint (v0.28.12)
CI workflow runs busted (tests) and luacheck (lint) on every pull
request and on every push to main. Job is named test-and-lint so
Phase C can require it as a status check.
Side cleanups to make luacheck pass cleanly under CI:- Add GameFontNormalLarge, GetItemInfo, GetRealmName,
GetNormalizedRealmName to .luacheckrc read_globals. They were
already used in production code, just missing from the config. - Ignore warning 542 (empty if branch) globally. The codebase uses
the empty-branch pattern intentionally to document early-out paths
via comments. - Suppress max_line_length for Core.lua and UI/ChangelogView.lua.
ChangelogView holds user-visible strings one line per entry on
purpose. Core has six identical long lines for record-timestamp
recovery; TODO follow-up to extract a SafeRecordTimestamp helper. - Delete a dead
local savedSchemacapture in Core.lua's migration
pass-1 path. It was assigned but never read.
- Add GameFontNormalLarge, GetItemInfo, GetRealmName,
- Merge pull request #2 from RussellFeinstein/infra/contributor-docs
Add contributor docs and PR template (v0.28.11) - Drop em dashes from contributor docs
Replaces em dashes in the new CONTRIBUTING.md, PR template, CODEOWNERS
comment, README contributing section, and the [0.28.11] CHANGELOG header
with periods, colons, or hyphens depending on context. Also tightens a
couple of phrasings ("PATH gymnastics", "deeper level than this guide").
The 0.28.11 CHANGELOG header now uses Keep a Changelog's canonical
"## [X.Y.Z] - YYYY-MM-DD" hyphen form, which the CONTRIBUTING doc
prescribes for new entries going forward. Older entries stay as-is. - Add contributor docs and PR template (v0.28.11)
Documents what was previously tribal knowledge living in CLAUDE.md so
external contributors can succeed without coaching. Internal conventions
(semver, CHANGELOG format, test discipline, WoW API gotchas) now have a
contributor-facing home.- CONTRIBUTING.md: quick-start setup, development workflow, commit /
versioning / changelog conventions, test expectations, code style,
condensed WoW gotchas, PR review process. MIT-license note with
no CLA. - .github/PULL_REQUEST_TEMPLATE.md: Summary / Testing / Screenshots
/ Checklist. Checklist flags the "external contributors leave
version alone" rule so drive-by PRs don't have to guess. - .github/CODEOWNERS: auto-request maintainer review on every PR.
- README: short "Contributing" section linking to CONTRIBUTING.md.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- CONTRIBUTING.md: quick-start setup, development workflow, commit /
- Credit @katogaming88 for bottom-frame spacing fix (v0.28.10)
Bookkeeping commit for PR #1 and the Sync/Changelog/About follow-up
extension. Bumps VERSION + toc + in-addon changelog data. Also adds a
no-op SetPoint to spec/mock_ace.lua's widget.frame so the tab-builder
specs cover the new anchor call pattern without crashing.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Merge pull request #1 from katogaming88/main
Remove extra space on bottom of frame - Extend bottom-frame anchor to Sync, Changelog, About tabs
Applies the same SetPoint("BOTTOMRIGHT", container.content, ...) pattern
from @katogaming88's original three tabs to the three tab builders that
live in separate UI/ modules. Same root cause (ScrollFrame / SimpleGroup
with SetFullHeight(true) rendering slightly short of the container's
content area), same one-line fix.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Repo housekeeping: LuaLS config, design doc, CurseForge copy, gitignore (v0.28.9)
Bundles four small admin items that had been sitting in the working
tree, none of them coupled to in-flight feature work:- .luarc.json — LuaLS workspace config (Lua 5.1 + WoW API globals
list). Now anyone cloning the repo gets consistent IDE diagnostics
instead of having to figure out the globals list themselves. - docs/sync-bucket-analysis.md — the v0.26.0 throughput audit that
concluded 6h fingerprint buckets are near-optimal. The conclusion
has been living in the memory index as project_bucket_analysis.md;
committing the source doc makes the reasoning reproducible by
anyone reading the repo. - .claude/curseforge-description.md — CurseForge listing copy
refresh (Beta tag, reorganized sections, updated category counts).
Marketing surface only; no code change. - .gitignore — now excludes .claude/walkthrough/ (personal Claude
walkthroughs) and .claude/settings.local.json (per-machine
Claude Code permission state). Stops these from showing up in
git status across machines.
Patch bump 0.28.8 -> 0.28.9 per the project's commit-versioning rule.
No code path changes, no test changes besides the version-string
assertion in spec/core_spec.lua. All tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- .luarc.json — LuaLS workspace config (Lua 5.1 + WoW API globals
- Split sort access into Layout Write and Sort-only tiers (v0.30.0)
The single-tier sortAccess granted both layout edits AND Execute to
anyone who passed the rank/delegate check. There was no way to grant
"just sort" without also granting layout writes, which made it hard
to delegate sort execution widely while keeping layout edits locked
down. Now sortAccess has two independent tiers, each with their own
rank threshold and delegate list:
* write — edit templates / capture / pin slots / change reserves;
inherently includes sort access.
* sort — press Execute on the Sort tab; cannot edit the layout.
Migration: existing single-tier configurations move into the write
tier on upgrade so no one silently loses a permission. The sort tier
starts empty; populate it in the Layout tab to grant sort-only access.
MigrateSortAccessShape is idempotent and runs from MigrateAllGuilds.
Defense-in-depth: SaveBankLayout and SetStockReserve now check
HasLayoutWrite() at the storage API, in addition to the existing UI
callback gate. A future UI bug that forgets the check can no longer
silently mutate the layout.
UI: the Layout tab's Sort Access section is split into two parallel
tier renderers via a shared renderAccessTier helper. Each tier has
its own rank dropdown, delegate list, and add/remove controls. Both
sections remain GM-only to edit (preventing self-escalation).
Tests: spec/sortaccess_spec.lua fixtures updated to the new shape and
extended with HasLayoutWrite, tier-split HasSortAccess, and migration
groups. New spec/layout_write_access_spec.lua pins the storage-API
gate. spec/banklayout_spec.lua before_each now sets rank=0 so the new
HasLayoutWrite gate doesn't reject existing tests. All 984 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Add Phase 4 overflow compaction (v0.29.24 source catch-up)
The v0.29.24 changelog entry was written and shipped in CHANGELOG.md
and the in-game ChangelogView, but the corresponding source change was
never committed — git log skips v0.29.23 → v0.29.25 and the working
tree had been carrying the SortPlanner.lua + spec/sortplanner_spec.lua
diffs ever since. This commit lands the missing source so the code
matches its already-published version history.
The change itself is what v0.29.24 promised: a Phase 4 in PlanSort that
reshapes the overflow tab into a deterministic contiguous run starting
at slot 1, sorted by (itemID ASC, count DESC, origSlot ASC). Phase 2's
pivot-break loop is extracted into a reusable pivotBreakLoop local so
Phase 4 can use the same cycle-breaking machinery. Repeat sorts are
now idempotent (already-compact overflow → zero Phase 4 ops).
No version bump — the version this code was authored against (v0.29.24)
is already documented and superseded by v0.29.25/v0.29.26.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Remove extra space on bottom of frame
- Fix sort progress counter + row realignment on replan (v0.29.26)
Two related bugs visible in v0.29.25 when a replan fires mid-sort:- Counter showed "34/33" and "35/33" after a replan. Root cause: the
display formula was (done+failed)/total. done and failed accumulate
across replans, while total is the current plan's size, so when a
replan reissued work the numerator could exceed the denominator.
Switched to "op N / T" using the executor's live opIndex against the
current plan's total. opIndex is always in [1, T] for the active
plan, so the numerator can never overshoot. - After a replan, the move list kept showing the original plan's rows
while the executor was actually running a different post-replan
plan. Row markers drifted onto the wrong moves. SortExecutor now
emits phase="planupdated" with the new plan after doReplan rebuilds
state.plan. SortView swaps the cached _sortLastPlan to match, clears
stale _sortOpStatus (old indices referred to different moves), and
rebuilds the move list.
Also fixed a latent issue: _SortView_Preview now uses the cached
plan during sort execution instead of re-running PlanSort. Without
this, a rescan-triggered rebuild could display a plan different
from the one actually executing.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- Counter showed "34/33" and "35/33" after a replan. Root cause: the
- Fix sort progress markers: ASCII glyphs + rebuild persistence (v0.29.25)
Two bugs in v0.29.23's live sort progress display:- The Unicode markers (>|ASCII|checkmark|cross) used for per-op status
don't render in WoW's default FRIZQT__ font. Users see colored
missing-glyph boxes. Swapped all three for colored ASCII chars:
'>' yellow (current), '+' green (done), 'x' red (failed). - Ledger.lua fires RefreshUI every time a rescan sees new transactions.
During a sort, every move creates a transaction log entry, so
RefreshUI fires frequently mid-sort. UI.lua's generic fallback calls
SelectTab('sort') for the sort tab, which re-enters BuildSortTab and
wipes self._sortOpRows. Per-op markers disappeared forever because
they were transition-triggered and the events had already fired; the
top progress label recovered on the next event because it rebinds.
Fix: the progress handler now writes every transition into a
persistent self._sortOpStatus (opIndex -> status) AND caches the
progress label text in self._sortProgressText, in addition to the
live SetText calls. On rebuild, the Preview ops-list loop and the
new progress-label init repaint from those tables. Tab-switching
mid-sort now also preserves state.
No test changes needed — the failure was a font glyph + UI rebuild
interaction invisible to the current AceGUI mock. Verified: 962 tests
pass, lint clean on UI/SortView.lua.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- The Unicode markers (>|ASCII|checkmark|cross) used for per-op status
- Live sort progress in the Sort tab (v0.29.23)
SortExecutor now emits GBL_SORT_PROGRESS on every state transition
(start / step / complete / failed / replan / reclassify / finish) with
a flat payload carrying opIndex, done, failed, replans, total,
currentOp, and per-phase extras.
SortView subscribes and updates:
* A running progress line at the top of the move list:
"Executing — 12 / 27 (10 done, 2 failed, 0 replans)".
* Per-op status markers prepended to each row as it advances:
- ▶ for the op currently in flight
- ✓ for completed (or late-ACK reclassified) ops
- ✗ for failed ops
Updates are direct SetText on persistent widget refs rather than a full
tab rebuild, so per-op cost is microseconds. Sort is 100% local so this
has zero bandwidth impact.
On completion, the progress line immediately shows
"Sort complete — N done, M failed, K replans. Rescanning..." while the
post-sort rescan runs; the tab then refreshes to the post-sort plan
when the scan lands. Previously the Sort tab appeared frozen for the
full 5s scan window after execution.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Fix late-ACK reclassification during in-flight ops (v0.29.22)
v0.29.19 gated the late-ACK grace window on state.waiting == nil. In a
live sort the inter-move gap is 0.3s, so state.waiting is almost always
populated for the NEXT op by the time a delayed ACK arrives. Result: the
grace window essentially never fired in the real failure mode the user
saw (v0.29.19 logs: every 'op N timed out' was immediately followed by
'op N+1 pre-check fail' cascading to replan).
The handler now runs two independent checks on each event:
1. Is this a late ACK for a recently-timed-out op? (reclassify done++,
failed--, don't trigger replan)
2. Does this advance the currently in-flight op? (normal happy path)
Both can fire from the same event — the same server broadcast can be
the resolution of a delayed prior op and confirmation of the current
one, if the mock happens to batch them.
Also dialed back the loud Capture-button diagnostics from v0.29.21. The
regression cleared on /reload and isn't reproducible; kept the
pcall-wrapped error handler and pinned-slot count (both are cheap
happy-path-silent wins).
Tests: new spec for the two-op case with a phantom prior timeout.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Add Capture button diagnostics to chase silent-failure regression (v0.29.21)
User reported: on tab 5 (which has no saved layout), changing mode to
Display and clicking Capture appears to do nothing — no chat messages,
no item rows. I can't reproduce by reading the code, so instrument the
click handler to print at every branch:
* "Capture: click on tab N..." — first line on every click, proves
the callback is firing.
* "Capture: scan=... slots=... dirty=... writable=..." — reveals
whether lastScanResults[tabIndex] exists and has .slots populated.
* pcall-wrapped CaptureTabLayout — a Lua error in capture now
surfaces as "Capture tab N crashed: <err>" instead of a silent
failure.
* Success message now also counts pinned slots.
No behavior change for the happy path — just visibility.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Add timeout-time state diagnostics to SortExecutor (v0.29.20)
When a move op times out, dump a classification ([none] / [partial] /
[complete] / [other]), the op's full details (src/dst + item + count),
and the observed live state of src, dst, and cursor. This distinguishes:
* Move never executed (src unchanged, dst empty) — server dropped
* Move partially executed (src emptied, cursor held) — drop failed
* Move completed silently (src drained, dst populated) — late ACK
* Anomalous state — something else
Also add the op-context line to dst-mismatch pre-check failures so the
cascading-replan pattern is legible in the audit trail. No behavior
change — purely diagnostic.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Fix sort abort from late-ACK misclassification (v0.29.19)
When GUILDBANKBAGSLOTS_CHANGED arrived slightly after MOVE_CONFIRM_TIMEOUT
fired, the executor classified the legitimate late ACK as foreign activity
and triggered a replan. Replan's fresh scan saw the move already settled,
but the new plan sometimes still listed the just-completed move as op 1,
producing a pre-check failure loop that exhausted all 5 replans before
aborting (observed in-game with 2/222 ops completed).
Fix: track the most recent timed-out op for a 5s grace window. When a
stray GUILDBANKBAGSLOTS_CHANGED arrives while idle and the timed-out op's
dst slot is now populated as expected, retroactively reclassify the op
as success rather than replanning.
Also widen MOVE_CONFIRM_TIMEOUT 2s -> 4s and SCAN_WAIT_TIMEOUT 5s -> 10s
to reduce the frequency of the race and to give full-bank scans enough
headroom (observed ~4s scans in-game).
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Unpin controls: full tab + per-item (v0.29.18)
User confirmed via v0.29.17 diagnostics that tab 4 (gems) had 66
pinned + 16 auto-placed, with 5 of the 16 falling through to
first-empty and scattering to the end of the tab during restock.
This is exactly the "captured layout conflicting with restock"
pattern — the fix is to let the user release pins when they decide
the layout should be declarative instead of fixed.
Three new UI elements in the Layout editor:- "Unpin all slots" button per display tab (next to Capture).
Wipes tab.slotOrder, retains tab.items. After save, the planner's
Pass 2 takes over placement via the same adjacency heuristic
that Capture's pinning would have fought against. One click
converts a rigid captured tab into a declarative one. - Per-item "Unpin" button on each item row. Clears slotOrder
entries for just that item; the rest of the tab stays pinned.
Use for "mostly frozen, except this one thing" setups. Disabled
when the item has zero pinned slots (nothing to unpin). - Pin-count label on every item row: "N pinned" in yellow if
pinned > 0, "not pinned" in gray otherwise. Visible at a glance
without scrolling down to the slot map.
Mode taxonomy now documented in CHANGELOG:
- Fully pinned: Capture then leave alone. Exact positions enforced.
- Fully declarative: Add Item only, or Capture + Unpin all. No
pins; planner places by adjacency at sort time. - Mixed: Capture, then Unpin specific items you want to flow
freely. Rest stay fixed.
Tests: 953 pass. LayoutEditor lints clean. No test added for the
UI buttons themselves (AceGUI mock coverage is thin); the underlying
slotOrder mutation is trivial.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- "Unpin all slots" button per display tab (next to Capture).
- Instrumentation: demand origin tracking (v0.29.17)
User reported the gem tab "having problems" — captured 47 gems,
bumped Slots up on some for restock, saw new stacks scatter to the
end of the tab instead of landing adjacent to their same-item
captured stack. To answer this kind of question definitively we
need to see not just WHAT the planner did, but WHY each demand sits
where it sits. Pinned by Capture? Extended from an adjacent pin?
Fell through to first-empty because no adjacency was available?
SortPlanner: each demand now carries one of four origin tags —
"pinned" (Pass 1 from slotOrder), "extend-right" / "extend-left"
(Pass 2a adjacency extension from an existing claim), "first-empty"
(Pass 2b scatter fallback). Origin is exposed on plan.demandMap[t]
[s] for UI/diagnostic consumers. Zero change to planner behavior —
purely additive metadata.
SummarizeSortPlan: each op line now appends "(dst pinned)" /
"(dst extend-right)" / etc. based on the destination demand's
origin. Ops targeting overflow or a pivot slot have no dst demand
and render without a suffix.
PrintSortPreview: after the "Plan: N moves, …" line, each display
tab prints a breakdown "T3 origins: 47 pinned + 3 auto-placed
(0 extend-right, 0 extend-left, 3 first-empty)". The gem-tab
pattern shows as many pinned + low extends + high first-empty.
Layout editor slot map header changes "N/98 pinned" into
"N pinned + M auto-placed; K empty" — three explicit counts
instead of one composite. Per-item "auto-placed" lines distinguish
items with no captured stacks ("N auto-placed") from items that
captured some and were restocked later ("X pinned + Y auto-
placed") — the latter is the gem-tab pattern.
Tests: 953 pass. New spec asserts all four origins are recorded
correctly against a fixture that exercises each path (pinned at
slots 1/3/4/6, extend-left fills slot 2 because right is blocked,
first-empty fills slot 5 for an item with no pins, extend-right
fills slot 7 from an existing pin at 6).
v0.29.18 will add the "Unpin slots" action and document when to
use each mode.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Hide content during Layout rebuild to mask scroll flicker (v0.29.16)
v0.29.15 preserved scroll position across RefreshLayoutTab rebuilds
but the mechanism (Release → Build at scroll=0 → C_Timer → SetScroll
back to saved value) was still visually perceivable as a blank-
then-snap. User reported it as distracting.
Fix: the TabGroup's .content frame is a stable WoW frame owned by
the TabGroup itself (it persists across child Release/Build), so
setting its alpha to 0 before the rebuild hides everything that
happens inside while layout runs. The scheduled C_Timer.After(0)
callback now both applies SetScroll and restores alpha to 1, so
the user sees the final state directly — no flicker, no scroll
jump, no visible rebuild.
Alpha 0 (not Hide) because Hide() can affect layout calculations
in AceGUI's Flow layout; alpha just makes the frames invisible
while leaving geometry intact.
Tests: 952 pass. Verification is in-game — press Enter on any
Layout field and the tab should appear static.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Preserve Layout tab scroll position across refresh (v0.29.15)
Layout editor's every-edit rebuild was destroying the ScrollFrame
and resetting scroll to 0. Press Enter on a Slots or Per slot field
halfway down the page → whole page rebuilds → scroll jumps to top →
you can't find where you were, much less rapidly edit multiple
items. Untenable.
Fix: AceGUI's ScrollFrame widget supports SetStatusTable(t) to let
callers own the scrollvalue/offset table. We pass a table persisted
on self (self._layoutScrollStatus), so the rebuild reads and writes
to the same dict across Release/Build. After BuildLayoutTab returns
we schedule a one-frame-deferred SetScroll(savedValue) via C_Timer
because AceGUI applies the value in LayoutFinished — trying to set
it before content has laid out is a no-op.
Why the tab rebuilds at all: it has to. The slot budget label, the
save/discard buttons' enabled states, and the slot map panel all
depend on draft state that changed. Refactoring toward in-place
updates would require holding references to every one of those
widgets and patching each on every field edit — a much larger
surgery. Preserving scroll position is the minimal fix that makes
the existing architecture usable.
Tests: 952 pass unchanged. The behavior (scroll stays put after
refresh) is verifiable in-game; no spec covers AceGUI scroll state.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Layout editor slot-map visualizer (v0.29.14)
The v0.29.12 "hidden swap" incident exposed a UX gap in the Layout
editor: per-item rows show aggregate counts (49 slots × 20 per slot)
but never the slot-level positions in slotOrder. Two layouts with
identical per-item counts but a pair of swapped slotOrder entries
looked identical in the editor — the bug only surfaced when sort
ran and /gbl deviations was invoked.
This commit adds a Slot Map panel under each display tab's item list.- computeSlotRuns(slotOrder) groups slotOrder into contiguous same-
item runs {startSlot, endSlot, itemID, length}. A nil entry breaks
the run — a single-slot anomaly between two long runs of another
item produces its own 1-wide run, which is visually obvious in
the rendered list. - _LayoutEditor_RenderSlotMap renders one Label per run formatted
like "S1-S23 (23): Silvermoon Health Potion × 20". When
self.lastScanResults[tabIndex] is populated, each run is compared
against the live bank and annotated with a green check (all match)
or red cross + per-slot detail lines naming the actual contents
(capped at 5 details per run to avoid flooding). Items whose
items[id].slots exceeds their pinned slotOrder count list below
as "auto-placed at sort time" — reflects the v0.29.13 ownership
split (Capture pins, planner places at sort time). - No dependency on SortPlanner — the editor computes its own runs
from tab.slotOrder / tab.items rather than re-running PlanSort on
every render. Deviations slash command continues to use the
planner's demandMap path; different surface, different consumer. - All scan reads are nil-safe; panel renders cleanly before a scan
has been taken with "(no scan loaded — comparison unavailable)".
Tests: - spec/ui/layouteditor_spec.lua (new) covers computeSlotRuns with
seven cases: empty input, single-item full fill, two contiguous
blocks, gap-breaks-run, the v0.29.12 four-run anomaly shape (two
1-wide outliers between big same-item runs), sparse non-adjacent
keys, and nil input. - 952 total tests pass. luacheck clean on LayoutEditor; new
ChangelogView line-length warnings match existing entries' style.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- computeSlotRuns(slotOrder) groups slotOrder into contiguous same-
- Remove duplicated slotOrder prefill from Layout editor (v0.29.13)
The Layout editor was calling a local pickSlotForItem helper
(right-extend → left-extend → first-empty) from the Add Item and
slots-up callbacks to pre-populate slotOrder with the item's new
positions. SortPlanner Pass 2 already runs the same algorithm
against the same inputs when slotOrder is empty, so the UI prefill
was pure duplication — worse, it blurred the semantic meaning of a
populated slotOrder entry:
* Capture: "pin this slot because I observed the bank in this state"
* Add Item / slots-up: "pin this slot because the heuristic happened
to pick it — the user never chose or saw this position"
Since the planner treats any populated slotOrder entry as an
authoritative pin, heuristic pins were indistinguishable from
captured ones and were rigidly enforced by sort. After this commit
slotOrder is written exclusively by CaptureTabLayout and represents
only observed positions; placement of un-captured items is deferred
to plan time.
Behavior preserved:- Slots-down still trims stale slotOrder pins above the new count,
so a captured-then-shrunk layout doesn't carry dangling pins. - Remove still wipes slotOrder entries for the removed item.
- Capture is entirely unchanged.
- Post-sort bank state is byte-identical to the pre-v0.29.13 flow
on an items-only tab (verified by new regression test).
Also fixes a latent partial-state bug: on a nearly-full captured
tab, Add Item used to set items[id].slots=N but silently fail to
populate slotOrder when pickSlotForItem ran out of empty slots.
Validation caught the aggregate over-budget at save time, but not
the specific "this item couldn't be placed" failure. With the
prefill gone, items[].slots is the single source of truth and the
over-budget case surfaces once cleanly.
Tests: - New spec/sortplanner_spec.lua test "plans items-only layouts
identically to heuristically pre-pinned slotOrder" covers the
byte-equivalence invariant the commit depends on. Three items
(X/Y/Z at 5/5/2 slots, empty slotOrder) land contiguous at
S1-5, S6-10, S11-12 in sortedID order — same as the old pre-pin. - 945 tests pass. luacheck clean on touched files.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- Slots-down still trims stale slotOrder pins above the new count,
- Instrumentation: /gbl deviations + verbose pre-check audit (v0.29.12)
The v0.29.11 sort changes still leave cases where the bank doesn't
match what the user expects, and the existing "N moves done, 0 failed"
chat print doesn't tell you WHICH slot came out wrong. This commit
adds the plumbing to see exactly what deviated.- plan.demandMap exposed on the PlanSort return: { [t][s] =
{itemID, perSlot} } covering both slotOrder-pinned and items.slots
extensions. This is what the planner actually expects the bank to
look like; using the same source as the executor guarantees the
check matches the planner's intent, not a second interpretation. - /gbl deviations (alias /gbl devs): scans current lastScanResults
and diffs against plan.demandMap. Reports three categories of
mismatch (wrong item / wrong count / empty where expected) plus
"extras" (items sitting in slots the layout doesn't claim). Output
capped at 40 lines; full detail stays in the audit trail. - Sort tab auto-runs PrintDeviations when the post-Execute rescan
completes (hooked into the existing RescanAndRefresh flow from
v0.29.9), so you see deviations immediately without typing a
command. - SortExecutor pre-check failures now audit the observed state, not
just a bare "src mismatch" message. "expected it:12345 x>=20, got
it:99999 x10" makes foreign activity, stack-size drift, and planner
bugs all distinguishable from each other.
Tests: - New regression in spec/sortplanner_spec.lua asserts plan.demandMap
covers slotOrder-pinned + items.slots-extended slots with the
correct itemID and perSlot; asserts it's nil for overflow tabs. - 944 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- plan.demandMap exposed on the PlanSort return: { [t][s] =
- Keep display tab and stock groups contiguous (v0.29.11)
Reported: sort was "filling empty slots anywhere most convenient" —
items ended up scattered instead of building a neat, organized display
and stock. Two specific failure modes:- Pass 2 demand extension (items[id].slots > slotOrder count) picked
"first unclaimed" slot. If Health (lower itemID) had its captured
range at 50-74 and Power had 1-25, Pass 2 ran Health first, grabbed
slots 26-49 as "first unclaimed," and fragmented both sections. - Phase 1B overflow routing popped from a FIFO of empty slots, so
spills landed in the first gap regardless of whether the same item
already had neighbors there — stock ended up interleaved.
Fix in both paths: right-extend existing same-item claims first, then
left-extend, only falling back to arbitrary unclaimed when both ends
are blocked. Result:
- Display: a group at 50-74 with items.slots=49 grows RIGHT to 50-98
before ever touching slot 49. The item whose group starts at 1 then
has slots 26-49 free to extend into. - Overflow: a Power spill lands adjacent to the existing Power bulk;
a Health spill lands adjacent to the Health bulk. - Layout editor: Add Item and Slots-field increase both use the same
adjacency rule so future saves stay neat without a recapture.
Tests: - spec/sortplanner_spec.lua:
extends Pass 2 demands contiguously adjacent to same-item claims(regression for the user's scenario —
Power 1-49 / Health 50-98 layout should be 0 ops against a matching
bank). Before the fix the planner produced a 2-cycle (Health stole
slot 49, Power got 98 via fallback). - spec/sortplanner_spec.lua:
groups overflow spills next to existing same-item stacks(regression for the stock organization gap).
943 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- Pass 2 demand extension (items[id].slots > slotOrder count) picked
- Wait for GUILDBANKBAGSLOTS_CHANGED before scanning a tab (v0.29.10)
Reported: first bank open after login, Sort tab shows everything as
deficits ("missing"). Subsequent opens are fine.
Root cause: Scanner.QueryAndScanTab registered the bank-slot event,
called QueryGuildBankTab, then immediately called TryScanCurrentTab.
On first open the client has no cached slot data — GetGuildBankItemLink
returned nil for all 98 slots, TryScanCurrentTab unregistered the event
(scanState.waitingForData = false), and moved on. When the server's
response actually arrived via GUILDBANKBAGSLOTS_CHANGED, the handler's
guard rejected it and the data was dropped.
Fix:- Scanner: remove the immediate-scan fast-path. Wait for the event (or a
3s timeout fallback for empty tabs that may not fire). The fallback
also covers network stalls so scans don't hang indefinitely. - mock_wow: QueryGuildBankTab now fires GUILDBANKBAGSLOTS_CHANGED
synchronously, simulating the server's response. Existing scanner
tests (which populate tabs before scanning) continue to work without
explicit event firing. - New regression test in scanner_spec.lua: overrides the mock to
suppress the auto-fire, verifies the scanner waits for the event,
then populates data + fires the event and confirms the scan picks it
up. Without the fix, this test captures 0 items instead of 2.
941 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- Scanner: remove the immediate-scan fast-path. Wait for the event (or a
- Auto-rescan Sort tab after Execute (v0.29.9)
Reported: after pressing Execute and sort finishing, the Sort tab still
showed the old plan until the user manually pressed "Scan bank". Root
cause: _SortView_Execute's onComplete callback called RefreshSortTab,
which re-runs Preview againstself.lastScanResults— but that snapshot
is the pre-sort scan. Same snapshot in, same plan out.
Fix: new_SortView_RescanAndRefreshhelper kicks off a StartFullScan
on sort completion, shows a "Rescanning bank after sort..." placeholder
while it polls, then RefreshSortTab once the fresh scan lands. Preview
now reflects the post-sort state automatically.- Status banner gets a new variant for the rescanning state.
- _SortView_Preview bails early with a wait-placeholder if the flag is
set (prevents flashing the stale plan while the rescan is in flight). _sortLastPlanis cleared on execute so the user can't accidentally
re-execute a stale plan.- 5-second poll deadline falls back to a plain refresh if the scan
doesn't complete in time.
940 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- Fix silent demand drop when Slots edited above captured count (v0.29.8)
Reported by in-game testing: user captured a tab (42 slotOrder entries),
edited Slots on several items upward to total 49, saved, messed up the
bank, pressed Preview — got "bank already matches layout" even though
the bank was obviously different.
Root cause: the planner counted demands from slotOrder entries only.
items[id].slots was used only by BankLayout.Validate's ≤98-slot budget
check, never by the planner. Editing Slots up in the UI updated
items[id].slots but left slotOrder unchanged, so the extra demands
silently fell off.
Fix:- SortPlanner: demand-building now has two passes. Pass 1 emits demands
from slotOrder positions capped at items[id].slots (handles the
slotOrder-too-many direction). Pass 2 adds demands at the first
unclaimed slot indices when items[id].slots exceeds emitted count
(handles the slotOrder-too-few direction). - SortPlanner: Phase 3 sweep now checks the effective demand map
(demandOfSlot) rather than raw slotOrder, so dynamically-added
demands at non-slotOrder positions don't get mis-evicted. - UI/LayoutEditor: Slots input now keeps slotOrder in sync on edit —
increases pin new positions at the first unclaimed indices, decreases
trim from the highest slot index down. Prevents the mismatch at the
source for future saves. - Core/PrintSortPreview: /gbl sortpreview now prints a diagnostic
breakdown (per-tab demand counts, overflow/ignore indices, scan
contents) and an explicit reason when the plan is empty ("layout has
no demands" vs. "every demand already satisfied"). Makes 0-op
outcomes diagnosable.
Tests: - Two new regression tests in spec/sortplanner_spec.lua pinning both
directions of the items[id].slots vs. slotOrder-count mismatch. - 940 tests pass. No changes to executor, sync, or other specs.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- SortPlanner: demand-building now has two passes. Pass 1 emits demands
- Rewrite sort planner as assign-then-schedule (v0.29.7)
Milestone M-sort-2.5 — drop-in replacement for the three-pass greedy.
Same public contract (PlanSort returns the same shape); SortExecutor
and UI/SortView are unchanged.
Algorithm:- Phase 1 assigns every demand to the best available source. Priority:
same-tab direct > overflow > cross-tab, largest-count first within
each tier, deterministic lex tiebreak. Keep-slots (slot matching its
own demand exactly) are never harvested; oversize keep-slots expose
their excess as free supply. - Phase 2 schedules ops against a mutating state model via a greedy
feasibility loop. Remaining blocked assignments form swap cycles;
each cycle is broken with a pivot slot (same-tab empty preferred,
overflow fallback). Unreachable cycles are reported as unplaced with
reason="cycle-no-pivot" rather than emitting half-broken ops. - Phase 3 sweeps any stragglers to overflow (defensive).
Wins over the shipped three-pass: - Direct intra-tab moves skip the overflow round-trip (1 op, not 2).
- Oversize stacks feed multiple demands from a single source.
- 2-cycles cost 3 ops (was 4), 3-cycles 4 ops (was 6).
- Largest-first source selection avoids multi-split fills.
Coverage: - 9 new tests in spec/sortplanner_spec.lua pin the algorithmic wins.
- spec/sortplanner_perf_spec.lua benchmarks the worst-case (8 tabs
x 98 slots, 90 demands) under a 250 ms budget. - All 938 tests pass; no regressions in SortExecutor, BankLayout,
sync, or any other spec. - plan.unplaced[].reason is a new backward-compatible field.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- Phase 1 assigns every demand to the best available source. Priority:
- Clarify Layout save-bar UX (v0.29.6)
The two-phase edit/save model on the Layout tab was unclear — users
saw a Save button and a Revert button with no indication of what
state the layout was in or whether changes had been committed.
Kept the model (edits buffer until Save, validation + sync broadcast
fire once per logical change) but made the state visible:
* Status banner above the save row: "You have unsaved changes" when
the draft differs from storage, "Layout is up to date" when clean,
or "Edits require sort access" for read-only viewers.
* Save button disables itself when there's nothing to commit and
re-labels to "Saved ✓" so the clean state is obvious.
* Revert renamed to "Discard changes" and disables when clean.
* Capture success message now explicitly says "Click Save Layout
to commit" so capture's two-phase behavior matches manual edits.
Tracked via self._layoutDirty which flips true in every mutation
callback (mode change, capture, add item, edit slots/perSlot,
remove item) and back to false after Save or Discard.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Make Capture current layout robust (v0.29.5)
The Capture button was silently no-oping when there was no stored scan
for the target tab: it called CaptureTabLayout, got an error back, and
printed a failure message that was easy to miss in chat. Symptom from
the user's perspective: "clicked Capture, nothing happened."
Now the button:
* warns if the bank is closed before doing anything,
* applies immediately if the latest scan already covers the tab,
* otherwise kicks off a scan and polls up to 5s for completion,
* applies the capture when data arrives,
* surfaces a specific failure if the scan completes but the tab
still isn't covered (e.g. the character can't view it).
Success prints a green "Captured tab N: M items." so it's visible.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Fix Layout tab interactivity (v0.29.4)
Two bugs in the v0.29.3 Layout tab:- Mode dropdowns and per-item Slots/Per-slot edits appeared to do
nothing — the draft state was being re-initialized from saved
storage on every RefreshLayoutTab call, so a change applied and
then immediately reverted on the rebuild. The draft now persists
across rebuilds; Save and Revert explicitly clear it so the next
render re-reads from storage. - Sort Access rank dropdown showed two blank entries at the top and
defaulted to -1 because the items table was built as an array,
but AceGUI Dropdown:SetList expects a hash keyed by the option
value. Rebuilt as a proper hash with "None (GM only)" at -1 and
"Rank N and above (rankname)" for each guild rank.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
- Mode dropdowns and per-item Slots/Per-slot edits appeared to do
- Add Layout editor and Sort tab UI (v0.29.3)
M-sort-2 (UI half): GM-usable layout editor + preview/execute UI.
Layout tab:
* Per-tab Mode dropdown (display / overflow / ignore).
* Display tabs show an item-template table with Slots and Per-slot
inputs, a live slot-budget readout, a Capture-current-layout
button that snapshots a hand-arranged tab, and an Add-item
input that takes an itemID or a pasted item link.
* Save / Revert buttons commit the working draft via SaveBankLayout.
* Sort Access sub-section (GM-only to edit, read-only to delegates):
rank-threshold dropdown populated from guild ranks + delegate
add/remove. Non-GMs see the current policy as a summary.
* Tab visibility gated by HasSortAccess; control disabled states
mirror the access check so viewers see a consistent read-only UI.
Sort tab:
* Preview button builds a plan from the latest scan and renders
moves, deficits, and unplaced items with human-readable names
via ItemCache.
* Execute runs the plan through SortExecutor (HasSortAccess-gated)
with result prints; Cancel aborts a running run; Scan-bank
shortcut refreshes state without leaving the tab.
The Layout tab slots in between Consumption and Sync in the tab row
for characters with sort access; everyone else still sees Sort.
M-sort-2 is now complete. M-sort-3 (Stock tab + bag restocker) is
next.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Add sort executor and sort-access policy (v0.29.2)
M-sort-2 (backbone): the non-UI half of milestone 2.
SortAccess policy: new AceDB field + HasSortAccess() helper. GM sets a
rank threshold and optional delegate list; those plus the GM himself
can edit layouts and execute sort. Policy writes are GM-only so
delegates can't self-escalate. Default on fresh install is GM-only.
SortExecutor: runs a plan one op at a time, 0.3s inter-move throttle,
pre-step verification against live bank state, event-driven confirmation
with 2s polling fallback, replan-on-foreign-activity (cap 5 replans),
bank-close abort, cursor-leak safety on every exit path. Audit entries
trace every step and every failure mode.
Two debug slash commands for in-game end-to-end testing without UI:
/gbl sortexec — execute the saved layout's plan (HasSortAccess-gated)
/gbl sortcancel — cancel a running sort
10 new executor tests, 9 new access-policy tests, mock additions for
PickupGuildBankItem/SplitGuildBankItem/ClearCursor/CursorHasItem with
cursor state and event firing, plus foreign-activity test helpers.
UI (LayoutEditor + SortView) ships next in v0.29.3.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Audit cleanup: doc sync + planner regression tests (v0.29.1)
M-sort-1.1: follow-up to the v0.29.0 audit. CLAUDE.md architecture
section now lists BankLayout and SortPlanner. Added four regression
tests for scenarios the v0.29.0 tests didn't pin down: ignore-tab
invisibility, keep-slot harvest protection, multi-tab orphan routing,
and no-duplicate-unplaced under overflow saturation. The last test
caught a latent bug — Pass 1 now drops its working-bank copy when
recording an unplaced entry so later passes don't re-process the
same slot. stockAlerts AceDB field now has an intent comment noting
it's reserved for the v1.3.0 alerts feature, not dead code.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com - Add bank layout model and sort planner (v0.29.0)
M-sort-1: data + planner foundation for the cross-tab sort + stocking
feature. BankLayout stores per-guild tab templates (display / overflow /
ignore), with capture-from-snapshot and full validation. SortPlanner
turns a bank scan + layout into a deterministic move plan that splits
oversize stacks, fills deficits from overflow or other display tabs,
evicts foreign items to overflow, and reports items it couldn't place.
Pure data + pure planner in this milestone — no movement execution,
no UI wiring, no sync transport yet. Those arrive in M-sort-2/3/4 on
the feature/sort-stock branch.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com