promotional bannermobile promotional banner

YippYapp Helper

All-in-one Midnight companion
Back to Files

v3.2.0

File nameYippYappHelper-v3.2.0.zip
Uploader
RyggsekkenRyggsekken
Uploaded
Aug 24, 2026
Downloads
69
Size
899.1 KB
Flavors
Retail
File ID
8722905
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7

What's new

tag ab78e564218cca21698bdeec830077cc54d187b1 v3.2.0
Author:    Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:    Mon Aug 24 11:32:12 2026 +0200

v3.2.0 - Crest advice, shared keystones, and a lighter addon

The advisor reads a whole wallet rather than one piece at a time: a crest
tier is counted over slots instead of over the pieces already worn, a
slot's bill is priced off the mark it will have rather than the one it
has, and a track you own nothing on can be advised at all. Crafting is
in.

Keystones moved onto LibKeystone, the library BigWigs, DBM and
EllesmereUI already embed, so the Guild tab lists people who do not run
this addon -- which, before, was everybody. This is a .toc change, so it
needs a full client restart rather than a /reload.

The loot browser writes down its instance list, its loot and its journal
link index, so a reload no longer refetches them, and a pass that
disagrees with itself is refused instead of saved.

Two features came out. The interrupt tracker had not worked for the whole
of 12.1 and could not be repaired in place -- the client stopped handing
out which spell a party member cast, so the party half was a guess keyed
on a 150ms timing coincidence. The Delves tab was a third place to keep
facts the game does not hide and this addon already said elsewhere;
delve chores, delve advice and the Progression page's delve tiers all
stay.

And a sweep for work done during ordinary play, none of it feature work:
the login prewarm no longer takes a whole frame's budget or runs inside a
dungeon, the battle-res timer no longer wakes on every charge-based spell
the character owns, the home page no longer rebuilds for other addons'
quests, and UNIT_INVENTORY_CHANGED is filtered to the player.

The watermark sweep is the one worth naming. It was fixed once and went
on freezing the client anyway, because only half of it had been spread
across frames: to decide what to queue it read all sixteen slots up
front, and that is the expensive half -- each read ends in a real
SetInventoryItem tooltip build, on a cache the calling handler wipes
immediately beforehand. The harness had missed it by stubbing exactly
that function. It counts slot reads now, controlled against the old
queue-builder.

commit e34e04a4edd1d1d8b9e41e7b6d4ac0c6f9ef50b1
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 24 11:31:42 2026 +0200

    Date the changelog for v3.2.0

    The version in the .toc went to 3.2.0 with the work itself so the build
    under test was the build that ships. This is the other half: the
    Unreleased heading becomes a dated one, which is the last thing that has
    to be true before the tag points at anything.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit e358796fccae8352a0ce7a9a9de04d5b5dec6860
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 24 11:01:13 2026 +0200

    The Delves tab out, and the client stubs nothing else was using

    A whole page to restate a companion's friendship rank, a key count and a
    tier ladder. The game hides none of that, and this addon already says
    two of the three somewhere else -- so the page was a third place to keep
    the same facts correct.

    Out with it: Features/Delves/, the shell page it registered, its entry in
    the /yh shell <page> list, and its check in the harness. The client stubs
    went too, because the page was their only consumer -- C_DelvesUI, the
    C_GossipInfo friendship pair, C_PartyInfo.IsDelveInProgress and
    C_Reputation are now gone from loadcheck rather than sitting there
    answering nobody.

    Delves are not gone from the addon, only their tab, and the difference is
    worth stating because "delve" still appears all over the source. The
    weekly checklist keeps its delve chores, PlannerData still raises "run a
    tier N delve" on the Dashboard when that is the cheapest upgrade
    available, the Progression page keeps its delve tier section reading
    ns.PROGRESSION.DELVES, and the vault's World row is still the one delves
    fill. W:HeroCard also keeps borrowing the delve companion ring atlas for
    its portrait rim -- the shell's character column draws it, and the atlas
    name is incidental to what it is used for.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit b91f710790949cac9144827e4482dcb567327ad6
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 23 23:49:06 2026 +0200

    Crest advice, shared keystones, and the interrupt tracker out

    The accumulated crest-advice branch, committed in one go because the
    strands are interleaved inside the same files -- Core.lua and
    LootBrowserData.lua each carry several of them -- and this environment
    has no interactive staging to separate them by hunk. CHANGELOG.md has
    the entry-by-entry account; this is the shape of it.

    Gear. The advisor reads a whole wallet rather than one piece at a time:
    a tier is counted over slots instead of over the pieces already worn, a
    slot's bill is priced off the mark it will have rather than the one it
    has, and a track you own nothing on can now be advised at all. Crafting
    is in. The per-wallet sentence strip above Improvements is out.

    Mythic+. Keystones moved onto LibKeystone, the library BigWigs, DBM and
    EllesmereUI already embed, so the Guild tab lists people who do not run
    this addon -- which, before, was everybody. Vendored from BigWigs;
    LibStub hands out whichever copy loads first, so keeping ours current
    matters. Needs a full client restart, not a /reload: it is a .toc change.

    Loot browser. The instance list, the loot and the journal link index are
    all written down now, so a reload does not refetch them, and a pass that
    disagrees with itself is refused rather than saved.

    Performance. A sweep for work done during ordinary play, none of it
    feature work: the login prewarm no longer takes a whole frame's budget
    or runs inside a dungeon, the battle-res timer no longer wakes on every
    charge-based spell the character owns, the home page no longer rebuilds
    for other addons' quests, and UNIT_INVENTORY_CHANGED is filtered to the
    player. `/yh trace` now genuinely costs nothing while off -- its
    watchdog is a parentless frame, which the client shows by default, so it
    had been running an OnUpdate every frame of every session.

    And the watermark sweep, which was fixed once and went on freezing the
    client anyway. Only half of it had been spread across frames: to decide
    what to queue it read all sixteen slots up front, and that is the
    expensive half -- each read ends in a real SetInventoryItem tooltip
    build, on a cache the calling handler wipes immediately beforehand. The
    harness missed it by stubbing exactly that function, so it measured the
    half that had been fixed. It counts slot reads now, controlled against
    the old queue-builder.

    Removed: the interrupt tracker. It had not worked for the whole of 12.1
    and could not be repaired in place -- the client stopped handing out
    which spell a party member cast, so the party half was a guess keyed on
    a 150ms timing coincidence. Doing it properly means the combat log,
    which is a rewrite and the one event class this addon has stayed clear
    of. Its settings and Edit Mode position are cleared on next login.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 8abc85518f017fcae829c07ceb4b4ea437737511
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 20:53:39 2026 +0200

    The spark row counts residue instead of guessing from seven quests

    Tidal Spark Dust is one grain per Spark of Tides OBTAINED and never goes
    down, so it answers the question the sparks themselves cannot: holding
    zero is equally consistent with "not collected" and "collected and
    already crafted with", and the second is normal for anyone who crafts.

    The client was asked rather than assumed, which is the only reason this
    is wired the way it is:

      totalEarned              4      the Total the tooltip shows
      maxQuantity              4      its "Current Season Maximum"
      useTotalEarnedForMaxQty  true   so totalEarned is the number to read
      isAccountWide            false  this character, which a per-character
                                      chore list wants
      quantityEarnedThisWeek   0      on a character holding four
      maxWeeklyQuantity        0

    That last pair is the trap. There is no "did you get one THIS week"
    field -- the currency does not meter itself weekly -- so the row reads a
    season total against a season ceiling instead. Which is better than what
    it asked before: behind is behind whether it happened this week or three
    weeks ago, and catching up is the part the player can act on.

    Out goes SPARK_SOURCES and its inference over seven quest ids. That
    worked and was wrong at both edges: it could not see the catch-up routes
    the tooltip names -- delves, Mythic+, raids, instanced PvP, outdoor
    events -- so somebody who made up a missed spark in a delve still read
    as owing one; and it needed 96995 Turn Back the Surge held out by hand
    for listing Spark of Tides among rewards it does not pay. The residue
    count has no edges to get wrong. LIADRIN_WEEKLY stays; its own row uses
    it.

    No progress bar, though sparkProgress would feed one directly. Only the
    crest row measures itself on this page and loadcheck enforces it,
    because the renderer draws a bar for anything carrying a fraction -- so
    a second one is a visible change to the page rather than a change to
    this row. Worth doing deliberately, not as a side effect.

    Checked at four points, because a row that always says done is
    indistinguishable from a correct one if the fixture only ever sits at
    the cap: behind, caught up, overshot by a catch-up week, and the client
    not having answered at all -- where it has to hand back to a manual tick
    the way it did before any currency was involved.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 3183c2bf26599554e21edb9465436d90ae1552de
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 20:01:30 2026 +0200

    Ask the client what Tidal Spark Dust knows

    The weekly checklist infers "this week's spark is in" from seven quest
    flags. That workaround exists because counting Sparks of Tides in the
    bags cannot distinguish "not collected" from "collected and already
    spent", and the second is the normal case for anyone who crafts.

    Currency 3509 is residue: one per spark OBTAINED, and it does not go
    down when the spark is used. Its tooltip carries a Total and a "Current
    Season Maximum" -- which is a better answer than the row currently
    gives, because it says how far behind you are rather than just yes or
    no, and it counts the catch-up routes the quest list has never heard of.

    Not rewiring anything on that yet. The tooltip does not say which API
    fields carry those two numbers, and guessing at an API's shape is
    exactly how C_ItemUpgrade.GetHighWatermarkForItem spent a season
    returning a silent zero. /yh debug now prints what
    GetCurrencyInfo(3509) actually returns -- name, quantity, maxQuantity,
    totalEarned, useTotalEarnedForMaxQty and the weekly and account fields
    -- so the decision can be made on the answer instead of the guess.

    ns:GetCurrencyCapProgress already computed exactly this pair and was
    deleted as dead an hour ago. If the fields line up it comes back.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 9540caa5f6bad2d15f43941bfe17d78994c685c0
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 19:45:00 2026 +0200

    Open the Raid page on the half of it that works alone

    Boss Guide leads the sub-tab strip and is what the page opens on. Those
    are one change, not two: Shell:InitialSubTab falls back to tabs[1].id
    when there is no remembered tab, so list order decides both. There is no
    separate default to set, which also means reordering that list for looks
    would silently change which view opens -- hence the check.

    The overview needs a group before it has anything to say, so leading
    with it meant most visits opened on an empty state and had to be
    clicked out of. The guide works on your own.

    Which is also why its hint line is gone. "The Boss Guide tab works on
    your own" was the empty overview apologising and pointing somewhere
    else; now that somewhere else is where you already are, and a sentence
    telling you to go to the tab you just came from is worse than nothing.

    ns:SetRaidPageTab's own fallback moves to "guide" with it. Nothing
    passes nil today, but a page whose internal default disagrees with
    tabs[1] lights one tab and draws the other -- the exact desync
    Shell:Mount already carries a long comment about, from when Trinkets did
    it.

    No initial show/hide change was needed: Shell:Mount calls OnSubTab after
    Build, so SetRaidPageTab runs on mount and sorts the containers out.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 5096b604fb9d688f7cdd5eea8362f74778f8cb5f
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 19:38:06 2026 +0200

    Sweep the code nothing calls, after twice being told the wrong answer

    742 lines across 18 files: 16 file-local functions, 17 ns.* functions,
    and six members written and never read. Three passes, because deleting
    dead code exposes more of it -- MakeWaypointButton was the only caller
    of SetWaypoint and ParseWaypoint, and RefreshRaidPerformance took
    PerfAcquire, FormatDPS, GetCombatData and Launder with it. The scanner
    reports zero dead file-locals now.

    The scanner itself was wrong twice, both times reporting live code as
    dead, and both bugs are now written down in it:

      1. `^\s*` under re.M. \s matches newlines, so the anchor slides past
         line starts and a call three lines down counts as a definition. It
         flagged 75 live functions.
      2. Treating `ns.Foo(` at the start of a line as a definition. It is a
         CALL. With that in the pattern uses == defs for every function in
         the addon, and all 88 looked dead -- including AddGlowHighlight,
         used in fifteen places, and CreateConsumablesFrame, wired up an
         hour earlier.

    A definition is `function ns.Foo(`, `function ns:Foo(` or `ns.Foo =`,
    and references are counted across .lua .xml .toc .py .md, because
    ShellPages drives entry points by name string and so does the harness.

    The largest single piece was ns:RefreshRaidPerformance at 215 lines. The
    biggest cluster was ReadyCheck's buff detection -- UnitHasAuraByName,
    FindAura, FindWellFed, IsEating, EvalFood, FindFlask, FindVantus,
    FindBronze -- a per-buff FindAura scan each, superseded by the batched
    single-pass scanner whose own comment says it replaced them. Kept
    EATING_AURA_NAMES: that scanner still reads the table.

    Also gone: ns.EnsureGearWindowBuilt, which I added this morning "so the
    next caller does not reach for the flag". No caller wanted it.

    Deliberately kept, and not an oversight:

      * The game-fact constants nothing currently reads -- CRAFT_CREST_COST,
        CREST_SAVE_DISCOUNT, VAULT_BREAKPOINTS, VENOMSTONE_SLOTS,
        VENOMSTONES_PER_UPGRADE, VETERAN_EMBELLISH_RESERVE,
        SEASON_MPLUS_START, SEASON_RAID_START. Each is one line and each
        encodes something that had to be established about the game. They
        cost nothing at runtime and re-deriving them costs real work.
      * ns.LOOT_DIFF_COLORS, which is dead together with the `color =
        "GREEN"` keys in the LOOT_DIFFICULTIES table beneath it. Removing
        the table alone would leave those strings pointing at nothing, and
        removing both is churn through every row for five lines.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit a0acba302ec00f85ff3a7c66519c0fb00b1ca1b7
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 18:59:34 2026 +0200

    Consumables built its tab strip during the loading screen

    32 frames and 77 regions -- the panel, the filter bar with its two
    dropdowns, and a tab button, scroll frame and container for each of
    three tabs -- built at file scope for a page that has to be clicked to
    be seen. Load is now 74 frames and 161 -> 84 regions, from 256 and 901
    at the start of the day.

    Same shape as Progression: no events, no registration, one construction
    script, so it becomes the body of ns:CreateConsumablesFrame and
    Core\ShellPages.lua gets `create`. 73 locals move inside, against Lua's
    cap of 200. Deleting the pre-shell windows made this simpler than
    Progression was -- the shell is the only door in now, so there were no
    other call sites to teach.

    Two things had to come OUT of the wrapper: TAB_DEFS and
    ns:GetConsumablesTabs. Shell:Mount restores the saved sub-tab, resolves
    the tab list and draws the strip BEFORE it calls Build, so a page built
    on first open still has to answer that question with no frames in
    existence. GetConsumablesTabs reads TAB_DEFS and nothing else, which is
    what makes it able to. Its partner SetConsumablesTab stays inside: that
    one drives real tab buttons and the shell only calls it on a click.

    This is the failure mode worth having a check for, because it hides
    from anyone who looks twice -- an empty strip on the first visit and a
    correct one on the second. The harness now asks every lazily-built page
    for its sub-tabs at load, before the render phase can build anything,
    and fails a page that answers with nothing:

      consumables:lazy:3  loot:lazy:2  mythicplus:built:2

    The loot browser has been lazy since it was written and already kept
    its tab function outside; Mythic+ is still eager, so it is covered
    against the day it is not. Putting TAB_DEFS back inside the builder
    fails the check with the first-visit/second-visit sentence spelled out.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 28cdb1e933d1d1775f854c07116a93c8efc71e02
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 14:14:54 2026 +0200

    Remove the boss trainer and the pre-shell window it was built beside

    Two removals that turned out to be one. The raid trainer's arena was
    already switched off in the guide -- TRAINER_READY = false, with a note
    saying a trainer that is confidently wrong teaches the wrong reflex --
    and Mr. Yeeper had no display surface outside Core\AppFrame.lua, the
    window the shell replaced. Both were loading in full, every session,
    for nobody.

    Gone: RaidTrainer.lua and RaidTrainerScenarios.lua (6,626 lines),
    AppFrame.lua and Integrations\Dashboard.lua (2,004), and Advisor.lua,
    Snark.lua and FunStats.lua (2,343). Snark had no callers at all and had
    not for some time. With them go /yh train, /yh advisor, /yh introreset,
    /yh fun and /yh classic, the guide's "Test my knowledge" button, and
    the yeeper-voice skill.

    The shell was already the only door. OpenMain and OpenTo tried the
    shell, then the app frame, then the dashboard; the second and third
    rungs only ran when Core\Shell.lua was absent, which is a .toc that
    predates it. mainIsOpen asked all three. All of that is now one branch.

    What this leaves at login: 106 frames and 161 regions, from 256 and 901
    at the start of the day. memcheck totals 3216 KB, from 4157. The addon
    is 49,772 lines of Lua, down from 60,745.

    The harness loses 27 trainer checks and 2,964 lines with them -- it was
    a third of the file. Removing them mechanically left one orphaned
    L.eval whose if/else had been swallowed by an overlapping span, which
    still ran and still reached for ns.RaidTrainer; the first boundary rule
    I used walked back to a blank line, and Lua source inside the triple
    quotes is full of them. Anchoring on the `= L.eval(` line instead is
    what made it reliable. Also dropped: the trainer's garbage budget, its
    scenarios file in LC_DATA_FILES, the guideOnly-has-no-scenario
    assertion, and the pre-shell-window check added earlier today.

    README loses the Mr. Yeeper section and five command rows. ROADMAP's
    Cracked Keystone item kept its reasoning but lost its conclusion --
    the advisor was the home it proposed, so it now says it needs a new one.
    CHANGELOG is left alone; it is history, not description.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit b839d2f9a9716da9ce6098712b1be806e59e56d4
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 13:34:10 2026 +0200

    Progression drew its card grid during the loading screen

    26 frames and 198 regions of Mythic+, raid and delve cards, built at
    file scope, for a page that has to be clicked to be seen. Load is now
    129 frames and 282 regions, from 155 and 480 after the pre-shell window
    and 256 and 901 before any of this.

    Core\ShellPages.lua already had the mechanism -- `create`, looked up by
    name and run before the page mounts -- and the loot browser has used it
    since it was written. This is the second page on it, not a new idea.

    Wrapped whole rather than picked apart. The file is one long
    construction script with no events and no registration, so the smallest
    honest change is to stop running it until somebody asks: the header goes
    in above the first CreateFrame and an `end` at the bottom, and its 29
    locals become the function's, which is why they were counted first (Lua
    allows 200). ns:SetProgressionAppMode moves inside with them, which is
    right rather than tolerable -- ShellPages looks it up by name AFTER
    running the creator, and every other caller guards on it.

    The two doors that skip the shell -- Integrations\Dashboard.lua and the
    pre-shell window's ShowAppPage -- now ask for the page instead of
    testing for it, the same shape the loot branch beside them already used.

    The harness needed the creator added to its render list. Without it
    SetProgressionAppMode reads "absent", which is not a failure, so the
    whole page would have quietly lost its coverage in the same commit that
    made it lazy.

    Next two down the list are not this shape. Both Consumables and the
    raid trainer publish a small API that has to answer BEFORE their frame
    exists -- Shell:Mount resolves a page's sub-tabs and builds its tab
    strip before it calls Build, and RaidGuideUI asks the trainer
    HasScenario while drawing a page the trainer has never opened. Wrapping
    either one whole would take those with it.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 25e2aa544d4881ccd5f2e47c51fbc54bf69b805a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 13:25:12 2026 +0200

    The pre-shell window built a dashboard nobody can open

    Core\AppFrame.lua is the window that opened before Core\Shell.lua
    existed. Both ns:OpenMain and ns:OpenTo reach for the shell first, so
    the only way to land here is to have no shell at all -- which happens
    in exactly one situation: an update installed and reloaded rather than
    restarted, where the .toc predates Shell.lua and the file is not
    loaded. "Nothing happens" is the worst answer to a key press, so the
    fallback stays.

    Its cost did not. The file built its whole home dashboard at load --
    character bar, profile dropdown, crest bars, Yeeper panel, planner,
    vault strip, nav buttons -- 64 frames and 209 regions, the largest
    single contributor in the addon, for a window that in any normal
    session cannot be opened by any means. It now returns at line one when
    the shell can Toggle and Open, and builds nothing.

    Load across the addon is now 155 frames and 480 regions, from 219 and
    689 after the gear window, from 256 and 901 before either. memcheck
    totals 3804 KB, from 4157.

    Everything this file publishes is read behind an `if ns.X then` by
    every caller, so absent reads as "this build has no old window".
    ns:ToggleDashboard survives regardless -- Integrations\Dashboard.lua
    loads later and defines its own.

    That leaves 1700 lines with no coverage at all, reachable only in the
    one case nothing tests. So the harness now takes the shell away, runs
    the file again, and checks the window it should leave behind: the frame,
    ShowAppPage, ToggleApp, and that it lands on home rather than blank.
    Neutering the guard fails it in the other direction, which is the half
    that proves the saving is real.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 5b665c9032086456180e2028b7180bc5885d935a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 12:49:03 2026 +0200

    The gear window cost 44 frames at login to open at a vendor

    Core\UI.lua built sixteen slot buttons and a crest frame per track at
    file scope: 48 frames and 228 regions created during login, every login,
    for a window that opens at an upgrade vendor -- somewhere most sessions
    never go. It is now 11 and 16. Across the addon, load went from 256
    frames and 901 regions to 219 and 689.

    Nothing had measured this because nothing could. A frame's real cost is
    C memory the client does not attribute to the addon, so
    GetAddOnMemoryUsage never showed it; and Tools/memcheck.py models frames
    as plain Lua tables, so it reported 211 KB against this file and made it
    look like data. Counting CreateFrame calls per file is what found it --
    the same count says Core\AppFrame.lua is now the biggest at 64 and 209.
    memcheck's total drops 4157 KB to 3999.

    The container stays eager: eleven places test ns.MainFrame for
    existence and a nil one reads as "not part of this build". Everything
    inside it moves behind EnsureBuilt, called from the container's OnShow
    and from the two refreshes that write into the pieces it builds.
    ns.AppCrestFrames is published empty at load and FILLED on build rather
    than replaced, because Features\Gear\Crests.lua takes its reference at
    load and would otherwise write into a table nothing draws.

    The harness snapshots the window's state the instant loading finishes --
    read any later and it would only ever say "built", whatever the code
    did -- then checks both doors in, that a second build does not
    duplicate, and that the table identity holds. Removing BuildSlotButtons
    from EnsureBuilt fails it with a nil button inside RefreshAllSlots,
    which is the crash a player would have seen.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit ebff73a03c89cb174f8d6b435a0f8d95a60e2e14
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 12:06:22 2026 +0200

    Eight teleport spells that exist in no game, written down twice

    1289772..1289782 covered every Midnight Season 2 portal in a tidy
    contiguous run, and appear in no addon on disk, no database and no game.
    Every Midnight tile on the Teleports page and the Mythic+ page was a
    dead button. They survived because they were plausible, sequential, and
    kept in TWO tables that agreed with each other -- the Teleports page had
    one grouped by expansion, the Mythic+ page a second keyed by dungeon
    name.

    Corrected against BigWigs\Tools\Keystones.lua, which is hand-maintained
    per season, and cross-checked against EllesmereUI's SEASON_PORTALS.
    Where the two disagreed with us they agreed with each other:

      Altar of Fangs        1289772 -> 1286812
      Murder Row            1289775 -> 1286809
      Den of Nalorakk       1289773 -> 1286807
      The Blinding Vale     1289776 -> 1286801
      Voidscar Arena        1289777 -> 1286804
      Kings' Rest           1289778 -> 1286831 (+ the BfA 272261)
      Temple of Sethraliss  1289782 -> 1286828 (+ the BfA 272267)
      Ruby Life Pools       dropped its invented second id; it kept the
                            Dragonflight spell when it came back

    Two more were wrong in ways nobody would have connected to this. Siege
    of Boralus and The MOTHERLODE!! have one spell per faction, and both
    were filed with a single id -- so half of all players saw a locked tile
    they could never unlock. The MOTHERLODE's was 272268, which
    AllTheThings files under NeverImplemented: it shipped in the data and
    never in the game, so IsSpellKnown could not tell it from "you have not
    earned this yet". Also dropped 1248186, 1253942 and 1286119, three more
    candidates with no trace anywhere.

    Now one table, Features\Teleports\TeleportData.lua, read by both pages.
    A wrong id is still possible; a wrong id in only half the addon is not.
    The harness asserts no two dungeons claim the same spell, that every
    season name and Mythic+ alias resolves, and that a faction pair keeps
    both spells with the player's first -- and it stubs UnitFactionGroup,
    which it never had, so that branch runs at all. It also stops
    hardcoding a teleport id of its own, which was a third copy.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit cd269869d4432f4289995b0119634d12811a1e11
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 11:48:00 2026 +0200

    The query wanted an item and got a location, so nothing was ever free

    C_ItemUpgrade.GetHighWatermarkForItem takes itemInfo -- the link/name/id
    form -- and had been handed an ItemLocation since it was written. That
    does not return zero, it throws, and the pcall around it turned every
    throw into an honest-looking "this slot has been nowhere". /yh debug
    printed wm:none down all sixteen slots and every row priced its ranks as
    paid. The whole free-upgrade half of the advisor was switched off, in a
    way nothing could see.

    The harness could not have caught it: its stub took the location and
    answered cheerfully. A stub more forgiving than the client is a stub
    that certifies a bug, so it throws on a non-string now, and the gear
    fixtures carry slot-encoded links so the query has something of the
    right shape to ask about. Play/Stop/IsPlaying became real for the same
    reason -- a no-op that answers "not playing" forever makes every
    `if not anim:IsPlaying() then anim:Play()` read as correct.

    Underneath that, two answers instead of one. GetMarkRead is what the
    client said, zero for unread; GetFreeUpgradeIlvl is that raised by
    anything bound in the bags, and standing on the bags alone when the
    client says nothing. Which one a caller wants depends on the direction
    being wrong would hurt: "this rank is free" off a floor is always true,
    and a price off a floor over-charges for everything above the line. So
    the free-rank rules run on the floor and work with no answer at all,
    while GetMarkLaunder and GetBagLift guard on the read.

    Rings and trinkets keep one line between them and it follows the lower
    of the pair, so the floor pools both worn pieces with the bagged ones
    and takes the second highest: one 295 over a 292 and a 292 buys nothing,
    a second 295 frees a rank in both slots.

    New: RULE 0c for a piece still inside its trade window, saying what
    keeping it is worth and that letting the timer run out counts as
    keeping. A pulsing cyan border on gear cards whose ranks are already
    paid for. A "Collect N free gear upgrades" row leading Worth Doing
    Tonight -- the only row on that panel that can be finished during a
    loading screen.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 15e49d027f1a70b41778ab148b71a79f3cc8288a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Fri Aug 21 10:09:57 2026 +0200

    Commit the tree as it stands, before another trinket scrape

    Everything that was outstanding on this branch, taken together at the
    user's request so the next data run lands on a clean baseline rather
    than mixed into work in progress.

    Not all of it is mine. The crest-advice work -- the raid guide and its
    Encounter Journal reader, the weekly checklist, the Mythic+ data and the
    roadmap -- was in the tree already, along with WhatsNew.lua's removal and
    the two .toc entries that went with it. What is mine here is the
    bloodmallet re-scrape from earlier today (Protection Warrior's
    single-target list came off last season's data, Enhancement's 5-target
    list was re-simmed), the README coverage counts that follow from it, and
    Tools/memcheck.py.

    memcheck.py reports where the addon's memory goes, per file, by loading
    it against loadcheck's stub and collecting garbage around each file. The
    headline: about 4.1 MB, 40% of it generated data tables, and the trinket
    file alone is 563 KB of which 350 KB is per-row table overhead rather
    than data.

    No tag and no push, as asked.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 94a2484b279a478cb79b35eac44c3f1dbfbc3b3f
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 23:01:57 2026 +0200

    Putting it on is one of the ways it binds, not the exception to it

    The last commit wrote the rule down as "binding marks a slot, not
    equipping it", which reads as though wearing a piece were the case that
    does NOT mark. Wearing it is how most pieces bind. So is keeping one
    until its trade timer runs out. The mark is the default outcome of
    having looted something, and there is exactly one way to miss it: hand
    the piece to somebody else while there is still time.

    Nothing computed changes -- unbound is read off the tooltip's own lines
    either way -- but one of the sentences a player reads said it, and a
    line that teaches the wrong rule is worse than one that teaches none.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 26fb015d70450b7983019de191a97175bba85026
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 22:49:51 2026 +0200

    Binding is what marks a slot, and a fresh drop has not decided yet

    Not equipping it and not upgrading it: the moment a piece is soulbound
    the slot has its item level, and a piece handed back to the group inside
    its trade window never marks anything at all. The previous pass left
    that as an open question in a comment; it is written down now, where the
    next person to price against a mark will be standing.

    It has one consequence, and it lands on the row shipped last commit. A
    Hero drop still sitting in the bags with time on it has put nothing on
    the slot, so the Champion piece next to it prices at a hundred -- and
    costs twenty the moment the player decides to keep what they looted. The
    row was confidently telling them to hold out for a cheaper spare on the
    strength of that hundred.

    So a slot with an unbound piece above its mark says the price is not
    settled and names the piece it is waiting on, instead of advising. How
    far the mark would move is deliberately not predicted: rings and
    trinkets share one between two slots.

    Read off the tooltip's own trade-window and binds-when-equipped lines,
    from the client globals so it survives a locale, rather than inferred
    from "above the mark, so it must be unbound" -- an inference that is
    wrong for exactly the paired slots this matters most in.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 12d4c4fb469ad25c35beacd08be2ed6a32065363
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 22:22:54 2026 +0200

    The spare in your bags is the only trade between two crest tiers

    A slot remembers the highest item level it has held, and upgrades up to
    that level cost no crests. That makes a lower-track piece for the same
    slot the one way to pay for a rank with a crest you have too many of --
    and it is a piece that is never worn, so nothing here could see it.

    Bags are scanned for upgradeable pieces by the slot they belong to, and
    ns:GetMarkLaunder prices the move against what the slot has already
    reached rather than against how far down its track the spare sits. That
    distinction is the point: a Champion 1/6 under a slot that has seen 305
    is one rank and twenty crests, not five and a hundred, because the other
    four land under the mark. Finishing it then hands the worn Hero piece
    its own 305 to 308 for nothing.

    Where it really is a hundred -- a paired slot whose mark is still down
    at the lower of the two -- the row says so, points at the cheaper spare
    worth waiting for, and prints the price of not waiting. That is a
    judgement call and it is left as one.

    A slot the client has not answered a mark for says nothing at all. Every
    number here is a difference from that one, so a zero does not make the
    advice vague, it makes it wrong.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 575cc6a51a1c80fd7cd4ee4d5f163dc6afc14adc
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 18:56:38 2026 +0200

    One paper doll order, not two that had drifted

    The gear page and the best-in-slot page each drew a doll from their own
    hardcoded list, and the lists disagreed. Hands and Waist finished the
    left column on one and started the right column on the other, so the
    same character read differently depending on which page was open.

    The best-in-slot page had it right -- the character sheet's order, head
    down the left and hands down the right. That list moves to
    ns.DOLL_LAYOUT and both pages read it.

      left   Head, Neck, Shoulders, Back, Chest, Wrist, Trinket 1, Main Hand
      right  Hands, Waist, Legs, Feet, Ring 1, Ring 2, Trinket 2, Off Hand

    The weapons are one row in the layout, side "W", because the
    best-in-slot page draws them spanning both columns. A page that wants
    two flat columns gets ns:GetDollColumns, which splits that row so the
    main hand lands under the left column and the off hand under the right
    -- side by side on the same line, which is what the weapon row looks
    like anyway.

    The check pins the order by name rather than by slot id, since the id
    order is exactly what made this unreadable, and pins that every slot the
    advisor has an opinion about has somewhere to be drawn. A slot missing
    from both columns is one the page silently cannot show.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 0ea164ad3e82d330a67e4f0dc456367c2a752dd2
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 17:51:13 2026 +0200

    Stop saying "the mark", and say why one trinket beat the other

    "You have to set the mark before you upgrade the Hero piece" explains a
    mechanic nobody asked to be taught, in vocabulary that belongs to this
    file rather than to the game or the player. What a held row needs is the
    price, why it is a bad price right now, and the two things that would
    change it:

      100 Champion to take this from 1/6 to 308, and 6 of your pieces are
      this far down — 580 to cover them all.
      Too expensive to spend on a guess. Hold until a Hero 1/6 actually
      lands here — finishing this piece then starts it at 2/6 instead — or
      until a Champion piece turns up further up its track, where the same
      crests reach the top for less.

    The same word was in the pair lines, which had not been reworded when
    the hold hover was:

      before  caps the track; mark needs Trinket 2 at 308 too
      after   caps it, but Trinket 2 must reach 308 too

      before  Rings and trinkets share one mark and it follows the LOWER of
              the pair, so this moves nothing on its own...
      after   Rings and trinkets count as a pair and the lower of the two is
              what matters — Trinket 2 is at 305, so nothing changes for
              either slot until it reaches 308 as well.

    Guarded now across the row and every hover line at once, rather than in
    the one place it was last found.

    The tie-break also says why it won, which is the question two identical
    trinkets actually raise:

      On your best-in-slot list, so it comes before Trinket 2.

    Only when the pair genuinely split on it -- announcing best-in-slot
    about a piece with nothing to be chosen over is noise.

    Two smaller things while in here. The stranded warning no longer repeats
    the pair line: naming the partner as short, twice, reads as two separate
    problems. And a single rank is "its last rank" rather than "all 1 rank".

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit dab43dc68f5de06432b031bfd2984f1e699b7c78
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 16:59:44 2026 +0200

    Stop naming held slots as prerequisites, and let best-in-slot break ties

    Two things the panel got wrong at once, both visible on the same screen.

    IT CONTRADICTED ITSELF. Trinket 1 read "Hold 100 Champion" while Trinket
    2 read "Feet and Trinket 1 first" -- naming as a prerequisite the very
    slot it had just said not to spend on. The plan still ALLOCATES to a held
    slot, because the walk knows nothing about holding; the hold is applied
    afterwards, per slot, and the blocker list was reading the allocation.
    Both ask ns:IsHeldForDrops now, so Trinket 2 reads "Main Hand first".

    AND IT BROKE TIES ON SLOT NUMBER. Two trinkets at the same rank and item
    level are the same purchase to every term in the scoring, so the walk
    fell through to slot id -- Trinket 1 beat Trinket 2 because 13 is less
    than 14. When one is the spec's best-in-slot and the other is filler
    that is not a tie at all: crests on the keeper stay bought, crests on
    the filler leave when it does.

    The scraped guide and the player's own pins both count, pins first --
    someone who has pinned a choice has simmed it or decided it, and that
    outranks a page scraped in August.

    Deliberately only a tie-break. Best-in-slot says which of two equal
    purchases to make; it does not say a best-in-slot piece is worth more
    crests than a bigger upgrade elsewhere. The guide is also one opinion at
    one point in time, which ns.GEAR_CAVEAT already says at length.

    Both pinned, and both from the direction that discriminates: the
    contradiction check refuses vacuity if no row is held, and the tie-break
    check first shows the tie falling to slot order with no guide data, then
    that marking the SECOND trinket moves the crests against that order, then
    that marking the first moves them back.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit f20f9b9b8b164dbf16bfac83415aeabadfb0e50e
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 15:49:57 2026 +0200

    Keep the overlap warning on the slot it is headed with

    On a row titled Neck, three-quarters of the sentence was about a belt:

      20 Hero for 305 to 308 — a maxed Champion piece lands there too.
      Spend Champion on Waist +5 more (140 held); keep Hero for rank 3+

    All true, and the wrong place for it. Someone reading the Neck row is
    deciding about the neck, and the fact that decides it is that the first
    ranks of this track land on item levels the track below already reaches.

      Ranks 1-2 are wasted Hero — Champion reaches 308 too. Keep Hero for
      rank 3+

    Seventy-four characters against a hundred and thirty, and it says the
    thing the old one buried between two instructions about other slots.

    Where the cheaper crests could go is still worth knowing, so it moved to
    the hover along with the arithmetic that was also crowded out -- which
    ranks, at what item levels, and how many crests are at stake. Both
    branches of the rule share the row now and differ only in whether the
    hover has somewhere to point.

    The check pins that no other slot's name appears on the row. It cannot
    be a blanket rule -- "Main Hand and Feet first" is a legitimate thing for
    a held row to say -- so it is scoped to the overlap warning, where
    directing crests elsewhere is what went wrong.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 6b09daecaf8a3689ef11936cf9cbfe4b40a06f5c
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 15:05:22 2026 +0200

    Remember the high-water marks, so free upgrades can be seen

    RULE 0 is the best advice the addon has -- ranks the player already owns,
    for no crests at all, sorted above every row that costs something. It
    could not fire for most of the addon's runtime.

    RefreshWatermarks ran only while the upgrade vendor was open, and
    ns.watermarkCache was a plain table that died with the session. So on any
    fresh login away from a vendor every mark read zero, GetFreeUpgradeIlvl
    returned zero, and a slot sitting below a rank it had already reached was
    listed as a normal paid upgrade.

    The cache is persisted to SavedVariables now and restored at
    ADDON_LOADED, and the refresh is no longer gated on standing in front of
    an NPC -- if the API answers away from one the cache fills on its own,
    and if it does not, nothing is lost by asking. Marks only ever go up, so
    a remembered one is never wrong; at worst it is behind, and the live
    query corrects it the moment it answers. That asymmetry is what makes
    caching it safe.

    Nothing stubbed C_ItemUpgrade or ItemLocation, so this rule had never
    been exercised by any check ever written against the advisor -- the mark
    always read zero there too, and the branch was invisible from both sides
    at once. Both are stubbed now, keyed by equipment slot so a check can put
    a mark above the equipped item and watch the advice change. It covers the
    rule firing, that free sorts first, and that the mark survives a reload
    with the live query dark.

    The check clears its own stored mark on the way out. Slot 16 is the
    weapon every other gear fixture uses, and leaving a 305 watermark on it
    would have had them reporting free upgrades for reasons of ours.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 1ba4fa79673ae7ac08fdd1f55ee15899321dda43
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 14:44:35 2026 +0200

    Record that the mark is retroactive

    The hold-and-react rule turns on this and the comment stated it wrong.
    Finishing the lower-track piece is enough on its own: the mark applies
    retroactively and the piece does not have to be worn, so a Champion
    piece can be topped up after the Hero drop has already landed in that
    slot and the Hero piece still starts a rank higher.

    Confirmed against the game rather than derived, which is why it is
    written down. Nothing user-facing says it -- a player upgrading a piece
    does not need the mechanic explained, only the advice.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit ba37060060e68a364327d363fb1ddb04bee522b9
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 14:38:48 2026 +0200

    Say the player has no choice, not that the drop has one

    "A Hero drop picks its own slot" was carrying the whole argument for
    holding, and it reads as the drop having a preference. The point is the
    opposite: the player has no say in where one lands, which is exactly why
    the crests are worth keeping until it does.

      before  The mark has to be set before the Hero piece is upgraded, and
              a Hero drop picks its own slot — so the crests are worth more
              in hand, ready for whichever one it lands in.
      after   You have to set the mark before you upgrade the Hero piece,
              and you do not choose which slot one drops in — so the crests
              are worth more in hand, ready for whichever slot it turns out
              to be.

    Same for the line under it: "a guess at which one the drop will choose"
    is now "a guess at which slot the drop will land in".

    The check refuses the animated phrasing outright, since it is the sort
    of thing that reads fine while writing it and not at all while playing.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 2428749cc40ff5ef5d8102250d3debf4750e341c
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 14:36:33 2026 +0200

    Hold the crests the drop has not chosen a slot for yet

    Two corrections, and the second undoes something this branch removed in
    its first commit.

    THE MARK IS INSURANCE, NOT A RACE. The previous pass killed the rebate
    entirely on the reasoning that a +10 hands out 311 in every slot, so a
    308 mark is always overtaken. True, and useless -- of course better gear
    exists, the whole season is the trip to it. Some Hero pieces arrive at
    1/6, and a slot marked at 308 starts those at 2/6. That is worth having.

    But it is worth a FIXED amount, and the price of it varies fivefold. A
    Champion piece one rank from its cap buys the same insurance for 20 that
    a piece at 1/6 buys for 100. Applied as a cost discount the rebate
    helped the expensive runs most -- a hundred crests knocked down to
    seventy-three still looked like a bargain -- which is backwards. It is a
    term in the numerator now, priced as the item level the saved ranks are
    worth, and the cheap mark wins by the margin it should.

    It also carries the slot's weight. Unweighted it competed against a
    number four or five times its scale and never won: a trinket took a
    hundred crests to reach a cap whose mark could not move, beating a slot
    that would have banked something.

    AND THE RESERVE WAS RIGHT. The mark has to be set BEFORE the higher
    piece is upgraded, and a Hero 1/6 picks its own slot without asking. So
    on a track the content has outgrown, a deep run is a bet on which slot
    that will be -- six pieces at the bottom, one drop, same hundred crests
    either way:

      Hold 100 Champion — spend it when a Hero 1/6 lands here

    Only for runs deep enough to be a real bet. A piece three ranks out is
    cheap enough to just do, and the anti-hoarding rule stands everywhere
    else -- crests held with nothing to react to are power not worn.

      Best Champion: 60 for 298 to 308, all 3 ranks — caps it; a Hero 1/6
      drop here then starts at 2/6

    Recorded as still-unverified: everything above rests on the mark saving
    one rank against a 305 arrival, and on raid item level varying by boss
    rather than by difficulty. Neither is checkable from the harness.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit b1db07260dbb49990d4f1164ceef9e6ddb7794bc
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 12:31:09 2026 +0200

    A mark you can farm past is a race, not a rebate

    End-of-dungeon loot is not slot-specific. A +10 hands out 311 in any
    slot it likes, so a 308 mark is overtaken everywhere sooner or later,
    and the crests that set it bought item level until then and nothing
    after.

    The raid could still drop a 305 into that slot first and cash the mark
    in -- and raid item level is per BOSS, not per difficulty, so the same
    slot comes off an early boss low and a late one high. But that is a coin
    toss on which source arrives, and advice should not be built on winning
    it.

    So the rebate is measured against the HIGH end of the drop band rather
    than the low one. The low end says what a mark could pay out on; the
    high end says whether it survives. Champion caps at 308 under a 311 farm
    level, so its rebate is now zero -- and every Champion row says why:

      before  Best Champion: 100 for 292 to 308, all 5 ranks -- caps the
              track, next Hero free to 308
      after   Best Champion: 100 for 292 to 308, all 5 ranks -- tops out at
              308; you farm 311 in every slot

    which is a fact about the TRACK, so it reads the same on a row that only
    reaches 298. The old phrasing said "caps at 308" there, claiming
    something the run did not do.

    Two consequences worth naming.

    The Champion plan is stats-only now, so it orders by slot value again:
    Main Hand, then a trinket. Nothing on that track is banked, and the
    panel no longer pretends otherwise.

    And the pair rule moved with it. A pair only matters where a rebate
    exists to withhold, so the checks that pin it -- one trinket never
    carried alone, both finished together when affordable -- now run on
    Hero, whose 321 cap does clear the band. On Champion there was nothing
    left to withhold and they were passing for the wrong reason.

    Recorded but not built: the Loot Browser already scans the Encounter
    Journal per boss, per spec, per slot, which is the data that would make
    this exact instead of banded. It is also the scan a previous probe got
    wrong by reading item level off an unresolved link, so it wants doing
    carefully rather than quickly.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 49bbe2bcd0884f4a9bedc7548e684c96148a8615
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 11:02:13 2026 +0200

    Rings and trinkets share one mark, and it follows the lower

    The high-water mark on a paired slot only moves when BOTH pieces reach
    the level. Carrying one trinket to 308 while its partner sits at 292
    leaves the mark at 292: the next piece to land in either slot is lifted
    to 292 and no further. A hundred crests bought item level on one item
    and no rebate at all.

    The advisor did not know that, and it was the load-bearing assumption
    under the last three commits. It recommended exactly the spend that
    wastes the crests -- and that is the trap this was reported from in the
    first place, a season lost to maxing trinkets.

    So the rebate is now gated on the partner. A run that caps a ring or a
    trinket earns it only if the other half is already there, and the plan
    re-sorts itself around that:

      before  Main Hand -> 308, Trinket 1 -> 308, Trinket 2 -> 298
      after   Main Hand -> 308, Feet -> 308, Trinket 1 -> 298

    Feet is worth less by slot priority and more by outcome, because Feet is
    a slot of one. The pair is a single purchase priced at two, and with 200
    in hand it is not affordable, so nothing is spent pretending otherwise.

    Where a pair IS the right call the row says what the other half needs
    rather than promising a rebate that cannot land:

      caps the track; mark needs Trinket 2 at 308 too

    and the hover names the mechanic and the partner's actual level.

    Pinned from both directions, because a rule that only ever refuses is
    indistinguishable from one that never funds a trinket: with 200 no
    paired slot is carried alone, and with 600 both trinkets finish
    together.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit aa0fab559c283b30cd7590515ef9437467f3583a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 10:48:28 2026 +0200

    Say which floor, and whether the track can reach it

    "Under your 305 floor, so a drop replaces it" on a Veteran piece quotes
    a Hero raid drop that has nothing to do with the item, and is vacuously
    true besides -- Veteran stops at 295, so no Veteran rank can ever clear
    305. It fired on every row of the track without telling any of them
    apart.

    Two different situations were being given one sentence:

    A track that CAN cross the floor. Champion reaches 308 against a 305
    floor, so stopping at 298 is a stopgap the player can fix, and the fix
    is the useful half:

      40 Champion for 292 to 298, 2 of 5 ranks -- stops under the 305 you
      loot; 308 clears it

    A track that cannot. Veteran caps ten below the worst thing this
    character is handed, so the floor is not the point -- the track is:

      Best Veteran: 60 for 285 to 295, all 3 ranks -- Veteran tops out under
      everything you loot

    That second case turned out to be worse than reported. It is tested
    before everything else now, because reaching such a cap previously read
    "caps the track" -- which sounds like an achievement and hides that the
    mark it sets can never pay out. No drop will ever arrive below 295, so
    the last rank buys exactly as little as the first.

    The wording check grew a Veteran piece to reach that branch at all, and
    refuses a row that quotes 305 on a track that cannot see it.

    Unrelated, and not from this change: the weekly checklist check failed
    twice during this work with two different messages, and has passed seven
    consecutive runs since. Both failures landed while WeeklyChecklist.lua
    was being edited in the working tree, so the likely cause is the harness
    reading it mid-save rather than anything in the file.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit a020c0277582124e503de1debd681145055f5c71
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 10:13:52 2026 +0200

    The cap is the constraint, not where the crest comes from

    "You only earn Champion once Hero caps" is false. Champion is on offer
    all season -- M0, low keys, tier 7-10 delves, the weekly outdoor stuff
    -- and a player 60 short of their cap will find those 60 without being
    told how. What they cannot do is exceed the cap, and that is the only
    thing standing between them and the rest of the track.

    So the shortfall resolves against the allowance instead:

      before  4 more Champion pieces to max would cost 400, and you have
              140. You only earn Champion once Hero caps, so drops will fix
              those slots before crests do.
      after   4 more Champion pieces to max would cost 400, and you have
              140. The cap allows 120 more this season, so about 3 weeks --
              or a drop finishes any of them sooner.

    Which also removes a branch. The allowance grows at the same rate on
    every track, so an outgrown one gets the same answer in the same shape,
    and the special case that existed only to refuse a week count goes with
    it. `outgrown` still decides what a rank is WORTH -- rental against
    banked, off the drop band -- and has no business making claims about
    income. The "(overflow only)" tag on the rows that cannot afford
    anything was the same error and is gone too.

    The checks now reject "only earn", "overflow" and "trickles" outright,
    so this particular invention cannot come back quietly.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 00fd2b3a17135f2128edd623965b2726085e9ad1
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 09:57:48 2026 +0200

    Say it in ranks, and in sentences that parse

    Two rewrites, both reported as unreadable rather than wrong.

    RANKS, NOT ITEM LEVELS. "A Hero piece dropping here would start at 308
    instead of 305" states the payoff in a unit nobody carries in their
    head. The game shows gear as Hero 1/6 and players talk about it that
    way, so the sentence does too:

      The next Hero piece to land here then starts at 2/6 instead of 1/6
      -- 20 Hero you never spend.

    MarkRebate already computed both ladder positions to get the difference
    and returned only the difference; it hands back all three now.

    AND ONE THOUGHT PER SENTENCE. This was the whole of what a track had
    left to say:

      Champion: 4 pieces and 400 crests left against 140 in hand, and it
      only comes in behind Hero now, so a drop in those slots is the faster
      route.

    which is the state, the income model and the verdict in one breath, and
    none of the three survives it. "Against 140 in hand" leaves the
    comparison implied, "comes in behind Hero" is the overflow rule
    compressed past the point of meaning anything, and "those slots" points
    at nothing the reader can see.

      4 more Champion pieces to max would cost 400, and you have 140. You
      only earn Champion once Hero caps, so drops will fix those slots
      before crests do.

    The checks now refuse item-level phrasing where a rank belongs, so the
    first of those cannot come back by accident.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 26a42d6f4ab270ce26ba2ddd5394bbb8be701f97
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 09:51:45 2026 +0200

    Say it in three lines, not five

    The hover had grown into a briefing: a progress count, a shortfall, the
    shape of what was left, a drops-or-crests verdict and a warning against
    saving up. Every line true and the whole thing unreadable -- nobody
    hovers a row to be briefed.

    The progress count went first. "0 of your 16 slots are already at 308 or
    better, and this makes 1" is filler on a good day and absurd on the day
    it fires, which is any character who has not started the track.

    The shortfall was the real failure. "260 short" is a number, not a
    decision, and it sat there as one. It now ends in the answer instead:
    about three weeks, or a drop in any of them. Counted the way a player
    counts -- weeks until the track is done, this one included -- where the
    old field reported only the resets beyond the season allowance and
    answered "2" to a question whose honest answer is 3.

    What replaces the four dropped lines is one that was missing.

    Reported from a real season: advice to max trinkets drained the wallet,
    and a Hero piece one rank from its cap could not be finished. The season
    cap was already reached, so there was no going back for it, and the Myth
    piece that wanted that slot's mark waited a reset. Twenty crests, a week
    late. Run scoring makes that rare on its own -- a one-rank completion is
    the best value per crest on any page -- but a high-priority deep run can
    still outbid one, and that is exactly when the row has to say so:

      Leaves Ring 2 20 short of its own cap, and the season cap is reached
      -- that one waits for reset.

    Both new branches are pinned, since neither is reachable from the
    standing fixture: the week arithmetic across all three shortfall shapes,
    and the stranded completion including that it does not cry wolf on the
    row being left behind.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit d272909746547ce3d2b14e4c9d3d5027b7e20536
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 02:34:08 2026 +0200

    Turn the shortfall into a decision

    "120 short" is a number, not something anyone can act on. The same
    figure is an evening's work or half a season, and the panel stated it
    and stopped.

    Three things decide which:

    WHERE THE CAP SITS. A shortfall inside this season's allowance is not a
    wait at all -- those crests are sitting in content the player has not
    run yet, and telling them to come back next reset is wrong. Only the
    part beyond the allowance costs resets, at CREST_WEEKLY_INCREMENT a
    week. That constant has been defined and read by nothing since it was
    written; this is its first caller.

    An outgrown track gets no week count on purpose. Its income is whatever
    spills out of a higher track capping, and nothing the addon can see says
    when that lands, so it states the condition rather than inventing "2
    resets".

    THE SHAPE OF WHAT IS LEFT. 260 crests across three pieces a rank from
    their cap is a drop away; the same 260 across three pieces at 1/6 is
    most of a season, and those slots are cheaper to replace than to carry.
    Same total, opposite advice -- so the remaining pieces are sorted and
    the nearest and deepest reported, and the line splits three ways:
    all close, uneven (name the cheap one), or all deep (chase gear first).

    AND NOT HOARDING. Crests held toward a total that will not be reached
    are power not being worn for every week until a drop resets the plan
    anyway. It fires exactly where saving up is the tempting mistake: a real
    shortfall with something affordable sitting in front of it.

      4 pieces left after this, 400 crests to finish them, 140 in hand, so
      260 short -- and Champion only trickles in behind a higher track now.
      All of them are 5+ ranks down, which a drop does for free. Chase gear
      in those slots before crests.
      Spend what you have anyway -- crests in the bag are power you are not
      wearing.

    Trimmed while adding: "drop" was being said in three separate lines, and
    the slots that need one folded into the progress count where they read
    as part of the same thought.

    The timing arithmetic is pinned across all three branches -- enough in
    hand, short inside the allowance, short beyond it -- since none of them
    were reachable from the standing fixture.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 580b6f0100a81c17c49e6938ac63306355cf5e09
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 02:21:49 2026 +0200

    Say what the upgrade does, not how the system works

    The hover was a rules lecture dressed as advice:

      Champion tops out at 308 and the item stops there; it does not move
      onto the Hero track.
      What carries on is the slot. Its mark sits at 308, so the next Hero
      piece to land here is lifted to 308 free -- worth 20 Hero.
      Everything you are handed arrives on the Hero track (305 and up),
      which Champion crests cannot upgrade.

    Every sentence true, none of them about this character. It was the
    reasoning behind the feature transcribed into the panel, which is a
    thing an author finds interesting and a player does not. Nobody hovers a
    recommendation to learn how the game works. They hover it to find out
    why THIS one.

      Maxes Main Hand -- the last 3 ranks Champion can give it, for 60
      crests.
      A Hero piece dropping here would start at 308 instead of 305 -- 1 free
      rank, 20 Hero you never spend.
      6 of your 16 slots are already at 308 or better, and this makes 7.
      3 pieces left after this, 260 crests to finish them, and 140 in hand,
      so 120 short.
      2 slots are below 308 and need a drop, not crests.

    Same mechanics, stated as what they do. "A Hero piece dropping here
    would start at 308" says everything the paragraph about high-water marks
    said, about the player's own weapon.

    The missing idea was that a track has a FINISH LINE, and it is not the
    achievement -- it is the moment every slot sits at or above the track's
    cap, because from then on the crest has nothing left to buy.
    GetTrackCompletion counts it, and slots count as done however they got
    there: a Hero piece at 311 is past the Champion cap and will never want
    a Champion crest, so it is finished for this purpose without a crest
    being spent on it.

    That turns a spend into progress instead of a purchase. "3 pieces and
    you never need Champion again, then it is only Hero and Myth that
    matter" is a reason to act; "Champion tops out at 308" is a fact to
    memorise.

    The style is pinned. The check refuses "tops out at", "does not move
    onto", "What carries on", "A rank above" and "cannot upgrade" outright,
    and requires the hover to name the slot, say what maxing buys on the
    piece that replaces it, and say how far along the track is -- so the
    next pass cannot drift back into explaining the system.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit a65f46a0f367b884cd3a9e2359ecf12b240ca488
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 02:04:11 2026 +0200

    Finishing a track pays the slot, not the item

    An item does not move onto the next track. A Champion piece at 6/6 is
    308 and is finished forever. The previous commit had it climbing onto
    Hero and told the player so on the panel, which is wrong twice over --
    about the mechanic, and about what the spend is worth.

    What carries on is the SLOT. Its high-water mark now sits at 308, so the
    next Hero piece to land there -- arriving at 305 -- is lifted to 308 for
    nothing. The crests saved are HERO crests, on an item not owned yet.

    That inverts a weight. The old promotionCovered discounted the payoff
    when the content already dropped the next track, on the theory that a
    drop was coming anyway. The opposite is true: the mark only ever pays
    out when such a piece DOES land, so a track the player is handed often
    is what makes finishing worth doing. MarkRebate reads the size off the
    drop band instead of the flat two ranks TRACK_FREE_RANKS names -- a 305
    raid piece is lifted one rank, a 311 key piece starts above the mark and
    owes it nothing -- and applies it to the run's COST, because 20 Hero
    saved is worth more than the 20 Champion it cost.

      before  292 to 308, all 5 ranks -- finishes the track, so the piece
              moves to Hero and can keep climbing (100 of your 200 Champion)
      after   100 Champion for 292 to 308, all 5 ranks -- caps the track,
              next Hero free to 308

    Which is also 50 characters shorter, and it needed to be: the Gear page
    set SetWordWrap(false) on a fixed 42px row, so every sentence long
    enough to need the space was cut mid-word -- the advice with most to say
    was the advice you could not read. The reason now wraps and the row
    measures itself, the way Core/UI.lua has rendered this same list all
    along, and the fixed 42/46 pair is named constants so they cannot drift.

    The reasoning the short line had to drop moved to a hover: the
    arithmetic, both ends of the drop band, what the mark does, and where
    the whole track's budget stands.

    That last one is new. Every question in this file was asked one slot at
    a time, and the most useful one about a track cannot be -- "can I finish
    this at all" is a fact about sixteen slots and one wallet. Without it
    the panel opens a 100-crest climb on the fifth Champion piece while four
    others sit in the same state and the wallet holds 200: every row true,
    the set of them describing a plan that cannot happen.

      Champion: 5 pieces, 460 to finish them all, you hold 200. Enough for
      2 -- Main Hand, Trinket 1. The other 3 want drops, not crests.

    GetGearCensus counts the whole doll, old-season and empty slots
    included, because those are demand that has not arrived yet. Cheapest
    completions first, since topping out is worth the same on every slot but
    costs whatever that piece has left to climb.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 5c5b428a689d54830bb15407f228f8b23e78eda7
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 01:42:35 2026 +0200

    Plan crest runs, not ranks

    The walk bought one rank at a time and re-picked the best slot after
    each, so equal-priority slots interleaved by item level. That reads like
    fairness and costs the player the only thing on a track worth saving
    for: the cap. Reaching it promotes the piece onto the next track, and
    the ranks nearest the cap are the ones landing high enough for the slot
    to keep them -- neither of which a one-rank lookahead can see.

    On the panel that prompted this, 160 Champion bought one piece to its
    cap and stranded two trinkets mid-track. The same crests finish two.

    Every candidate now offers every run it could still buy -- one rank, two,
    up to its cap -- scored on value kept per crest, best run wins whole. A
    promotion only counts toward a run the wallet can actually finish, which
    is what stops the plan talking itself into a climb it cannot complete.
    Runs are chosen whole but RECORDED rank by rank, so the steps list, the
    running total and the paid/unpaid line are untouched.

    What a rank is worth is no longer its item level. It is its item level
    times how long the player keeps it, which the drop band decides: above
    the band it is permanent, inside it the slot's mark refunds it when the
    weaker source fills the slot, below it any drop clears it. ns.PLAN_VALUE
    carries the weights and why they are coarse.

    The strings answered "what does this buy" and never "does any of it
    survive", and they had to CHOOSE between the spend-here-first lead and
    the clause explaining that -- first won, which is why a weapon and a
    trinket in identical positions got opposite advice. Both print now, and
    the lead names its wallet, so five tracks no longer each claim to be THE
    first thing to do.

      before  Spend here first: 298 to 302, all 3 ranks to 308
              (60 of your 160 Champion to spend)
      after   Best Champion spend: 298 to 308, all 3 ranks -- finishes the
              track, so the piece moves to Hero and can keep climbing
              (60 of your 200 Champion)

    "N more Champion covers this one too" read as an instruction to farm
    Champion. On an outgrown wallet nothing the player runs pays that tier
    directly; it arrives by capping a higher one and spilling down, which is
    a different plan for the week, so the row says so.

    riskRank no longer orders the walk. Risk is a gap against a ceiling built
    from the raid track's MAX -- ten levels above what the raid drops -- so
    on a Heroic profile every slot below Hero reads "high" and the signal is
    flat. The band replaces it and is measured against the right number.
    Also drops a GetReplacementRisk call per slot that nothing read.

    Two checks added: the plan outcome on the original numbers, and the
    sentences the panel builds from it.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit a88ccc65a57551f0060e7cd9d7f4fbce58c1cc63
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Thu Aug 20 01:24:00 2026 +0200

    Stop reserving crests the content has outgrown

    The Improvements panel reported a 200-crest Champion wallet as 160 and
    spent it in an order that put two rows under "Spend here first". The 40
    missing crests were a reserve, held back for the next drop.

    But Champion caps at 308 and this character is handed 305 by the raid
    and 311 by a +10 key. Every piece they get arrives on the Hero track,
    which Champion crests cannot upgrade -- so there is no future drop the
    reserve could ever pay for. It was retiring item levels the player could
    be wearing, in a currency with nowhere else to go, and the 40 it held
    were exactly what a second piece needed to reach its track cap.

    GetDropCeiling already computed the two numbers this turns on and
    returned only the higher one. GetDropBand returns both: a rank landing
    under the low end is rental, one landing between the ends banks a refund
    the weaker source pays out through the slot's high-water mark.

    Worth recording that this leaves the reserve unreachable. It needs a
    capped track with no income and a "high" risk slot, and risk is a 20+
    gap against a ceiling built from the raid track's max -- which no Hero
    or Myth slot can ever show. Every wallet that could reach the branch was
    one the player had outgrown, which is where reserving is wrong. The gate
    is the correct semantic and stays; the risk model is due to be
    reconciled against the band.

    Also here, both small and both long overdue:

    The advisor and /yh discounts named the achievement "of the Dawn" while
    checking Season 2 ids -- players were sent to look up an achievement
    that no longer matched. One shared suffix now, guarded by a check.

    GetAchievementBlockers exposes the split GetAchievementProgress was
    already making and throwing away. Rule 5.5 only speaks when every
    remaining slot is upgradeable, so the moment one slot needs a drop the
    addon worked the whole thing out and said nothing -- silent in exactly
    the case worth naming.

    ROADMAP carries the rest of the model: promotion-aware planning, one
    global order, scarcity derived from CREST_SOURCES rather than declared.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This mod has no additional files